mirror of https://git.dotclear.org/dev/dotclear
Remove CB as submodule, includes it into inc/helper (keep unit tests), addresses #202
parent
e27742b64e
commit
a2b9d4f15c
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
use atoum\atoum\reports\coverage;
|
||||
use atoum\atoum\writers\std;
|
||||
|
||||
$script->addDefaultReport();
|
||||
|
||||
$coverage = new coverage\html();
|
||||
$coverage->addWriter(new std\out());
|
||||
$coverage->setOutPutDirectory(__DIR__ . '/coverage/html');
|
||||
$runner->addReport($coverage);
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
use atoum\atoum;
|
||||
use atoum\atoum\reports;
|
||||
|
||||
// Enable extension
|
||||
$extension = new reports\extension($script);
|
||||
$extension->addToRunner($runner);
|
||||
|
||||
// Write all on stdout.
|
||||
$stdOutWriter = new atoum\writers\std\out();
|
||||
|
||||
// Generate a CLI report.
|
||||
$cliReport = new atoum\reports\realtime\cli();
|
||||
$cliReport->addWriter($stdOutWriter);
|
||||
|
||||
// Xunit report
|
||||
$xunit = new atoum\reports\asynchronous\xunit();
|
||||
$runner->addReport($xunit);
|
||||
|
||||
// Xunit writer
|
||||
$writer = new atoum\writers\file('tests/atoum.xunit.xml');
|
||||
$xunit->addWriter($writer);
|
||||
|
||||
$runner->addTestsFromDirectory('tests/unit/');;
|
||||
$runner->addReport($cliReport);
|
|
@ -9,3 +9,4 @@
|
|||
/vendor
|
||||
/composer.phar
|
||||
/doxygen
|
||||
/coverage
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
[submodule "inc/libs/clearbricks"]
|
||||
path = inc/libs/clearbricks
|
||||
url = git@git.dotclear.org:dev/clearbricks.git
|
||||
branch = master
|
8
Makefile
8
Makefile
|
@ -17,14 +17,6 @@ config: clean config-stamp
|
|||
mkdir -p ./$(DC)/locales
|
||||
cp -pRf ./locales/README ./locales/en ./locales/fr ./$(DC)/locales/
|
||||
|
||||
## Remove tests directories and test stuff, idem for doxygen documentation
|
||||
rm -fr ./$(DC)/inc/libs/clearbricks/tests ./$(DC)/inc/libs/clearbricks/composer.* \
|
||||
./$(DC)/inc/libs/clearbricks/.atoum.* ./$(DC)/inc/libs/clearbricks/vendor \
|
||||
./$(DC)/inc/libs/clearbricks/bin ./$(DC)/inc/libs/clearbricks/_dist \
|
||||
./$(DC)/.atoum.* ./$(DC)/tests ./$(DC)/coverage \
|
||||
./$(DC)/composer.* \
|
||||
./$(DC)/doxygen ./$(DC)/clearbricks/doxygen
|
||||
|
||||
## Create cache, var, db, plugins, themes and public folders
|
||||
mkdir ./$(DC)/cache ./$(DC)/var ./$(DC)/db ./$(DC)/plugins ./$(DC)/themes ./$(DC)/public
|
||||
cp -p inc/.htaccess ./$(DC)/cache/
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../vendor/atoum/atoum/bin/atoum)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/vendor/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) {
|
||||
include("phpvfscomposer://" . __DIR__ . '/..'.'/vendor/atoum/atoum/bin/atoum');
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
include __DIR__ . '/..'.'/vendor/atoum/atoum/bin/atoum';
|
|
@ -13,7 +13,10 @@
|
|||
"php": ">=7.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "1.9.x-dev"
|
||||
"phpstan/phpstan": "1.9.x-dev",
|
||||
"atoum/atoum": "4.1.0",
|
||||
"atoum/reports-extension": "4.0.0",
|
||||
"fakerphp/faker": "1.21.0"
|
||||
},
|
||||
"config": {
|
||||
"bin-dir": "bin/"
|
||||
|
|
|
@ -4,9 +4,222 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "26c89c6a9a4e4605265683ce75d4e79f",
|
||||
"content-hash": "913c261bf25bb7890a0e28fcb4bf56b6",
|
||||
"packages": [],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "atoum/atoum",
|
||||
"version": "4.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/atoum/atoum.git",
|
||||
"reference": "e866f3d4ad683c35757cd73fc6da3e3d5e563667"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/atoum/atoum/zipball/e866f3d4ad683c35757cd73fc6da3e3d5e563667",
|
||||
"reference": "e866f3d4ad683c35757cd73fc6da3e3d5e563667",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-hash": "*",
|
||||
"ext-json": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"ext-xml": "*",
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"replace": {
|
||||
"mageekguy/atoum": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.2"
|
||||
},
|
||||
"suggest": {
|
||||
"atoum/stubs": "Provides IDE support (like autocompletion) for atoum",
|
||||
"ext-mbstring": "Provides support for UTF-8 strings",
|
||||
"ext-xdebug": "Provides code coverage report (>= 2.3)"
|
||||
},
|
||||
"bin": [
|
||||
"bin/atoum"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"classes/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Frédéric Hardy",
|
||||
"email": "frederic.hardy@atoum.org",
|
||||
"homepage": "http://blog.mageekbox.net"
|
||||
},
|
||||
{
|
||||
"name": "François Dussert",
|
||||
"email": "francois.dussert@atoum.org"
|
||||
},
|
||||
{
|
||||
"name": "Gérald Croes",
|
||||
"email": "gerald.croes@atoum.org"
|
||||
},
|
||||
{
|
||||
"name": "Julien Bianchi",
|
||||
"email": "julien.bianchi@atoum.org"
|
||||
},
|
||||
{
|
||||
"name": "Ludovic Fleury",
|
||||
"email": "ludovic.fleury@atoum.org"
|
||||
}
|
||||
],
|
||||
"description": "Simple modern and intuitive unit testing framework for PHP 5.3+",
|
||||
"homepage": "http://www.atoum.org",
|
||||
"keywords": [
|
||||
"TDD",
|
||||
"atoum",
|
||||
"test",
|
||||
"unit testing"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/atoum/atoum/issues",
|
||||
"source": "https://github.com/atoum/atoum/tree/4.1"
|
||||
},
|
||||
"time": "2022-11-20T20:18:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "atoum/reports-extension",
|
||||
"version": "4.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/atoum/reports-extension.git",
|
||||
"reference": "5668fc693f0cc484edede1825d23f2b1dce48ef8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/atoum/reports-extension/zipball/5668fc693f0cc484edede1825d23f2b1dce48ef8",
|
||||
"reference": "5668fc693f0cc484edede1825d23f2b1dce48ef8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"atoum/atoum": "^4.0",
|
||||
"php": "^7.2 || ^8.0.0",
|
||||
"symfony/filesystem": "^5.0",
|
||||
"twig/twig": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"configuration.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"atoum\\atoum\\reports\\": "classes"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "jubianchi",
|
||||
"email": "contact@jubianchi.fr"
|
||||
}
|
||||
],
|
||||
"description": "atoum Reports extension",
|
||||
"homepage": "http://www.atoum.org",
|
||||
"keywords": [
|
||||
"TDD",
|
||||
"atoum",
|
||||
"atoum-extension",
|
||||
"reports",
|
||||
"test",
|
||||
"unit testing"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/atoum/reports-extension/issues",
|
||||
"source": "https://github.com/atoum/reports-extension/tree/4.0.0"
|
||||
},
|
||||
"time": "2021-02-05T14:58:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fakerphp/faker",
|
||||
"version": "v1.21.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/FakerPHP/Faker.git",
|
||||
"reference": "92efad6a967f0b79c499705c69b662f738cc9e4d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/FakerPHP/Faker/zipball/92efad6a967f0b79c499705c69b662f738cc9e4d",
|
||||
"reference": "92efad6a967f0b79c499705c69b662f738cc9e4d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"psr/container": "^1.0 || ^2.0",
|
||||
"symfony/deprecation-contracts": "^2.2 || ^3.0"
|
||||
},
|
||||
"conflict": {
|
||||
"fzaninotto/faker": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.4.1",
|
||||
"doctrine/persistence": "^1.3 || ^2.0",
|
||||
"ext-intl": "*",
|
||||
"phpunit/phpunit": "^9.5.26",
|
||||
"symfony/phpunit-bridge": "^5.4.16"
|
||||
},
|
||||
"suggest": {
|
||||
"doctrine/orm": "Required to use Faker\\ORM\\Doctrine",
|
||||
"ext-curl": "Required by Faker\\Provider\\Image to download images.",
|
||||
"ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.",
|
||||
"ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.",
|
||||
"ext-mbstring": "Required for multibyte Unicode string functionality."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "v1.21-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Faker\\": "src/Faker/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "François Zaninotto"
|
||||
}
|
||||
],
|
||||
"description": "Faker is a PHP library that generates fake data for you.",
|
||||
"keywords": [
|
||||
"data",
|
||||
"faker",
|
||||
"fixtures"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/FakerPHP/Faker/issues",
|
||||
"source": "https://github.com/FakerPHP/Faker/tree/v1.21.0"
|
||||
},
|
||||
"time": "2022-12-13T13:54:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "1.9.x-dev",
|
||||
|
@ -66,6 +279,520 @@
|
|||
}
|
||||
],
|
||||
"time": "2022-12-22T17:02:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/container",
|
||||
"version": "dev-master",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/container.git",
|
||||
"reference": "90db7b9ac2a2c5b849fcb69dde58f3ae182c68f5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/container/zipball/90db7b9ac2a2c5b849fcb69dde58f3ae182c68f5",
|
||||
"reference": "90db7b9ac2a2c5b849fcb69dde58f3ae182c68f5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.4.0"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Container\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common Container Interface (PHP FIG PSR-11)",
|
||||
"homepage": "https://github.com/php-fig/container",
|
||||
"keywords": [
|
||||
"PSR-11",
|
||||
"container",
|
||||
"container-interface",
|
||||
"container-interop",
|
||||
"psr"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-fig/container/issues",
|
||||
"source": "https://github.com/php-fig/container/tree/master"
|
||||
},
|
||||
"time": "2022-07-19T17:36:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "dev-main",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/deprecation-contracts.git",
|
||||
"reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/1ee04c65529dea5d8744774d474e7cbd2f1206d3",
|
||||
"reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "3.3-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"url": "https://github.com/symfony/contracts"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"function.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-25T10:21:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "5.4.x-dev",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "ac09569844a9109a5966b9438fc29113ce77cf51"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/ac09569844a9109a5966b9438fc29113ce77cf51",
|
||||
"reference": "ac09569844a9109a5966b9438fc29113ce77cf51",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-mbstring": "~1.8",
|
||||
"symfony/polyfill-php80": "^1.16"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Filesystem\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/5.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-09-21T19:53:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "dev-main",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
|
||||
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"provide": {
|
||||
"ext-ctype": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-ctype": "For best performance"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Ctype\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gert de Pagter",
|
||||
"email": "BackEndTea@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for ctype functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"ctype",
|
||||
"polyfill",
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "dev-main",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
||||
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"provide": {
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "For best performance"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Mbstring\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for the Mbstring extension",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"mbstring",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"version": "dev-main",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
|
||||
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Php80\\": ""
|
||||
},
|
||||
"classmap": [
|
||||
"Resources/stubs"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ion Bazan",
|
||||
"email": "ion.bazan@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "3.x-dev",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/Twig.git",
|
||||
"reference": "e5c87f882183134a0e56845fd3e2e63c4a10396a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/e5c87f882183134a0e56845fd3e2e63c4a10396a",
|
||||
"reference": "e5c87f882183134a0e56845fd3e2e63c4a10396a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/polyfill-ctype": "^1.8",
|
||||
"symfony/polyfill-mbstring": "^1.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"psr/container": "^1.0",
|
||||
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.4-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Twig\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com",
|
||||
"homepage": "http://fabien.potencier.org",
|
||||
"role": "Lead Developer"
|
||||
},
|
||||
{
|
||||
"name": "Twig Team",
|
||||
"role": "Contributors"
|
||||
},
|
||||
{
|
||||
"name": "Armin Ronacher",
|
||||
"email": "armin.ronacher@active-4.com",
|
||||
"role": "Project Founder"
|
||||
}
|
||||
],
|
||||
"description": "Twig, the flexible, fast, and secure template language for PHP",
|
||||
"homepage": "https://twig.symfony.com",
|
||||
"keywords": [
|
||||
"templating"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/twigphp/Twig/issues",
|
||||
"source": "https://github.com/twigphp/Twig/tree/3.x"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-12-15T10:46:42+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
|
|
|
@ -1150,6 +1150,19 @@ class dcUpgrade
|
|||
);
|
||||
}
|
||||
|
||||
if (version_compare($version, '2.25', '<')) {
|
||||
// A bit of housecleaning for no longer needed folders
|
||||
self::houseCleaning(
|
||||
// Files
|
||||
[
|
||||
],
|
||||
// Folders
|
||||
[
|
||||
'inc/libs',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
dcCore::app()->setVersion('core', DC_VERSION);
|
||||
dcCore::app()->blogDefaults();
|
||||
|
||||
|
|
|
@ -0,0 +1,260 @@
|
|||
<?php
|
||||
/**
|
||||
* @package Clearbricks
|
||||
*
|
||||
* Tiny library including:
|
||||
* - Database abstraction layer (MySQL/MariadDB, postgreSQL and SQLite)
|
||||
* - File manager
|
||||
* - Feed reader
|
||||
* - HTML filter/validator
|
||||
* - Images manipulation tools
|
||||
* - Mail utilities
|
||||
* - HTML pager
|
||||
* - REST Server
|
||||
* - Database driven session handler
|
||||
* - Simple Template Systeme
|
||||
* - URL Handler
|
||||
* - Wiki to XHTML Converter
|
||||
* - HTTP/NNTP clients
|
||||
* - XML-RPC Client and Server
|
||||
* - Zip tools
|
||||
* - Diff tools
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
* @version 1.4
|
||||
*/
|
||||
define('CLEARBRICKS_VERSION', '1.4');
|
||||
|
||||
// Autoload for clearbricks
|
||||
class Autoload
|
||||
{
|
||||
public $stack = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
spl_autoload_register([$this, 'loadClass']);
|
||||
|
||||
/*
|
||||
* @deprecated since 1.3, use Clearbricks::lib()->autoload() instead
|
||||
*/
|
||||
$GLOBALS['__autoload'] = &$this->stack;
|
||||
}
|
||||
|
||||
public function loadClass(string $name)
|
||||
{
|
||||
if (isset($this->stack[$name]) && is_file($this->stack[$name])) {
|
||||
require_once $this->stack[$name];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add class(es) to autoloader stack
|
||||
*
|
||||
* @param array $stack Array of class => file (strings)
|
||||
*/
|
||||
public function add(array $stack)
|
||||
{
|
||||
if (is_array($stack)) {
|
||||
$this->stack = array_merge($this->stack, $stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the source file of a registered class
|
||||
*
|
||||
* @param string $class The class
|
||||
*
|
||||
* @return mixed source file of class, false is not set
|
||||
*/
|
||||
public function source(string $class)
|
||||
{
|
||||
if (isset($this->stack[$class])) {
|
||||
return $this->stack[$class];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class Clearbricks
|
||||
{
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* @var Autoload instance
|
||||
*/
|
||||
private $autoloader;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Singleton mode
|
||||
if (self::$instance) {
|
||||
throw new Exception('Library can not be loaded twice.', 500);
|
||||
}
|
||||
self::$instance = $this;
|
||||
|
||||
$this->autoloader = new Autoload();
|
||||
|
||||
$this->autoloader->add([
|
||||
// Common helpers
|
||||
'crypt' => __DIR__ . '/common/lib.crypt.php',
|
||||
'dt' => __DIR__ . '/common/lib.date.php',
|
||||
'files' => __DIR__ . '/common/lib.files.php',
|
||||
'path' => __DIR__ . '/common/lib.files.php',
|
||||
'form' => __DIR__ . '/common/lib.form.php',
|
||||
'formSelectOption' => __DIR__ . '/common/lib.form.php',
|
||||
'html' => __DIR__ . '/common/lib.html.php',
|
||||
'http' => __DIR__ . '/common/lib.http.php',
|
||||
'l10n' => __DIR__ . '/common/lib.l10n.php',
|
||||
'text' => __DIR__ . '/common/lib.text.php',
|
||||
|
||||
// Database Abstraction Layer
|
||||
'dbLayer' => __DIR__ . '/dblayer/dblayer.php',
|
||||
'dbStruct' => __DIR__ . '/dbschema/class.dbstruct.php',
|
||||
'dbSchema' => __DIR__ . '/dbschema/class.dbschema.php',
|
||||
|
||||
// Files Manager
|
||||
'filemanager' => __DIR__ . '/filemanager/class.filemanager.php',
|
||||
'fileItem' => __DIR__ . '/filemanager/class.filemanager.php',
|
||||
|
||||
// Feed Reader
|
||||
'feedParser' => __DIR__ . '/net.http.feed/class.feed.parser.php',
|
||||
'feedReader' => __DIR__ . '/net.http.feed/class.feed.reader.php',
|
||||
|
||||
// HTML Filter
|
||||
'htmlFilter' => __DIR__ . '/html.filter/class.html.filter.php',
|
||||
|
||||
// HTML Validator
|
||||
'htmlValidator' => __DIR__ . '/html.validator/class.html.validator.php',
|
||||
|
||||
// Image Manipulation Tools
|
||||
'imageMeta' => __DIR__ . '/image/class.image.meta.php',
|
||||
'imageTools' => __DIR__ . '/image/class.image.tools.php',
|
||||
|
||||
// Send Mail Utilities
|
||||
'mail' => __DIR__ . '/mail/class.mail.php',
|
||||
|
||||
// Send Mail Through Sockets
|
||||
'socketMail' => __DIR__ . '/mail/class.socket.mail.php',
|
||||
|
||||
// HTML Pager
|
||||
'pager' => __DIR__ . '/pager/class.pager.php',
|
||||
|
||||
// REST Server
|
||||
'restServer' => __DIR__ . '/rest/class.rest.php',
|
||||
'xmlTag' => __DIR__ . '/rest/class.rest.php',
|
||||
|
||||
// Database PHP Session
|
||||
'sessionDB' => __DIR__ . '/session.db/class.session.db.php',
|
||||
|
||||
// Simple Template Systeme
|
||||
'template' => __DIR__ . '/template/class.template.php',
|
||||
'tplNode' => __DIR__ . '/template/class.tplnode.php',
|
||||
'tplNodeBlock' => __DIR__ . '/template/class.tplnodeblock.php',
|
||||
'tplNodeText' => __DIR__ . '/template/class.tplnodetext.php',
|
||||
'tplNodeValue' => __DIR__ . '/template/class.tplnodevalue.php',
|
||||
'tplNodeBlockDefinition' => __DIR__ . '/template/class.tplnodeblockdef.php',
|
||||
'tplNodeValueParent' => __DIR__ . '/template/class.tplnodevalueparent.php',
|
||||
|
||||
// URL Handler
|
||||
'urlHandler' => __DIR__ . '/url.handler/class.url.handler.php',
|
||||
|
||||
// Wiki to XHTML Converter
|
||||
'wiki2xhtml' => __DIR__ . '/text.wiki2xhtml/class.wiki2xhtml.php',
|
||||
|
||||
// Common Socket Class
|
||||
'netSocket' => __DIR__ . '/net/class.net.socket.php',
|
||||
|
||||
// HTTP Client
|
||||
'netHttp' => __DIR__ . '/net.http/class.net.http.php',
|
||||
'HttpClient' => __DIR__ . '/net.http/class.net.http.php',
|
||||
|
||||
// XML-RPC Client and Server
|
||||
'xmlrpcValue' => __DIR__ . '/net.xmlrpc/class.net.xmlrpc.php',
|
||||
'xmlrpcMessage' => __DIR__ . '/net.xmlrpc/class.net.xmlrpc.php',
|
||||
'xmlrpcRequest' => __DIR__ . '/net.xmlrpc/class.net.xmlrpc.php',
|
||||
'xmlrpcDate' => __DIR__ . '/net.xmlrpc/class.net.xmlrpc.php',
|
||||
'xmlrpcBase64' => __DIR__ . '/net.xmlrpc/class.net.xmlrpc.php',
|
||||
'xmlrpcClient' => __DIR__ . '/net.xmlrpc/class.net.xmlrpc.php',
|
||||
'xmlrpcClientMulticall' => __DIR__ . '/net.xmlrpc/class.net.xmlrpc.php',
|
||||
'xmlrpcBasicServer' => __DIR__ . '/net.xmlrpc/class.net.xmlrpc.php',
|
||||
'xmlrpcIntrospectionServer' => __DIR__ . '/net.xmlrpc/class.net.xmlrpc.php',
|
||||
|
||||
// Zip tools
|
||||
'fileUnzip' => __DIR__ . '/zip/class.unzip.php',
|
||||
'fileZip' => __DIR__ . '/zip/class.zip.php',
|
||||
|
||||
// Diff tools
|
||||
'diff' => __DIR__ . '/diff/lib.diff.php',
|
||||
'tidyDiff' => __DIR__ . '/diff/lib.tidy.diff.php',
|
||||
|
||||
// HTML Form helpers
|
||||
'formComponent' => __DIR__ . '/html.form/class.form.component.php',
|
||||
'formForm' => __DIR__ . '/html.form/class.form.form.php',
|
||||
'formTextarea' => __DIR__ . '/html.form/class.form.textarea.php',
|
||||
'formInput' => __DIR__ . '/html.form/class.form.input.php',
|
||||
'formButton' => __DIR__ . '/html.form/class.form.button.php',
|
||||
'formCheckbox' => __DIR__ . '/html.form/class.form.checkbox.php',
|
||||
'formColor' => __DIR__ . '/html.form/class.form.color.php',
|
||||
'formDate' => __DIR__ . '/html.form/class.form.date.php',
|
||||
'formDatetime' => __DIR__ . '/html.form/class.form.datetime.php',
|
||||
'formEmail' => __DIR__ . '/html.form/class.form.email.php',
|
||||
'formFile' => __DIR__ . '/html.form/class.form.file.php',
|
||||
'formHidden' => __DIR__ . '/html.form/class.form.hidden.php',
|
||||
'formNumber' => __DIR__ . '/html.form/class.form.number.php',
|
||||
'formPassword' => __DIR__ . '/html.form/class.form.password.php',
|
||||
'formRadio' => __DIR__ . '/html.form/class.form.radio.php',
|
||||
'formSubmit' => __DIR__ . '/html.form/class.form.submit.php',
|
||||
'formTime' => __DIR__ . '/html.form/class.form.time.php',
|
||||
'formUrl' => __DIR__ . '/html.form/class.form.url.php',
|
||||
'formLabel' => __DIR__ . '/html.form/class.form.label.php',
|
||||
'formFieldset' => __DIR__ . '/html.form/class.form.fieldset.php',
|
||||
'formLegend' => __DIR__ . '/html.form/class.form.legend.php',
|
||||
'formSelect' => __DIR__ . '/html.form/class.form.select.php',
|
||||
'formOptgroup' => __DIR__ . '/html.form/class.form.optgroup.php',
|
||||
'formOption' => __DIR__ . '/html.form/class.form.option.php',
|
||||
]);
|
||||
|
||||
// We may need l10n __() function
|
||||
l10n::bootstrap();
|
||||
|
||||
// We set default timezone to avoid warning
|
||||
dt::setTZ('UTC');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Clearbricks singleton instance
|
||||
*
|
||||
* @return Clearbricks
|
||||
*/
|
||||
public static function lib(): Clearbricks
|
||||
{
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Autoload: register class(es)
|
||||
*
|
||||
* @param array $stack Array of class => file (strings)
|
||||
*/
|
||||
public function autoload(array $stack)
|
||||
{
|
||||
$this->autoloader->add($stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return source file associated with a registered class
|
||||
*
|
||||
* @param string $class The class
|
||||
*
|
||||
* @return mixed Source file or false
|
||||
*/
|
||||
public function autoloadSource(string $class)
|
||||
{
|
||||
return $this->autoloader->source($class);
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton
|
||||
new Clearbricks();
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
/**
|
||||
* @class crypt
|
||||
* @brief Functions to handle passwords or sensitive data
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Common
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class crypt
|
||||
{
|
||||
/**
|
||||
* SHA1 or MD5 + HMAC
|
||||
*
|
||||
* Returns an HMAC encoded value of <var>$data</var>, using the said <var>$key</var>
|
||||
* and <var>$hashfunc</var> as hash method (sha1 or md5 are accepted if hash_hmac function not exists.)
|
||||
*
|
||||
* @param string $key Hash key
|
||||
* @param string $data Data
|
||||
* @param string $hashfunc Hash function (md5 or sha1)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function hmac(string $key, string $data, string $hashfunc = 'sha1'): string
|
||||
{
|
||||
if (function_exists('hash_hmac')) {
|
||||
if (!in_array($hashfunc, hash_algos())) {
|
||||
$hashfunc = 'sha1';
|
||||
}
|
||||
|
||||
return hash_hmac($hashfunc, $data, $key);
|
||||
}
|
||||
|
||||
return self::hmac_legacy($key, $data, $hashfunc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy hmac method
|
||||
*
|
||||
* @param string $key The key
|
||||
* @param string $data The data
|
||||
* @param string $hashfunc The hashfunc
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function hmac_legacy(string $key, string $data, string $hashfunc = 'sha1'): string
|
||||
{
|
||||
// Legacy way
|
||||
if ($hashfunc != 'sha1') {
|
||||
$hashfunc = 'md5';
|
||||
}
|
||||
$blocksize = 64;
|
||||
if (strlen($key) > $blocksize) {
|
||||
$key = pack('H*', $hashfunc($key));
|
||||
}
|
||||
$key = str_pad($key, $blocksize, chr(0x00));
|
||||
$ipad = str_repeat(chr(0x36), $blocksize);
|
||||
$opad = str_repeat(chr(0x5c), $blocksize);
|
||||
$hmac = pack('H*', $hashfunc(($key ^ $opad) . pack('H*', $hashfunc(($key ^ $ipad) . $data))));
|
||||
|
||||
return bin2hex($hmac);
|
||||
}
|
||||
|
||||
/**
|
||||
* Password generator
|
||||
*
|
||||
* Returns an n characters random password.
|
||||
*
|
||||
* @param integer $length required length
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function createPassword(int $length = 8): string
|
||||
{
|
||||
// First shuffle charset random time (from 1 to 10)
|
||||
$charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890$!@';
|
||||
for ($x = 1; $x <= random_int(1, 10); $x++) {
|
||||
$charset = str_shuffle($charset);
|
||||
}
|
||||
|
||||
// Then generate a random password from the resulting charset
|
||||
$password = '';
|
||||
for ($s = 1; $s <= $length; $s++) {
|
||||
$password .= substr($charset, random_int(0, strlen($charset) - 1), 1);
|
||||
}
|
||||
|
||||
return $password;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,517 @@
|
|||
<?php
|
||||
/**
|
||||
* @class dt
|
||||
* @brief Date/time utilities
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Common
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class dt
|
||||
{
|
||||
private static $timezones = null;
|
||||
|
||||
/**
|
||||
* strftime() replacement when PHP version ≥ PHP 8.1
|
||||
*
|
||||
* Adapted from: <https://github.com/alphp/strftime>
|
||||
*
|
||||
* Locale-formatted strftime using IntlDateFormatter (PHP 8.1 compatible)
|
||||
* This provides a cross-platform alternative to strftime() for when it will be removed from PHP.
|
||||
* Note that output can be slightly different between libc sprintf and this function as it is using ICU.
|
||||
*
|
||||
* Usage:
|
||||
* use function \PHP81_BC\strftime;
|
||||
* echo strftime('%A %e %B %Y %X', new \DateTime('2021-09-28 00:00:00'), 'fr_FR');
|
||||
*
|
||||
* Original use:
|
||||
* \setlocale(LC_TIME, 'fr_FR.UTF-8');
|
||||
* echo \strftime('%A %e %B %Y %X', strtotime('2021-09-28 00:00:00'));
|
||||
*
|
||||
* @param string $format Date format
|
||||
* @param integer|string|DateTime $timestamp Timestamp
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @author BohwaZ <https://bohwaz.net/>
|
||||
*/
|
||||
public static function strftime(string $format, $timestamp = null, ?string $locale = null): string
|
||||
{
|
||||
if (version_compare(PHP_VERSION, '8.1', '<')) {
|
||||
return @strftime($format, $timestamp);
|
||||
}
|
||||
|
||||
if (!($timestamp instanceof DateTimeInterface)) {
|
||||
$timestamp = is_int($timestamp) ? '@' . $timestamp : (string) $timestamp;
|
||||
|
||||
try {
|
||||
$timestamp = new DateTime($timestamp);
|
||||
} catch (Exception $e) {
|
||||
throw new InvalidArgumentException('$timestamp argument is neither a valid UNIX timestamp, a valid date-time string or a DateTime object.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
$timestamp->setTimezone(new DateTimeZone(date_default_timezone_get()));
|
||||
|
||||
if (empty($locale)) {
|
||||
// get current locale
|
||||
$locale = setlocale(LC_TIME, '0');
|
||||
}
|
||||
// remove trailing part not supported by ext-intl locale
|
||||
$locale = preg_replace('/[^\w-].*$/', '', $locale);
|
||||
|
||||
$intl_formats = [
|
||||
'%a' => 'EEE', // An abbreviated textual representation of the day Sun through Sat
|
||||
'%A' => 'EEEE', // A full textual representation of the day Sunday through Saturday
|
||||
'%b' => 'MMM', // Abbreviated month name, based on the locale Jan through Dec
|
||||
'%B' => 'MMMM', // Full month name, based on the locale January through December
|
||||
'%h' => 'MMM', // Abbreviated month name, based on the locale (an alias of %b) Jan through Dec
|
||||
];
|
||||
|
||||
$intl_formatter = function (DateTimeInterface $timestamp, string $format) use ($intl_formats, $locale) {
|
||||
$tz = $timestamp->getTimezone();
|
||||
$date_type = IntlDateFormatter::FULL;
|
||||
$time_type = IntlDateFormatter::FULL;
|
||||
$pattern = '';
|
||||
|
||||
switch ($format) {
|
||||
// %c = Preferred date and time stamp based on locale
|
||||
// Example: Tue Feb 5 00:45:10 2009 for February 5, 2009 at 12:45:10 AM
|
||||
case '%c':
|
||||
$date_type = IntlDateFormatter::LONG;
|
||||
$time_type = IntlDateFormatter::SHORT;
|
||||
|
||||
break;
|
||||
|
||||
// %x = Preferred date representation based on locale, without the time
|
||||
// Example: 02/05/09 for February 5, 2009
|
||||
case '%x':
|
||||
$date_type = IntlDateFormatter::SHORT;
|
||||
$time_type = IntlDateFormatter::NONE;
|
||||
|
||||
break;
|
||||
|
||||
// Localized time format
|
||||
case '%X':
|
||||
$date_type = IntlDateFormatter::NONE;
|
||||
$time_type = IntlDateFormatter::MEDIUM;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$pattern = $intl_formats[$format];
|
||||
}
|
||||
|
||||
// In October 1582, the Gregorian calendar replaced the Julian in much of Europe, and
|
||||
// the 4th October was followed by the 15th October.
|
||||
// ICU (including IntlDateFormattter) interprets and formats dates based on this cutover.
|
||||
// Posix (including strftime) and timelib (including DateTimeImmutable) instead use
|
||||
// a "proleptic Gregorian calendar" - they pretend the Gregorian calendar has existed forever.
|
||||
// This leads to the same instants in time, as expressed in Unix time, having different representations
|
||||
// in formatted strings.
|
||||
// To adjust for this, a custom calendar can be supplied with a cutover date arbitrarily far in the past.
|
||||
$calendar = IntlGregorianCalendar::createInstance();
|
||||
$calendar->setGregorianChange(PHP_INT_MIN);
|
||||
|
||||
return (new IntlDateFormatter($locale, $date_type, $time_type, $tz, $calendar, $pattern))->format($timestamp);
|
||||
};
|
||||
|
||||
// Same order as https://www.php.net/manual/en/function.strftime.php
|
||||
$translation_table = [
|
||||
// Day
|
||||
'%a' => $intl_formatter,
|
||||
'%A' => $intl_formatter,
|
||||
'%d' => 'd',
|
||||
'%e' => fn ($timestamp) => sprintf('% 2u', $timestamp->format('j')),
|
||||
'%j' => function ($timestamp) {
|
||||
// Day number in year, 001 to 366
|
||||
return sprintf('%03d', $timestamp->format('z') + 1);
|
||||
},
|
||||
'%u' => 'N',
|
||||
'%w' => 'w',
|
||||
|
||||
// Week
|
||||
'%U' => function ($timestamp) {
|
||||
// Number of weeks between date and first Sunday of year
|
||||
$day = new DateTime(sprintf('%d-01 Sunday', $timestamp->format('Y')));
|
||||
|
||||
return sprintf('%02u', 1 + ($timestamp->format('z') - $day->format('z')) / 7);
|
||||
},
|
||||
'%V' => 'W',
|
||||
'%W' => function ($timestamp) {
|
||||
// Number of weeks between date and first Monday of year
|
||||
$day = new DateTime(sprintf('%d-01 Monday', $timestamp->format('Y')));
|
||||
|
||||
return sprintf('%02u', 1 + ($timestamp->format('z') - $day->format('z')) / 7);
|
||||
},
|
||||
|
||||
// Month
|
||||
'%b' => $intl_formatter,
|
||||
'%B' => $intl_formatter,
|
||||
'%h' => $intl_formatter,
|
||||
'%m' => 'm',
|
||||
|
||||
// Year
|
||||
'%C' => function ($timestamp) {
|
||||
// Century (-1): 19 for 20th century
|
||||
return floor($timestamp->format('Y') / 100);
|
||||
},
|
||||
'%g' => fn ($timestamp) => substr($timestamp->format('o'), -2),
|
||||
'%G' => 'o',
|
||||
'%y' => 'y',
|
||||
'%Y' => 'Y',
|
||||
|
||||
// Time
|
||||
'%H' => 'H',
|
||||
'%k' => fn ($timestamp) => sprintf('% 2u', $timestamp->format('G')),
|
||||
'%I' => 'h',
|
||||
'%l' => fn ($timestamp) => sprintf('% 2u', $timestamp->format('g')),
|
||||
'%M' => 'i',
|
||||
'%p' => 'A', // AM PM (this is reversed on purpose!)
|
||||
'%P' => 'a', // am pm
|
||||
'%r' => 'h:i:s A', // %I:%M:%S %p
|
||||
'%R' => 'H:i', // %H:%M
|
||||
'%S' => 's',
|
||||
'%T' => 'H:i:s', // %H:%M:%S
|
||||
'%X' => $intl_formatter, // Preferred time representation based on locale, without the date
|
||||
|
||||
// Timezone
|
||||
'%z' => 'O',
|
||||
'%Z' => 'T',
|
||||
|
||||
// Time and Date Stamps
|
||||
'%c' => $intl_formatter,
|
||||
'%D' => 'm/d/Y',
|
||||
'%F' => 'Y-m-d',
|
||||
'%s' => 'U',
|
||||
'%x' => $intl_formatter,
|
||||
];
|
||||
|
||||
$out = preg_replace_callback('/(?<!%)%([_#-]?)([a-zA-Z])/', function ($match) use ($translation_table, $timestamp) {
|
||||
$prefix = $match[1];
|
||||
$char = $match[2];
|
||||
$pattern = '%' . $char;
|
||||
if ($pattern == '%n') {
|
||||
return "\n";
|
||||
} elseif ($pattern == '%t') {
|
||||
return "\t";
|
||||
}
|
||||
|
||||
if (!isset($translation_table[$pattern])) {
|
||||
throw new InvalidArgumentException(sprintf('Format "%s" is unknown in time format', $pattern));
|
||||
}
|
||||
|
||||
$replace = $translation_table[$pattern];
|
||||
|
||||
if (is_string($replace)) {
|
||||
$result = $timestamp->format($replace);
|
||||
} else {
|
||||
$result = $replace($timestamp);
|
||||
}
|
||||
|
||||
switch ($prefix) {
|
||||
case '_':
|
||||
// replace leading zeros with spaces but keep last char if also zero
|
||||
return preg_replace('/\G0(?=.)/', ' ', $result);
|
||||
case '#':
|
||||
case '-':
|
||||
// remove leading zeros but keep last char if also zero
|
||||
return preg_replace('/^0+(?=.)/', '', $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}, $format);
|
||||
|
||||
$out = str_replace('%%', '%', $out);
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp formating
|
||||
*
|
||||
* Returns a date formated like PHP <a href="http://www.php.net/manual/en/function.strftime.php">strftime</a>
|
||||
* function.
|
||||
* Special cases %a, %A, %b and %B are handled by {@link l10n} library.
|
||||
*
|
||||
* @param string $pattern Format pattern
|
||||
* @param int|bool $timestamp Timestamp
|
||||
* @param string $timezone Timezone
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function str(string $pattern, $timestamp = null, string $timezone = null): string
|
||||
{
|
||||
if ($timestamp === null || $timestamp === false) {
|
||||
$timestamp = time();
|
||||
}
|
||||
|
||||
$hash = '799b4e471dc78154865706469d23d512';
|
||||
$pattern = preg_replace('/(?<!%)%(a|A)/', '{{' . $hash . '__$1%w__}}', $pattern);
|
||||
$pattern = preg_replace('/(?<!%)%(b|B)/', '{{' . $hash . '__$1%m__}}', $pattern);
|
||||
|
||||
if ($timezone) {
|
||||
$current_timezone = self::getTZ();
|
||||
self::setTZ($timezone);
|
||||
}
|
||||
|
||||
$res = self::strftime($pattern, $timestamp);
|
||||
|
||||
if ($timezone) {
|
||||
self::setTZ($current_timezone);
|
||||
}
|
||||
|
||||
$res = preg_replace_callback(
|
||||
'/{{' . $hash . '__(a|A|b|B)([0-9]{1,2})__}}/',
|
||||
function ($args) {
|
||||
$b = [
|
||||
1 => '_Jan',
|
||||
2 => '_Feb',
|
||||
3 => '_Mar',
|
||||
4 => '_Apr',
|
||||
5 => '_May',
|
||||
6 => '_Jun',
|
||||
7 => '_Jul',
|
||||
8 => '_Aug',
|
||||
9 => '_Sep',
|
||||
10 => '_Oct',
|
||||
11 => '_Nov',
|
||||
12 => '_Dec', ];
|
||||
|
||||
$B = [
|
||||
1 => 'January',
|
||||
2 => 'February',
|
||||
3 => 'March',
|
||||
4 => 'April',
|
||||
5 => 'May',
|
||||
6 => 'June',
|
||||
7 => 'July',
|
||||
8 => 'August',
|
||||
9 => 'September',
|
||||
10 => 'October',
|
||||
11 => 'November',
|
||||
12 => 'December', ];
|
||||
|
||||
$a = [
|
||||
1 => '_Mon',
|
||||
2 => '_Tue',
|
||||
3 => '_Wed',
|
||||
4 => '_Thu',
|
||||
5 => '_Fri',
|
||||
6 => '_Sat',
|
||||
0 => '_Sun', ];
|
||||
|
||||
$A = [
|
||||
1 => 'Monday',
|
||||
2 => 'Tuesday',
|
||||
3 => 'Wednesday',
|
||||
4 => 'Thursday',
|
||||
5 => 'Friday',
|
||||
6 => 'Saturday',
|
||||
0 => 'Sunday', ];
|
||||
|
||||
return __(${$args[1]}[(int) $args[2]]);
|
||||
},
|
||||
$res
|
||||
);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Date to date
|
||||
*
|
||||
* Format a literal date to another literal date.
|
||||
*
|
||||
* @param string $pattern Format pattern
|
||||
* @param string $datetime Date
|
||||
* @param string $timezone Timezone
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function dt2str(string $pattern, string $datetime, ?string $timezone = null): string
|
||||
{
|
||||
return self::str($pattern, strtotime($datetime), $timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* ISO-8601 formatting
|
||||
*
|
||||
* Returns a timestamp converted to ISO-8601 format.
|
||||
*
|
||||
* @param integer $timestamp Timestamp
|
||||
* @param string $timezone Timezone
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function iso8601(int $timestamp, string $timezone = 'UTC'): string
|
||||
{
|
||||
$offset = self::getTimeOffset($timezone, $timestamp);
|
||||
$printed_offset = sprintf('%02u:%02u', abs($offset) / 3600, (abs($offset) % 3600) / 60);
|
||||
|
||||
return date('Y-m-d\\TH:i:s', $timestamp) . ($offset < 0 ? '-' : '+') . $printed_offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-822 formatting
|
||||
*
|
||||
* Returns a timestamp converted to RFC-822 format.
|
||||
*
|
||||
* @param integer $timestamp Timestamp
|
||||
* @param string $timezone Timezone
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function rfc822(int $timestamp, string $timezone = 'UTC'): string
|
||||
{
|
||||
# Get offset
|
||||
$offset = self::getTimeOffset($timezone, $timestamp);
|
||||
$printed_offset = sprintf('%02u%02u', abs($offset) / 3600, (abs($offset) % 3600) / 60);
|
||||
|
||||
// Avoid deprecated notice until PHP 9 should be supported or a correct strftime() replacement
|
||||
return @strftime('%a, %d %b %Y %H:%M:%S ' . ($offset < 0 ? '-' : '+') . $printed_offset, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Timezone set
|
||||
*
|
||||
* Set timezone during script execution.
|
||||
*
|
||||
* @param string $timezone Timezone
|
||||
*/
|
||||
public static function setTZ(string $timezone)
|
||||
{
|
||||
if (function_exists('date_default_timezone_set')) {
|
||||
date_default_timezone_set($timezone);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
putenv('TZ=' . $timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current timezone
|
||||
*
|
||||
* Returns current timezone.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTZ(): string
|
||||
{
|
||||
if (function_exists('date_default_timezone_get')) {
|
||||
return date_default_timezone_get();
|
||||
}
|
||||
|
||||
return date('T');
|
||||
}
|
||||
|
||||
/**
|
||||
* Time offset
|
||||
*
|
||||
* Get time offset for a timezone and an optionnal $ts timestamp.
|
||||
*
|
||||
* @param string $timezone Timezone
|
||||
* @param integer|boolean $timestamp Timestamp
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public static function getTimeOffset(string $timezone, $timestamp = false): int
|
||||
{
|
||||
if (!$timestamp) {
|
||||
$timestamp = time();
|
||||
}
|
||||
|
||||
$server_timezone = self::getTZ();
|
||||
$server_offset = date('Z', $timestamp);
|
||||
|
||||
self::setTZ($timezone);
|
||||
$current_offset = date('Z', $timestamp);
|
||||
|
||||
self::setTZ($server_timezone);
|
||||
|
||||
return $current_offset - $server_offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* UTC conversion
|
||||
*
|
||||
* Returns any timestamp from current timezone to UTC timestamp.
|
||||
*
|
||||
* @param integer $timestamp Timestamp
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public static function toUTC(int $timestamp): int
|
||||
{
|
||||
return $timestamp + self::getTimeOffset('UTC', $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add timezone
|
||||
*
|
||||
* Returns a timestamp with its timezone offset.
|
||||
*
|
||||
* @param string $timezone Timezone
|
||||
* @param integer|boolean $timestamp Timestamp
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public static function addTimeZone(string $timezone, $timestamp = false): int
|
||||
{
|
||||
if ($timestamp === false) {
|
||||
$timestamp = time();
|
||||
}
|
||||
|
||||
return $timestamp + self::getTimeOffset($timezone, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Timzones
|
||||
*
|
||||
* Returns an array of supported timezones, codes are keys and names are values.
|
||||
*
|
||||
* @param boolean $flip Names are keys and codes are values
|
||||
* @param boolean $groups Return timezones in arrays of continents
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getZones(bool $flip = false, bool $groups = false): array
|
||||
{
|
||||
if (is_null(self::$timezones)) {
|
||||
// Read timezones from file
|
||||
if (!is_readable($file = dirname(__FILE__) . '/tz.dat')) {
|
||||
return [];
|
||||
}
|
||||
$timezones = file($file);
|
||||
$res = [];
|
||||
foreach ($timezones as $timezone) {
|
||||
$timezone = trim($timezone);
|
||||
if ($timezone) {
|
||||
$res[$timezone] = str_replace('_', ' ', $timezone);
|
||||
}
|
||||
}
|
||||
// Store timezones for further accesses
|
||||
self::$timezones = $res;
|
||||
} else {
|
||||
// Timezones already read from file
|
||||
$res = self::$timezones;
|
||||
}
|
||||
|
||||
if ($flip) {
|
||||
$res = array_flip($res);
|
||||
if ($groups) {
|
||||
$tmp = [];
|
||||
foreach ($res as $code => $timezone) {
|
||||
$group = explode('/', $code);
|
||||
$tmp[$group[0]][$code] = $timezone;
|
||||
}
|
||||
$res = $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,659 @@
|
|||
<?php
|
||||
/**
|
||||
* @class files
|
||||
* @brief Files manipulation utilities
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Common
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class files
|
||||
{
|
||||
/**
|
||||
* Default directories mode
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public static $dir_mode = null;
|
||||
|
||||
/**
|
||||
* MIME types
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $mime_types = [
|
||||
'odt' => 'application/vnd.oasis.opendocument.text',
|
||||
'odp' => 'application/vnd.oasis.opendocument.presentation',
|
||||
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
|
||||
|
||||
'sxw' => 'application/vnd.sun.xml.writer',
|
||||
'sxc' => 'application/vnd.sun.xml.calc',
|
||||
'sxi' => 'application/vnd.sun.xml.impress',
|
||||
|
||||
'ppt' => 'application/mspowerpoint',
|
||||
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'doc' => 'application/msword',
|
||||
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'xls' => 'application/msexcel',
|
||||
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
|
||||
'pdf' => 'application/pdf',
|
||||
'ps' => 'application/postscript',
|
||||
'ai' => 'application/postscript',
|
||||
'eps' => 'application/postscript',
|
||||
'json' => 'application/json',
|
||||
'xml' => 'application/xml',
|
||||
|
||||
'bin' => 'application/octet-stream',
|
||||
'exe' => 'application/octet-stream',
|
||||
|
||||
'bz2' => 'application/x-bzip',
|
||||
'deb' => 'application/x-debian-package',
|
||||
'gz' => 'application/x-gzip',
|
||||
'jar' => 'application/x-java-archive',
|
||||
'rar' => 'application/rar',
|
||||
'rpm' => 'application/x-redhat-package-manager',
|
||||
'tar' => 'application/x-tar',
|
||||
'tgz' => 'application/x-gtar',
|
||||
'zip' => 'application/zip',
|
||||
|
||||
'aiff' => 'audio/x-aiff',
|
||||
'ua' => 'audio/basic',
|
||||
'mp3' => 'audio/mpeg3',
|
||||
'mid' => 'audio/x-midi',
|
||||
'midi' => 'audio/x-midi',
|
||||
'ogg' => 'application/ogg',
|
||||
'ra' => 'audio/x-pn-realaudio',
|
||||
'ram' => 'audio/x-pn-realaudio',
|
||||
'wav' => 'audio/x-wav',
|
||||
'wma' => 'audio/x-ms-wma',
|
||||
|
||||
'swf' => 'application/x-shockwave-flash',
|
||||
'swfl' => 'application/x-shockwave-flash',
|
||||
'js' => 'application/javascript',
|
||||
|
||||
'bmp' => 'image/bmp',
|
||||
'gif' => 'image/gif',
|
||||
'ico' => 'image/vnd.microsoft.icon',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpe' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'svg' => 'image/svg+xml',
|
||||
'tiff' => 'image/tiff',
|
||||
'tif' => 'image/tiff',
|
||||
'webp' => 'image/webp',
|
||||
'xbm' => 'image/x-xbitmap',
|
||||
|
||||
'css' => 'text/css',
|
||||
'csv' => 'text/csv',
|
||||
'html' => 'text/html',
|
||||
'htm' => 'text/html',
|
||||
'txt' => 'text/plain',
|
||||
'rtf' => 'text/richtext',
|
||||
'rtx' => 'text/richtext',
|
||||
|
||||
'mpg' => 'video/mpeg',
|
||||
'mpeg' => 'video/mpeg',
|
||||
'mpe' => 'video/mpeg',
|
||||
'ogv' => 'video/ogg',
|
||||
'viv' => 'video/vnd.vivo',
|
||||
'vivo' => 'video/vnd.vivo',
|
||||
'qt' => 'video/quicktime',
|
||||
'mov' => 'video/quicktime',
|
||||
'mp4' => 'video/mp4',
|
||||
'm4v' => 'video/x-m4v',
|
||||
'flv' => 'video/x-flv',
|
||||
'avi' => 'video/x-msvideo',
|
||||
'wmv' => 'video/x-ms-wmv',
|
||||
];
|
||||
|
||||
/**
|
||||
* Directory scanning
|
||||
*
|
||||
* Returns a directory child files and directories.
|
||||
*
|
||||
* @param string $directory Path to scan
|
||||
* @param boolean $order Order results
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function scandir(string $directory, bool $order = true): array
|
||||
{
|
||||
$res = [];
|
||||
$handle = @opendir($directory);
|
||||
|
||||
if ($handle === false) {
|
||||
throw new Exception(__('Unable to open directory.'));
|
||||
}
|
||||
|
||||
while (($file = readdir($handle)) !== false) {
|
||||
$res[] = $file;
|
||||
}
|
||||
closedir($handle);
|
||||
|
||||
if ($order) {
|
||||
sort($res);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* File extension
|
||||
*
|
||||
* Returns a file extension.
|
||||
*
|
||||
* @param string $filename File name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getExtension(string $filename): string
|
||||
{
|
||||
return strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
}
|
||||
|
||||
/**
|
||||
* MIME type
|
||||
*
|
||||
* Returns a file MIME type, based on static var {@link $mime_types}
|
||||
*
|
||||
* @param string $filename File name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getMimeType(string $filename): string
|
||||
{
|
||||
$ext = self::getExtension($filename);
|
||||
$types = self::mimeTypes();
|
||||
|
||||
if (isset($types[$ext])) {
|
||||
return $types[$ext];
|
||||
}
|
||||
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* MIME types
|
||||
*
|
||||
* Returns all defined MIME types.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function mimeTypes(): array
|
||||
{
|
||||
return self::$mime_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* New MIME types
|
||||
*
|
||||
* Append new MIME types to defined MIME types.
|
||||
*
|
||||
* @param array $types New MIME types.
|
||||
*/
|
||||
public static function registerMimeTypes(array $types): void
|
||||
{
|
||||
self::$mime_types = array_merge(self::$mime_types, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a file or directory deletable.
|
||||
*
|
||||
* Returns true if $f is a file or directory and is deletable.
|
||||
*
|
||||
* @param string $filename File or directory
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isDeletable(string $filename): bool
|
||||
{
|
||||
if (is_file($filename)) {
|
||||
return is_writable(dirname($filename));
|
||||
} elseif (is_dir($filename)) {
|
||||
return (is_writable(dirname($filename)) && count(files::scandir($filename)) <= 2);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive removal
|
||||
*
|
||||
* Remove recursively a directory.
|
||||
*
|
||||
* @param string $directory Directory patch
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function deltree(string $directory): bool
|
||||
{
|
||||
$current_dir = opendir($directory);
|
||||
while ($filename = readdir($current_dir)) {
|
||||
if (is_dir($directory . '/' . $filename) && ($filename != '.' && $filename != '..')) {
|
||||
if (!files::deltree($directory . '/' . $filename)) {
|
||||
return false;
|
||||
}
|
||||
} elseif ($filename != '.' && $filename != '..') {
|
||||
if (!@unlink($directory . '/' . $filename)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($current_dir);
|
||||
|
||||
return @rmdir($directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Touch file
|
||||
*
|
||||
* Set file modification time to now.
|
||||
*
|
||||
* @param string $filename File to change
|
||||
*/
|
||||
public static function touch(string $filename): void
|
||||
{
|
||||
if (is_writable($filename)) {
|
||||
@touch($filename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory creation.
|
||||
*
|
||||
* Creates directory $f. If $r is true, attempts to create needed parents
|
||||
* directories.
|
||||
*
|
||||
* @param string $name Directory to create
|
||||
* @param boolean $recursive Create parent directories
|
||||
*/
|
||||
public static function makeDir(string $name, bool $recursive = false): void
|
||||
{
|
||||
if (empty($name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DIRECTORY_SEPARATOR == '\\') {
|
||||
$name = str_replace('/', '\\', $name);
|
||||
}
|
||||
|
||||
if (is_dir($name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($recursive) {
|
||||
$path = path::real($name, false);
|
||||
$directories = [];
|
||||
|
||||
while (!is_dir($path)) {
|
||||
array_unshift($directories, basename($path));
|
||||
$path = dirname($path);
|
||||
}
|
||||
|
||||
foreach ($directories as $directory) {
|
||||
$path .= DIRECTORY_SEPARATOR . $directory;
|
||||
if ($directory != '' && !is_dir($path)) {
|
||||
self::makeDir($path);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (@mkdir($name) === false) {
|
||||
throw new Exception(__('Unable to create directory.'));
|
||||
}
|
||||
self::inheritChmod($name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode inheritage
|
||||
*
|
||||
* Sets file or directory mode according to its parent.
|
||||
*
|
||||
* @param string $file File to change
|
||||
*/
|
||||
public static function inheritChmod(string $file): bool
|
||||
{
|
||||
if (self::$dir_mode === null) {
|
||||
return @chmod($file, @fileperms(dirname($file)));
|
||||
}
|
||||
|
||||
return @chmod($file, self::$dir_mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes file content.
|
||||
*
|
||||
* Writes $f_content into $f file.
|
||||
*
|
||||
* @param string $file File to edit
|
||||
* @param string $content Content to write
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function putContent(string $file, string $content): bool
|
||||
{
|
||||
if (file_exists($file) && !is_writable($file)) {
|
||||
throw new Exception(__('File is not writable.'));
|
||||
}
|
||||
|
||||
$handle = @fopen($file, 'w');
|
||||
|
||||
if ($handle === false) {
|
||||
throw new Exception(__('Unable to open file.'));
|
||||
}
|
||||
|
||||
fwrite($handle, $content, strlen($content));
|
||||
fclose($handle);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Human readable file size.
|
||||
*
|
||||
* @param integer $size Bytes
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function size(int $size): string
|
||||
{
|
||||
$kb = 1024;
|
||||
$mb = 1024 * $kb;
|
||||
$gb = 1024 * $mb;
|
||||
$tb = 1024 * $gb;
|
||||
|
||||
if ($size < $kb) {
|
||||
return $size . ' B';
|
||||
} elseif ($size < $mb) {
|
||||
return round($size / $kb, 2) . ' KB';
|
||||
} elseif ($size < $gb) {
|
||||
return round($size / $mb, 2) . ' MB';
|
||||
} elseif ($size < $tb) {
|
||||
return round($size / $gb, 2) . ' GB';
|
||||
}
|
||||
|
||||
return round($size / $tb, 2) . ' TB';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a human readable file size to bytes.
|
||||
*
|
||||
* @param string $size Size
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public static function str2bytes(string $size): float
|
||||
{
|
||||
$size = trim($size);
|
||||
$last = strtolower(substr($size, -1, 1));
|
||||
$size = (float) substr($size, 0, -1);
|
||||
switch ($last) {
|
||||
case 'g':
|
||||
$size *= 1024;
|
||||
case 'm':
|
||||
$size *= 1024;
|
||||
case 'k':
|
||||
$size *= 1024;
|
||||
}
|
||||
|
||||
return $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload status
|
||||
*
|
||||
* Returns true if upload status is ok, throws an exception instead.
|
||||
*
|
||||
* @param array $file File array as found in $_FILES
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function uploadStatus(array $file): bool
|
||||
{
|
||||
if (!isset($file['error'])) {
|
||||
throw new Exception(__('Not an uploaded file.'));
|
||||
}
|
||||
|
||||
switch ($file['error']) {
|
||||
case UPLOAD_ERR_OK:
|
||||
return true;
|
||||
case UPLOAD_ERR_INI_SIZE:
|
||||
case UPLOAD_ERR_FORM_SIZE:
|
||||
throw new Exception(__('The uploaded file exceeds the maximum file size allowed.'));
|
||||
case UPLOAD_ERR_PARTIAL:
|
||||
throw new Exception(__('The uploaded file was only partially uploaded.'));
|
||||
case UPLOAD_ERR_NO_FILE:
|
||||
throw new Exception(__('No file was uploaded.'));
|
||||
case UPLOAD_ERR_NO_TMP_DIR:
|
||||
throw new Exception(__('Missing a temporary folder.'));
|
||||
case UPLOAD_ERR_CANT_WRITE:
|
||||
throw new Exception(__('Failed to write file to disk.'));
|
||||
case UPLOAD_ERR_EXTENSION:
|
||||
throw new Exception(__('A PHP extension stopped the file upload.'));
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
# Packages generation methods
|
||||
#
|
||||
|
||||
/**
|
||||
* Recursive directory scanning
|
||||
*
|
||||
* Returns an array of a given directory's content. The array contains two arrays: dirs and files.
|
||||
* Directory's content is fetched recursively.
|
||||
*
|
||||
* @param string $directory Directory name
|
||||
* @param array $list Contents array (leave it empty)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getDirList(string $directory, array &$list = null): array
|
||||
{
|
||||
if (!$list) {
|
||||
$list = [
|
||||
'dirs' => [],
|
||||
'files' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$exclude_list = ['.', '..', '.svn', '.git', '.hg'];
|
||||
|
||||
$directory = preg_replace('|/$|', '', $directory);
|
||||
if (!is_dir($directory)) {
|
||||
throw new Exception(sprintf(__('%s is not a directory.'), $directory));
|
||||
}
|
||||
|
||||
$list['dirs'][] = $directory;
|
||||
|
||||
$handle = @dir($directory);
|
||||
if ($handle === false) {
|
||||
throw new Exception(__('Unable to open directory.'));
|
||||
}
|
||||
|
||||
while ($file = $handle->read()) {
|
||||
if (!in_array($file, $exclude_list)) {
|
||||
if (is_dir($directory . '/' . $file)) {
|
||||
files::getDirList($directory . '/' . $file, $list);
|
||||
} else {
|
||||
$list['files'][] = $directory . '/' . $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
$handle->close();
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename cleanup
|
||||
*
|
||||
* Removes unwanted characters in a filename.
|
||||
*
|
||||
* @param string $filename Filename
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function tidyFileName(string $filename): string
|
||||
{
|
||||
$filename = preg_replace('/^[.]/u', '', text::deaccent($filename));
|
||||
|
||||
return preg_replace('/[^A-Za-z0-9._-]/u', '_', $filename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class path
|
||||
* @brief Path manipulation utilities
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Common
|
||||
*/
|
||||
class path
|
||||
{
|
||||
/**
|
||||
* Returns the real path of a file.
|
||||
*
|
||||
* If parameter $strict is true, file should exist. Returns false if
|
||||
* file does not exist.
|
||||
*
|
||||
* @param string $filename Filename
|
||||
* @param boolean $strict File should exists
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public static function real(string $filename, bool $strict = true)
|
||||
{
|
||||
$os = (DIRECTORY_SEPARATOR == '\\') ? 'win' : 'nix';
|
||||
|
||||
# Absolute path?
|
||||
if ($os == 'win') {
|
||||
$absolute = preg_match('/^\w+:/', $filename);
|
||||
} else {
|
||||
$absolute = substr($filename, 0, 1) == '/';
|
||||
}
|
||||
|
||||
# Standard path form
|
||||
if ($os == 'win') {
|
||||
$filename = str_replace('\\', '/', $filename);
|
||||
}
|
||||
|
||||
# Adding root if !$_abs
|
||||
if (!$absolute) {
|
||||
$filename = dirname($_SERVER['SCRIPT_FILENAME']) . '/' . $filename;
|
||||
}
|
||||
|
||||
# Clean up
|
||||
$filename = preg_replace('|/+|', '/', $filename);
|
||||
|
||||
if (strlen($filename) > 1) {
|
||||
$filename = preg_replace('|/$|', '', $filename);
|
||||
}
|
||||
|
||||
$prefix = '';
|
||||
if ($os == 'win') {
|
||||
[$prefix, $filename] = explode(':', $filename);
|
||||
$prefix .= ':/';
|
||||
} else {
|
||||
$prefix = '/';
|
||||
}
|
||||
$filename = substr($filename, 1);
|
||||
|
||||
# Go through
|
||||
$parts = explode('/', $filename);
|
||||
$res = [];
|
||||
|
||||
for ($i = 0; $i < count($parts); $i++) {
|
||||
if ($parts[$i] == '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($parts[$i] == '..') {
|
||||
if (count($res) > 0) {
|
||||
array_pop($res);
|
||||
}
|
||||
} else {
|
||||
array_push($res, $parts[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
$filename = $prefix . implode('/', $res);
|
||||
|
||||
if ($strict && !@file_exists($filename)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clean file path
|
||||
*
|
||||
* @param string $filename File path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function clean(?string $filename): string
|
||||
{
|
||||
// Remove double point (upper directory)
|
||||
$filename = preg_replace(['|^\.\.|', '|/\.\.|', '|\.\.$|'], '', (string) $filename);
|
||||
|
||||
// Replace double slashes by one
|
||||
$filename = preg_replace('|/{2,}|', '/', (string) $filename);
|
||||
|
||||
// Remove trailing slash
|
||||
$filename = preg_replace('|/$|', '', (string) $filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Path information
|
||||
*
|
||||
* Returns an array of information:
|
||||
* - dirname
|
||||
* - basename
|
||||
* - extension
|
||||
* - base (basename without extension)
|
||||
*
|
||||
* @param string $filename File path
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function info(string $filename): array
|
||||
{
|
||||
$pathinfo = pathinfo($filename);
|
||||
$res = [];
|
||||
|
||||
$res['dirname'] = (string) $pathinfo['dirname'];
|
||||
$res['basename'] = (string) $pathinfo['basename'];
|
||||
$res['extension'] = $pathinfo['extension'] ?? '';
|
||||
$res['base'] = preg_replace('/\.' . preg_quote($res['extension'], '/') . '$/', '', $res['basename']);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full path with root
|
||||
*
|
||||
* Returns a path with root concatenation unless path begins with a slash
|
||||
*
|
||||
* @param string $path File path
|
||||
* @param string $root Root path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function fullFromRoot(string $path, string $root): string
|
||||
{
|
||||
if (substr($path, 0, 1) == '/') {
|
||||
return $path;
|
||||
}
|
||||
|
||||
return $root . '/' . $path;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,960 @@
|
|||
<?php
|
||||
/**
|
||||
* @class form
|
||||
* @brief HTML Forms creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Common
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class form
|
||||
{
|
||||
/**
|
||||
* return id and name from given argument
|
||||
*
|
||||
* @param string|array $nid input argument
|
||||
* @param string $name returned name
|
||||
* @param string $id returned id
|
||||
*
|
||||
* @static
|
||||
* @access private
|
||||
*/
|
||||
private static function getNameAndId($nid, &$name, &$id): void
|
||||
{
|
||||
if (is_array($nid)) {
|
||||
$name = $nid[0];
|
||||
$id = !empty($nid[1]) ? $nid[1] : null;
|
||||
} else {
|
||||
$name = $id = $nid;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* return an associative array of optional parameters of a class method
|
||||
*
|
||||
* @param string $class class name
|
||||
* @param string $method method name
|
||||
* @return array
|
||||
*
|
||||
* @static
|
||||
* @access private
|
||||
*/
|
||||
private static function getDefaults(string $class, string $method): array
|
||||
{
|
||||
$options = [];
|
||||
$reflect = new ReflectionMethod($class, $method);
|
||||
foreach ($reflect->getParameters() as $param) {
|
||||
if ($param->isOptional()) {
|
||||
$options[$param->getName()] = $param->getDefaultValue();
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select Box
|
||||
*
|
||||
* Returns HTML code for a select box.
|
||||
* **$nid** could be a string or an array of name and ID.
|
||||
* **$data** is an array with option titles keys and values in values
|
||||
* or an array of object of type {@link formSelectOption}. If **$data** is an array of
|
||||
* arrays, optgroups will be created.
|
||||
*
|
||||
* **$default** could be a string or an associative array of any of optional parameters:
|
||||
*
|
||||
* ```php
|
||||
* form::combo(['name', 'id'], $data, ['class' => 'maximal', 'extra_html' => 'data-language="php"']);
|
||||
* ```
|
||||
*
|
||||
* @uses formSelectOption
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param mixed $data Select box data
|
||||
* @param mixed $default Default value in select box | associative array of optional parameters
|
||||
* @param string $class Element class name
|
||||
* @param string $tabindex Element tabindex
|
||||
* @param boolean $disabled True if disabled
|
||||
* @param string $extra_html Extra HTML attributes
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function combo(
|
||||
$nid,
|
||||
$data,
|
||||
$default = '',
|
||||
?string $class = '',
|
||||
?string $tabindex = '',
|
||||
bool $disabled = false,
|
||||
?string $extra_html = ''
|
||||
): string {
|
||||
self::getNameAndId($nid, $name, $id);
|
||||
if (func_num_args() > 2 && is_array($default)) {
|
||||
// Cope with associative array of optional parameters
|
||||
$options = self::getDefaults(__CLASS__, __FUNCTION__);
|
||||
$args = array_merge($options, array_intersect_key($default, $options));
|
||||
extract($args);
|
||||
}
|
||||
|
||||
return '<select name="' . $name . '" ' .
|
||||
|
||||
($id ? 'id="' . $id . '" ' : '') .
|
||||
($class ? 'class="' . $class . '" ' : '') .
|
||||
($tabindex ? 'tabindex="' . strval((int) $tabindex) . '" ' : '') .
|
||||
($disabled ? 'disabled ' : '') .
|
||||
$extra_html .
|
||||
|
||||
'>' . "\n" .
|
||||
self::comboOptions($data, $default) .
|
||||
'</select>' . "\n";
|
||||
}
|
||||
|
||||
private static function comboOptions(array $data, $default): string
|
||||
{
|
||||
$res = '';
|
||||
$option = '<option value="%1$s"%3$s>%2$s</option>' . "\n";
|
||||
$optgroup = '<optgroup label="%1$s">' . "\n" . '%2$s' . "</optgroup>\n";
|
||||
|
||||
foreach ($data as $k => $v) {
|
||||
if (is_array($v)) {
|
||||
$res .= sprintf($optgroup, $k, self::comboOptions($v, $default));
|
||||
} elseif ($v instanceof formSelectOption) {
|
||||
$res .= $v->render($default);
|
||||
} else {
|
||||
$s = ((string) $v == (string) $default) ? ' selected="selected"' : '';
|
||||
$res .= sprintf($option, $v, $k, $s);
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Radio button
|
||||
*
|
||||
* Returns HTML code for a radio button.
|
||||
* $nid could be a string or an array of name and ID.
|
||||
* $checked could be a boolean or an associative array of any of optional parameters
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param mixed $value Element value
|
||||
* @param mixed $checked True if checked | associative array of optional parameters
|
||||
* @param string $class Element class name
|
||||
* @param string $tabindex Element tabindex
|
||||
* @param boolean $disabled True if disabled
|
||||
* @param string $extra_html Extra HTML attributes
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function radio(
|
||||
$nid,
|
||||
$value,
|
||||
$checked = false,
|
||||
?string $class = '',
|
||||
?string $tabindex = '',
|
||||
bool $disabled = false,
|
||||
?string $extra_html = ''
|
||||
): string {
|
||||
self::getNameAndId($nid, $name, $id);
|
||||
if (func_num_args() > 2 && is_array($checked)) {
|
||||
// Cope with associative array of optional parameters
|
||||
$options = self::getDefaults(__CLASS__, __FUNCTION__);
|
||||
$args = array_merge($options, array_intersect_key($checked, $options));
|
||||
extract($args);
|
||||
}
|
||||
|
||||
return '<input type="radio" name="' . $name . '" value="' . $value . '" ' .
|
||||
|
||||
($id ? 'id="' . $id . '" ' : '') .
|
||||
($checked ? 'checked ' : '') .
|
||||
($class ? 'class="' . $class . '" ' : '') .
|
||||
($tabindex ? 'tabindex="' . strval((int) $tabindex) . '" ' : '') .
|
||||
($disabled ? 'disabled ' : '') .
|
||||
$extra_html .
|
||||
|
||||
'/>' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox
|
||||
*
|
||||
* Returns HTML code for a checkbox.
|
||||
* $nid could be a string or an array of name and ID.
|
||||
* $checked could be a boolean or an associative array of any of optional parameters
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param mixed $value Element value
|
||||
* @param mixed $checked True if checked | associative array of optional parameters
|
||||
* @param string $class Element class name
|
||||
* @param string $tabindex Element tabindex
|
||||
* @param boolean $disabled True if disabled
|
||||
* @param string $extra_html Extra HTML attributes
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function checkbox(
|
||||
$nid,
|
||||
$value,
|
||||
$checked = false,
|
||||
?string $class = '',
|
||||
?string $tabindex = '',
|
||||
bool $disabled = false,
|
||||
?string $extra_html = ''
|
||||
): string {
|
||||
self::getNameAndId($nid, $name, $id);
|
||||
if (func_num_args() > 2 && is_array($checked)) {
|
||||
// Cope with associative array of optional parameters
|
||||
$options = self::getDefaults(__CLASS__, __FUNCTION__);
|
||||
$args = array_merge($options, array_intersect_key($checked, $options));
|
||||
extract($args);
|
||||
}
|
||||
|
||||
return '<input type="checkbox" name="' . $name . '" value="' . $value . '" ' .
|
||||
|
||||
($id ? 'id="' . $id . '" ' : '') .
|
||||
($checked ? 'checked ' : '') .
|
||||
($class ? 'class="' . $class . '" ' : '') .
|
||||
($tabindex ? 'tabindex="' . strval((int) $tabindex) . '" ' : '') .
|
||||
($disabled ? 'disabled ' : '') .
|
||||
$extra_html .
|
||||
|
||||
' />' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Input field
|
||||
*
|
||||
* Returns HTML code for an input field.
|
||||
* $nid could be a string or an array of name and ID.
|
||||
* $default could be a string or an associative array of any of optional parameters
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param integer $size Element size
|
||||
* @param integer $max Element maxlength
|
||||
* @param mixed $default Element value | associative array of optional parameters
|
||||
* @param string $class Element class name
|
||||
* @param string $tabindex Element tabindex
|
||||
* @param boolean $disabled True if disabled
|
||||
* @param string $extra_html Extra HTML attributes
|
||||
* @param boolean $required Element is required
|
||||
* @param string $type Input type
|
||||
* @param string $autocomplete Autocomplete attributes if relevant
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function field(
|
||||
$nid,
|
||||
?int $size,
|
||||
?int $max,
|
||||
$default = '',
|
||||
?string $class = '',
|
||||
?string $tabindex = '',
|
||||
bool $disabled = false,
|
||||
?string $extra_html = '',
|
||||
bool $required = false,
|
||||
?string $type = 'text',
|
||||
?string $autocomplete = ''
|
||||
): string {
|
||||
self::getNameAndId($nid, $name, $id);
|
||||
if (func_num_args() > 3 && is_array($default)) {
|
||||
// Cope with associative array of optional parameters
|
||||
$options = self::getDefaults(__CLASS__, __FUNCTION__);
|
||||
$args = array_merge($options, array_intersect_key($default, $options));
|
||||
extract($args);
|
||||
}
|
||||
|
||||
return '<input type="' . $type . '" name="' . $name . '" ' .
|
||||
|
||||
($id ? 'id="' . $id . '" ' : '') .
|
||||
($size ? 'size="' . $size . '" ' : '') .
|
||||
($max ? 'maxlength="' . $max . '" ' : '') .
|
||||
($default || $default === '0' ? 'value="' . $default . '" ' : '') .
|
||||
($class ? 'class="' . $class . '" ' : '') .
|
||||
($tabindex ? 'tabindex="' . strval((int) $tabindex) . '" ' : '') .
|
||||
($disabled ? 'disabled ' : '') .
|
||||
($required ? 'required ' : '') .
|
||||
($autocomplete ? 'autocomplete="' . $autocomplete . '" ' : '') .
|
||||
$extra_html .
|
||||
|
||||
' />' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Password field
|
||||
*
|
||||
* Returns HTML code for a password field.
|
||||
* $nid could be a string or an array of name and ID.
|
||||
* $default could be a string or an associative array of any of optional parameters
|
||||
*
|
||||
* @uses form::field
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param integer $size Element size
|
||||
* @param integer $max Element maxlength
|
||||
* @param mixed $default Element value | associative array of optional parameters
|
||||
* @param string $class Element class name
|
||||
* @param string $tabindex Element tabindex
|
||||
* @param boolean $disabled True if disabled
|
||||
* @param string $extra_html Extra HTML attributes
|
||||
* @param boolean $required Element is required
|
||||
* @param string $autocomplete Autocomplete attributes if relevant (new-password/current-password)
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function password(
|
||||
$nid,
|
||||
int $size,
|
||||
?int $max,
|
||||
$default = '',
|
||||
?string $class = '',
|
||||
?string $tabindex = '',
|
||||
bool $disabled = false,
|
||||
?string $extra_html = '',
|
||||
bool $required = false,
|
||||
?string $autocomplete = ''
|
||||
): string {
|
||||
if (func_num_args() > 3 && is_array($default)) {
|
||||
// Cope with associative array of optional parameters
|
||||
$options = self::getDefaults(__CLASS__, __FUNCTION__);
|
||||
$args = array_merge($options, array_intersect_key($default, $options));
|
||||
extract($args);
|
||||
}
|
||||
|
||||
return self::field(
|
||||
$nid,
|
||||
$size,
|
||||
$max,
|
||||
$default,
|
||||
$class,
|
||||
$tabindex,
|
||||
$disabled,
|
||||
$extra_html,
|
||||
$required,
|
||||
'password',
|
||||
$autocomplete
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 Color field
|
||||
*
|
||||
* Returns HTML code for an input color field.
|
||||
* $nid could be a string or an array of name and ID.
|
||||
* $size could be a integer or an associative array of any of optional parameters
|
||||
*
|
||||
* @uses form::field
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param mixed $size Element size | associative array of optional parameters
|
||||
* @param integer $max Element maxlength
|
||||
* @param string $default Element value
|
||||
* @param string $class Element class name
|
||||
* @param string $tabindex Element tabindex
|
||||
* @param boolean $disabled True if disabled
|
||||
* @param string $extra_html Extra HTML attributes
|
||||
* @param boolean $required Element is required
|
||||
* @param string $autocomplete Autocomplete attributes if relevant
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function color(
|
||||
$nid,
|
||||
$size = 7,
|
||||
?int $max = 7,
|
||||
?string $default = '',
|
||||
?string $class = '',
|
||||
?string $tabindex = '',
|
||||
bool $disabled = false,
|
||||
?string $extra_html = '',
|
||||
bool $required = false,
|
||||
?string $autocomplete = ''
|
||||
): string {
|
||||
if (func_num_args() > 1 && is_array($size)) {
|
||||
// Cope with associative array of optional parameters
|
||||
$options = self::getDefaults(__CLASS__, __FUNCTION__);
|
||||
$args = array_merge($options, array_intersect_key($size, $options));
|
||||
extract($args);
|
||||
}
|
||||
|
||||
return self::field(
|
||||
$nid,
|
||||
$size,
|
||||
$max,
|
||||
$default,
|
||||
$class,
|
||||
$tabindex,
|
||||
$disabled,
|
||||
$extra_html,
|
||||
$required,
|
||||
'color',
|
||||
$autocomplete
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 Email field
|
||||
*
|
||||
* Returns HTML code for an input email field.
|
||||
* $nid could be a string or an array of name and ID.
|
||||
* $size could be a integer or an associative array of any of optional parameters
|
||||
*
|
||||
* @uses form::field
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param mixed $size Element size | associative array of optional parameters
|
||||
* @param integer $max Element maxlength
|
||||
* @param string $default Element value
|
||||
* @param string $class Element class name
|
||||
* @param string $tabindex Element tabindex
|
||||
* @param boolean $disabled True if disabled
|
||||
* @param string $extra_html Extra HTML attributes
|
||||
* @param boolean $required Element is required
|
||||
* @param string $autocomplete Autocomplete attributes if relevant
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function email(
|
||||
$nid,
|
||||
$size = 20,
|
||||
?int $max = 255,
|
||||
?string $default = '',
|
||||
?string $class = '',
|
||||
?string $tabindex = '',
|
||||
bool $disabled = false,
|
||||
?string $extra_html = '',
|
||||
bool $required = false,
|
||||
?string $autocomplete = ''
|
||||
): string {
|
||||
if (func_num_args() > 1 && is_array($size)) {
|
||||
// Cope with associative array of optional parameters
|
||||
$options = self::getDefaults(__CLASS__, __FUNCTION__);
|
||||
$args = array_merge($options, array_intersect_key($size, $options));
|
||||
extract($args);
|
||||
}
|
||||
|
||||
return self::field(
|
||||
$nid,
|
||||
$size,
|
||||
$max,
|
||||
$default,
|
||||
$class,
|
||||
$tabindex,
|
||||
$disabled,
|
||||
$extra_html,
|
||||
$required,
|
||||
'email',
|
||||
$autocomplete
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 URL field
|
||||
*
|
||||
* Returns HTML code for an input (absolute) URL field.
|
||||
* $nid could be a string or an array of name and ID.
|
||||
* $size could be a integer or an associative array of any of optional parameters
|
||||
*
|
||||
* @uses form::field
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param mixed $size Element size | associative array of optional parameters
|
||||
* @param integer $max Element maxlength
|
||||
* @param string $default Element value
|
||||
* @param string $class Element class name
|
||||
* @param string $tabindex Element tabindex
|
||||
* @param boolean $disabled True if disabled
|
||||
* @param string $extra_html Extra HTML attributes
|
||||
* @param boolean $required Element is required
|
||||
* @param string $autocomplete Autocomplete attributes if relevant
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function url(
|
||||
$nid,
|
||||
$size = 20,
|
||||
?int $max = 255,
|
||||
?string $default = '',
|
||||
?string $class = '',
|
||||
?string $tabindex = '',
|
||||
bool $disabled = false,
|
||||
?string $extra_html = '',
|
||||
bool $required = false,
|
||||
?string $autocomplete = ''
|
||||
): string {
|
||||
if (func_num_args() > 1 && is_array($size)) {
|
||||
// Cope with associative array of optional parameters
|
||||
$options = self::getDefaults(__CLASS__, __FUNCTION__);
|
||||
$args = array_merge($options, array_intersect_key($size, $options));
|
||||
extract($args);
|
||||
}
|
||||
|
||||
return self::field(
|
||||
$nid,
|
||||
$size,
|
||||
$max,
|
||||
$default,
|
||||
$class,
|
||||
$tabindex,
|
||||
$disabled,
|
||||
$extra_html,
|
||||
$required,
|
||||
'url',
|
||||
$autocomplete
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 Datetime (local) field
|
||||
*
|
||||
* Returns HTML code for an input datetime field.
|
||||
* $nid could be a string or an array of name and ID.
|
||||
* $size could be a integer or an associative array of any of optional parameters
|
||||
*
|
||||
* @uses form::field
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param mixed $size Element size | associative array of optional parameters
|
||||
* @param integer $max Element maxlength
|
||||
* @param string $default Element value (in YYYY-MM-DDThh:mm format)
|
||||
* @param string $class Element class name
|
||||
* @param string $tabindex Element tabindex
|
||||
* @param boolean $disabled True if disabled
|
||||
* @param string $extra_html Extra HTML attributes
|
||||
* @param boolean $required Element is required
|
||||
* @param string $autocomplete Autocomplete attributes if relevant
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function datetime(
|
||||
$nid,
|
||||
$size = 16,
|
||||
?int $max = 16,
|
||||
?string $default = '',
|
||||
?string $class = '',
|
||||
?string $tabindex = '',
|
||||
bool $disabled = false,
|
||||
?string $extra_html = '',
|
||||
bool $required = false,
|
||||
?string $autocomplete = ''
|
||||
): string {
|
||||
if (func_num_args() > 1 && is_array($size)) {
|
||||
// Cope with associative array of optional parameters
|
||||
$options = self::getDefaults(__CLASS__, __FUNCTION__);
|
||||
$args = array_merge($options, array_intersect_key($size, $options));
|
||||
extract($args);
|
||||
}
|
||||
// Cope with unimplemented input type for some browser (type="text" + pattern + placeholder)
|
||||
if (strpos(strtolower($extra_html), 'pattern=') === false) {
|
||||
$extra_html .= ' pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}"';
|
||||
}
|
||||
if (strpos(strtolower($extra_html), 'placeholder') === false) {
|
||||
$extra_html .= ' placeholder="1962-05-13T14:45"';
|
||||
}
|
||||
|
||||
return self::field(
|
||||
$nid,
|
||||
$size,
|
||||
$max,
|
||||
$default,
|
||||
$class,
|
||||
$tabindex,
|
||||
$disabled,
|
||||
$extra_html,
|
||||
$required,
|
||||
'datetime-local',
|
||||
$autocomplete
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 Date field
|
||||
*
|
||||
* Returns HTML code for an input date field.
|
||||
* $nid could be a string or an array of name and ID.
|
||||
* $size could be a integer or an associative array of any of optional parameters
|
||||
*
|
||||
* @uses form::field
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param mixed $size Element size | associative array of optional parameters
|
||||
* @param integer $max Element maxlength
|
||||
* @param string $default Element value (in YYYY-MM-DD format)
|
||||
* @param string $class Element class name
|
||||
* @param string $tabindex Element tabindex
|
||||
* @param boolean $disabled True if disabled
|
||||
* @param string $extra_html Extra HTML attributes
|
||||
* @param boolean $required Element is required
|
||||
* @param string $autocomplete Autocomplete attributes if relevant
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function date(
|
||||
$nid,
|
||||
$size = 10,
|
||||
?int $max = 10,
|
||||
?string $default = '',
|
||||
?string $class = '',
|
||||
?string $tabindex = '',
|
||||
bool $disabled = false,
|
||||
?string $extra_html = '',
|
||||
bool $required = false,
|
||||
?string $autocomplete = ''
|
||||
): string {
|
||||
if (func_num_args() > 1 && is_array($size)) {
|
||||
// Cope with associative array of optional parameters
|
||||
$options = self::getDefaults(__CLASS__, __FUNCTION__);
|
||||
$args = array_merge($options, array_intersect_key($size, $options));
|
||||
extract($args);
|
||||
}
|
||||
// Cope with unimplemented input type for some browser (type="text" + pattern + placeholder)
|
||||
if (strpos(strtolower($extra_html), 'pattern=') === false) {
|
||||
$extra_html .= ' pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}"';
|
||||
}
|
||||
if (strpos(strtolower($extra_html), 'placeholder') === false) {
|
||||
$extra_html .= ' placeholder="1962-05-13"';
|
||||
}
|
||||
|
||||
return self::field(
|
||||
$nid,
|
||||
$size,
|
||||
$max,
|
||||
$default,
|
||||
$class,
|
||||
$tabindex,
|
||||
$disabled,
|
||||
$extra_html,
|
||||
$required,
|
||||
'date',
|
||||
$autocomplete
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 Time (local) field
|
||||
*
|
||||
* Returns HTML code for an input time field.
|
||||
* $nid could be a string or an array of name and ID.
|
||||
* $size could be a integer or an associative array of any of optional parameters
|
||||
*
|
||||
* @uses form::field
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param mixed $size Element size | associative array of optional parameters
|
||||
* @param integer $max Element maxlength
|
||||
* @param string $default Element value (in hh:mm format)
|
||||
* @param string $class Element class name
|
||||
* @param string $tabindex Element tabindex
|
||||
* @param boolean $disabled True if disabled
|
||||
* @param string $extra_html Extra HTML attributes
|
||||
* @param boolean $required Element is required
|
||||
* @param string $autocomplete Autocomplete attributes if relevant
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function time(
|
||||
$nid,
|
||||
$size = 5,
|
||||
?int $max = 5,
|
||||
?string $default = '',
|
||||
?string $class = '',
|
||||
?string $tabindex = '',
|
||||
bool $disabled = false,
|
||||
?string $extra_html = '',
|
||||
bool $required = false,
|
||||
?string $autocomplete = ''
|
||||
): string {
|
||||
if (func_num_args() > 1 && is_array($size)) {
|
||||
// Cope with associative array of optional parameters
|
||||
$options = self::getDefaults(__CLASS__, __FUNCTION__);
|
||||
$args = array_merge($options, array_intersect_key($size, $options));
|
||||
extract($args);
|
||||
}
|
||||
// Cope with unimplemented input type for some browser (type="text" + pattern + placeholder)
|
||||
if (strpos(strtolower($extra_html), 'pattern=') === false) {
|
||||
$extra_html .= ' pattern="[0-9]{2}:[0-9]{2}"';
|
||||
}
|
||||
if (strpos(strtolower($extra_html), 'placeholder') === false) {
|
||||
$extra_html .= ' placeholder="14:45"';
|
||||
}
|
||||
|
||||
return self::field(
|
||||
$nid,
|
||||
$size,
|
||||
$max,
|
||||
$default,
|
||||
$class,
|
||||
$tabindex,
|
||||
$disabled,
|
||||
$extra_html,
|
||||
$required,
|
||||
'time',
|
||||
$autocomplete
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 file field
|
||||
*
|
||||
* Returns HTML code for an input file field.
|
||||
* $nid could be a string or an array of name and ID.
|
||||
* $default could be a integer or an associative array of any of optional parameters
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param mixed $default Element value | associative array of optional parameters
|
||||
* @param string $class Element class name
|
||||
* @param string $tabindex Element tabindex
|
||||
* @param boolean $disabled True if disabled
|
||||
* @param string $extra_html Extra HTML attributes
|
||||
* @param boolean $required Element is required
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function file(
|
||||
$nid,
|
||||
$default = '',
|
||||
?string $class = '',
|
||||
?string $tabindex = '',
|
||||
bool $disabled = false,
|
||||
?string $extra_html = '',
|
||||
bool $required = false
|
||||
): string {
|
||||
self::getNameAndId($nid, $name, $id);
|
||||
if (func_num_args() > 1 && is_array($default)) {
|
||||
// Cope with associative array of optional parameters
|
||||
$options = self::getDefaults(__CLASS__, __FUNCTION__);
|
||||
$args = array_merge($options, array_intersect_key($default, $options));
|
||||
extract($args);
|
||||
}
|
||||
|
||||
return '<input type="file" ' . '" name="' . $name . '" ' .
|
||||
|
||||
($id ? 'id="' . $id . '" ' : '') .
|
||||
($default || $default === '0' ? 'value="' . $default . '" ' : '') .
|
||||
($class ? 'class="' . $class . '" ' : '') .
|
||||
($tabindex ? 'tabindex="' . strval((int) $tabindex) . '" ' : '') .
|
||||
($disabled ? 'disabled ' : '') .
|
||||
($required ? 'required ' : '') .
|
||||
$extra_html .
|
||||
|
||||
' />' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML5 number input field
|
||||
*
|
||||
* Returns HTML code for an number input field.
|
||||
* $nid could be a string or an array of name and ID.
|
||||
* $min could be a string or an associative array of any of optional parameters
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param mixed $min Element min value (may be negative) | associative array of optional parameters
|
||||
* @param integer $max Element max value (may be negative)
|
||||
* @param string $default Element value
|
||||
* @param string $class Element class name
|
||||
* @param string $tabindex Element tabindex
|
||||
* @param boolean $disabled True if disabled
|
||||
* @param string $extra_html Extra HTML attributes
|
||||
* @param boolean $required Element is required
|
||||
* @param string $autocomplete Autocomplete attributes if relevant
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function number(
|
||||
$nid,
|
||||
$min = null,
|
||||
?int $max = null,
|
||||
?string $default = '',
|
||||
?string $class = '',
|
||||
?string $tabindex = '',
|
||||
bool $disabled = false,
|
||||
?string $extra_html = '',
|
||||
bool $required = false,
|
||||
?string $autocomplete = ''
|
||||
): string {
|
||||
self::getNameAndId($nid, $name, $id);
|
||||
if (func_num_args() > 1 && is_array($min)) {
|
||||
// Cope with associative array of optional parameters
|
||||
$options = self::getDefaults(__CLASS__, __FUNCTION__);
|
||||
$args = array_merge($options, array_intersect_key($min, $options));
|
||||
extract($args);
|
||||
}
|
||||
|
||||
return '<input type="number" name="' . $name . '" ' .
|
||||
|
||||
($id ? 'id="' . $id . '" ' : '') .
|
||||
($min !== null ? 'min="' . $min . '" ' : '') .
|
||||
($max !== null ? 'max="' . $max . '" ' : '') .
|
||||
($default || $default === '0' ? 'value="' . $default . '" ' : '') .
|
||||
($class ? 'class="' . $class . '" ' : '') .
|
||||
($tabindex ? 'tabindex="' . strval((int) $tabindex) . '" ' : '') .
|
||||
($disabled ? 'disabled ' : '') .
|
||||
($required ? 'required ' : '') .
|
||||
($autocomplete ? 'autocomplete="' . $autocomplete . '" ' : '') .
|
||||
$extra_html .
|
||||
|
||||
' />' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Textarea
|
||||
*
|
||||
* Returns HTML code for a textarea.
|
||||
* $nid could be a string or an array of name and ID.
|
||||
* $default could be a string or an associative array of any of optional parameters
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param integer $cols Number of columns
|
||||
* @param integer $rows Number of rows
|
||||
* @param mixed $default Element value | associative array of optional parameters
|
||||
* @param string $class Element class name
|
||||
* @param string $tabindex Element tabindex
|
||||
* @param boolean $disabled True if disabled
|
||||
* @param string $extra_html Extra HTML attributes
|
||||
* @param boolean $required Element is required
|
||||
* @param string $autocomplete Autocomplete attributes if relevant
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function textArea(
|
||||
$nid,
|
||||
int $cols,
|
||||
int $rows,
|
||||
$default = '',
|
||||
?string $class = '',
|
||||
?string $tabindex = '',
|
||||
bool $disabled = false,
|
||||
?string $extra_html = '',
|
||||
bool $required = false,
|
||||
?string $autocomplete = ''
|
||||
): string {
|
||||
self::getNameAndId($nid, $name, $id);
|
||||
if (func_num_args() > 3 && is_array($default)) {
|
||||
// Cope with associative array of optional parameters
|
||||
$options = self::getDefaults(__CLASS__, __FUNCTION__);
|
||||
$args = array_merge($options, array_intersect_key($default, $options));
|
||||
extract($args);
|
||||
}
|
||||
|
||||
return '<textarea cols="' . $cols . '" rows="' . $rows . '" name="' . $name . '" ' .
|
||||
|
||||
($id ? 'id="' . $id . '" ' : '') .
|
||||
($tabindex != '' ? 'tabindex="' . strval((int) $tabindex) . '" ' : '') .
|
||||
($class ? 'class="' . $class . '" ' : '') .
|
||||
($disabled ? 'disabled ' : '') .
|
||||
($required ? 'required ' : '') .
|
||||
($autocomplete ? 'autocomplete="' . $autocomplete . '" ' : '') .
|
||||
$extra_html . '>' . $default . '</textarea>' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Hidden field
|
||||
*
|
||||
* Returns HTML code for an hidden field. $nid could be a string or an array of
|
||||
* name and ID.
|
||||
*
|
||||
* @param string|array $nid Element ID and name
|
||||
* @param mixed $value Element value
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
public static function hidden($nid, $value): string
|
||||
{
|
||||
self::getNameAndId($nid, $name, $id);
|
||||
|
||||
return '<input type="hidden" name="' . $name . '" value="' . $value . '" ' .
|
||||
|
||||
($id ? 'id="' . $id . '" ' : '') .
|
||||
|
||||
' />' . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class formSelectOption
|
||||
* @brief HTML Forms creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Common
|
||||
*/
|
||||
class formSelectOption
|
||||
{
|
||||
public $name; ///< string Option name
|
||||
public $value; ///< mixed Option value
|
||||
public $class_name; ///< string Element class name
|
||||
public $html; ///< string Extra HTML attributes
|
||||
|
||||
/**
|
||||
* sprintf template for option
|
||||
* @var string $option
|
||||
* @access private
|
||||
*/
|
||||
private $option = '<option value="%1$s"%3$s>%2$s</option>' . "\n";
|
||||
|
||||
/**
|
||||
* Option constructor
|
||||
*
|
||||
* @param string $name Option name
|
||||
* @param mixed $value Option value
|
||||
* @param string $class_name Element class name
|
||||
* @param string $html Extra HTML attributes
|
||||
*/
|
||||
public function __construct(string $name, $value, string $class_name = '', string $html = '')
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->value = $value;
|
||||
$this->class_name = $class_name;
|
||||
$this->html = $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Option renderer
|
||||
*
|
||||
* Returns option HTML code
|
||||
*
|
||||
* @param string $default Value of selected option
|
||||
* @return string
|
||||
*/
|
||||
public function render(?string $default): string
|
||||
{
|
||||
$attr = $this->html ? ' ' . $this->html : '';
|
||||
$attr .= $this->class_name ? ' class="' . $this->class_name . '"' : '';
|
||||
|
||||
if ($this->value == $default) {
|
||||
$attr .= ' selected';
|
||||
}
|
||||
|
||||
return sprintf($this->option, $this->value, $this->name, $attr) . "\n";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
<?php
|
||||
/**
|
||||
* @class html
|
||||
* @brief HTML utilities
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Common
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class html
|
||||
{
|
||||
/**
|
||||
* Array of regular expression for {@link absoluteURLs()}
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $absolute_regs = [
|
||||
'/((?:action|cite|data|download|formaction|href|imagesrcset|itemid|itemprop|itemtype|ping|poster|src|srcset)=")(.*?)(")/msu',
|
||||
];
|
||||
|
||||
/**
|
||||
* HTML escape
|
||||
*
|
||||
* Replaces HTML special characters by entities.
|
||||
*
|
||||
* @param string $str String to escape
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function escapeHTML(?string $str): string
|
||||
{
|
||||
return htmlspecialchars($str ?? '', ENT_COMPAT, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode HTML entities
|
||||
*
|
||||
* Returns a string with all entities decoded.
|
||||
*
|
||||
* @param string $str String to protect
|
||||
* @param string|bool $keep_special Keep special characters: > < &
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function decodeEntities(?string $str, $keep_special = false): string
|
||||
{
|
||||
if ($keep_special) {
|
||||
$str = str_replace(
|
||||
['&', '>', '<'],
|
||||
['&amp;', '&gt;', '&lt;'],
|
||||
$str
|
||||
);
|
||||
}
|
||||
|
||||
# Some extra replacements
|
||||
$extra = [
|
||||
''' => "'",
|
||||
];
|
||||
|
||||
$str = str_replace(array_keys($extra), array_values($extra), $str);
|
||||
|
||||
return html_entity_decode($str, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove markup
|
||||
*
|
||||
* Removes every tags, comments, cdata from string
|
||||
*
|
||||
* @param string $str String to clean
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function clean(?string $str): string
|
||||
{
|
||||
$str = strip_tags($str);
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Javascript escape
|
||||
*
|
||||
* Returns a protected JavaScript string
|
||||
*
|
||||
* @param string $str String to protect
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function escapeJS(?string $str): string
|
||||
{
|
||||
$str = htmlspecialchars($str, ENT_NOQUOTES, 'UTF-8');
|
||||
$str = str_replace("'", "\'", $str);
|
||||
$str = str_replace('"', '\"', $str);
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL escape
|
||||
*
|
||||
* Returns an escaped URL string for HTML content
|
||||
*
|
||||
* @param string $str String to escape
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function escapeURL(?string $str): string
|
||||
{
|
||||
return str_replace('&', '&', $str);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL sanitize
|
||||
*
|
||||
* Encode every parts between / in url
|
||||
*
|
||||
* @param string $str String to satinize
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitizeURL(?string $str): string
|
||||
{
|
||||
return str_replace('%2F', '/', rawurlencode($str));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove host in URL
|
||||
*
|
||||
* Removes host part in URL
|
||||
*
|
||||
* @param string $url URL to transform
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function stripHostURL(?string $url): string
|
||||
{
|
||||
return preg_replace('|^[a-z]{3,}://.*?(/.*$)|', '$1', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set links to absolute ones
|
||||
*
|
||||
* Appends $root URL to URIs attributes in $str.
|
||||
*
|
||||
* @param string $str HTML to transform
|
||||
* @param string $root Base URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function absoluteURLs(?string $str, ?string $root): string
|
||||
{
|
||||
foreach (self::$absolute_regs as $pattern) {
|
||||
$str = preg_replace_callback(
|
||||
$pattern,
|
||||
function (array $matches) use ($root) {
|
||||
$url = $matches[2];
|
||||
|
||||
$link = str_replace('%', '%%', $matches[1]) . '%s' . str_replace('%', '%%', $matches[3]);
|
||||
$host = preg_replace('|^([a-z]{3,}://)(.*?)/(.*)$|', '$1$2', $root);
|
||||
|
||||
$parse = parse_url($matches[2]);
|
||||
if (empty($parse['scheme'])) {
|
||||
if (strpos($url, '//') === 0) {
|
||||
// Nothing to do. Already an absolute URL.
|
||||
} elseif (strpos($url, '/') === 0) {
|
||||
// Beginning by a / return host + url
|
||||
$url = $host . $url;
|
||||
} elseif (strpos($url, '#') === 0) {
|
||||
// Beginning by a # return root + hash
|
||||
$url = $root . $url;
|
||||
} elseif (preg_match('|/$|', $root)) {
|
||||
// Root is ending by / return root + url
|
||||
$url = $root . $url;
|
||||
} else {
|
||||
$url = dirname($root) . '/' . $url;
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf($link, $url);
|
||||
},
|
||||
$str
|
||||
);
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,461 @@
|
|||
<?php
|
||||
/**
|
||||
* @class http
|
||||
* @brief HTTP utilities
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Common
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class http
|
||||
{
|
||||
/**
|
||||
* Force HTTPS scheme on server port 443 in {@link getHost()}
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $https_scheme_on_443 = false;
|
||||
|
||||
/**
|
||||
* Cache max age for {@link cache()}
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public static $cache_max_age = 0;
|
||||
|
||||
/**
|
||||
* use X-FORWARD headers on getHost()
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $reverse_proxy = false;
|
||||
|
||||
/**
|
||||
* Self root URI
|
||||
*
|
||||
* Returns current scheme, host and port.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getHost(): string
|
||||
{
|
||||
if (self::$reverse_proxy && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
//admin have choose to allow a reverse proxy,
|
||||
//and HTTP_X_FORWARDED_FOR header means it's beeing using
|
||||
|
||||
if (!isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
|
||||
throw new Exception('Reverse proxy parametter is setted, header HTTP_X_FORWARDED_FOR is found but not the X-Forwarded-Proto. Please check your reverse proxy server settings');
|
||||
}
|
||||
|
||||
$scheme = $_SERVER['HTTP_X_FORWARDED_PROTO'];
|
||||
|
||||
if (isset($_SERVER['HTTP_HOST'])) {
|
||||
$name_port_array = explode(':', $_SERVER['HTTP_HOST']);
|
||||
} else {
|
||||
// Fallback to server name and port
|
||||
$name_port_array = [
|
||||
$_SERVER['SERVER_NAME'],
|
||||
$_SERVER['SERVER_PORT'],
|
||||
];
|
||||
}
|
||||
$server_name = $name_port_array[0];
|
||||
|
||||
$port = isset($name_port_array[1]) ? ':' . $name_port_array[1] : '';
|
||||
if (($port == ':80' && $scheme == 'http') || ($port == ':443' && $scheme == 'https')) {
|
||||
$port = '';
|
||||
}
|
||||
|
||||
return $scheme . '://' . $server_name . $port;
|
||||
}
|
||||
|
||||
if (isset($_SERVER['HTTP_HOST'])) {
|
||||
$server_name = explode(':', $_SERVER['HTTP_HOST']);
|
||||
$server_name = $server_name[0];
|
||||
} else {
|
||||
// Fallback to server name
|
||||
$server_name = $_SERVER['SERVER_NAME'];
|
||||
}
|
||||
|
||||
if (self::$https_scheme_on_443 && $_SERVER['SERVER_PORT'] == '443') {
|
||||
$scheme = 'https';
|
||||
$port = '';
|
||||
} elseif (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
|
||||
$scheme = 'https';
|
||||
$port = !in_array($_SERVER['SERVER_PORT'], ['80', '443']) ? ':' . $_SERVER['SERVER_PORT'] : '';
|
||||
} else {
|
||||
$scheme = 'http';
|
||||
$port = ($_SERVER['SERVER_PORT'] != '80') ? ':' . $_SERVER['SERVER_PORT'] : '';
|
||||
}
|
||||
|
||||
return $scheme . '://' . $server_name . $port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Self root URI
|
||||
*
|
||||
* Returns current scheme and host from a static URL.
|
||||
*
|
||||
* @param string $url URL to retrieve the host from.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getHostFromURL(string $url): string
|
||||
{
|
||||
preg_match('~^(?:((?:[a-z]+:)?//)|:(//))?(?:([^:\r\n]*?)/[^:\r\n]*|([^:\r\n]*))$~', $url, $matches);
|
||||
array_shift($matches);
|
||||
|
||||
return join($matches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Self URI
|
||||
*
|
||||
* Returns current URI with full hostname.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getSelfURI(): string
|
||||
{
|
||||
if (substr($_SERVER['REQUEST_URI'], 0, 1) != '/') {
|
||||
return self::getHost() . '/' . $_SERVER['REQUEST_URI'];
|
||||
}
|
||||
|
||||
return self::getHost() . $_SERVER['REQUEST_URI'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a full redirect URI from a relative or absolute URL
|
||||
*
|
||||
* @param string $relative_url Relative URL
|
||||
*
|
||||
* @return string full URI
|
||||
*/
|
||||
protected static function prepareRedirect(string $relative_url): string
|
||||
{
|
||||
if (preg_match('%^http[s]?://%', $relative_url)) {
|
||||
$full_url = $relative_url;
|
||||
} else {
|
||||
$host = self::getHost();
|
||||
|
||||
if (substr($relative_url, 0, 1) == '/') {
|
||||
$full_url = $host . $relative_url;
|
||||
} else {
|
||||
$path = str_replace(DIRECTORY_SEPARATOR, '/', dirname($_SERVER['PHP_SELF']));
|
||||
if (substr($path, -1) == '/') {
|
||||
$path = substr($path, 0, -1);
|
||||
}
|
||||
if ($path == '.') {
|
||||
$path = '';
|
||||
}
|
||||
$full_url = $host . $path . '/' . $relative_url;
|
||||
}
|
||||
}
|
||||
|
||||
return $full_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect
|
||||
*
|
||||
* Performs a conforming HTTP redirect for a relative URL.
|
||||
*
|
||||
* @param string $relative_url Relative URL
|
||||
*/
|
||||
public static function redirect(string $relative_url): string
|
||||
{
|
||||
# Close session if exists
|
||||
if (session_id()) {
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
header('Location: ' . self::prepareRedirect($relative_url));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concat URL and path
|
||||
*
|
||||
* Appends a path to a given URL. If path begins with "/" it will replace the original URL path.
|
||||
*
|
||||
* @param string $url URL
|
||||
* @param string $path Path to append
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function concatURL(string $url, string $path): string
|
||||
{
|
||||
// Ensure there is a trailing slash
|
||||
if (substr($url, -1, 1) != '/') {
|
||||
$url .= '/';
|
||||
}
|
||||
|
||||
if (substr($path, 0, 1) != '/') {
|
||||
return $url . $path;
|
||||
}
|
||||
|
||||
return preg_replace('#^(.+?//.+?)/(.*)$#', '$1' . $path, $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Real IP
|
||||
*
|
||||
* Returns the real client IP (or tries to do its best).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function realIP(): ?string
|
||||
{
|
||||
return $_SERVER['REMOTE_ADDR'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client unique ID
|
||||
*
|
||||
* Returns a "almost" safe client unique ID.
|
||||
*
|
||||
* @param string $key HMAC key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function browserUID(string $key): string
|
||||
{
|
||||
return crypt::hmac($key, ($_SERVER['HTTP_USER_AGENT'] ?? '') . ($_SERVER['HTTP_ACCEPT_CHARSET'] ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Client language
|
||||
*
|
||||
* Returns a two letters language code take from HTTP_ACCEPT_LANGUAGE.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAcceptLanguage(): string
|
||||
{
|
||||
$client_language_code = '';
|
||||
|
||||
if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
|
||||
$accepted_languages = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
|
||||
$first_acccepted_language = explode(';', $accepted_languages[0]);
|
||||
$client_language_code = substr(trim((string) $first_acccepted_language[0]), 0, 2);
|
||||
}
|
||||
|
||||
return $client_language_code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client languages
|
||||
*
|
||||
* Returns an array of accepted langages ordered by priority.
|
||||
* can be a two letters language code or a xx-xx variant.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getAcceptLanguages(): array
|
||||
{
|
||||
$accepted_languages = [];
|
||||
|
||||
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
|
||||
|
||||
// break up string into pieces (languages and q factors)
|
||||
preg_match_all(
|
||||
'/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i',
|
||||
$_SERVER['HTTP_ACCEPT_LANGUAGE'],
|
||||
$matches
|
||||
);
|
||||
|
||||
if (count($matches[1])) {
|
||||
// create a list like "en" => 0.8
|
||||
$accepted_languages = array_combine($matches[1], $matches[4]);
|
||||
|
||||
// set default to 1 for any without q factor
|
||||
foreach ($accepted_languages as $language => $q_factor) {
|
||||
if ($q_factor === '') {
|
||||
$accepted_languages[$language] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// sort list based on value
|
||||
arsort($accepted_languages, SORT_NUMERIC);
|
||||
$accepted_languages = array_map('strtolower', array_keys($accepted_languages));
|
||||
}
|
||||
}
|
||||
|
||||
return $accepted_languages;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP Cache
|
||||
*
|
||||
* Sends HTTP cache headers (304) according to a list of files and an optionnal.
|
||||
* list of timestamps.
|
||||
*
|
||||
* @param array $mod_files Files on which check mtime
|
||||
* @param array $mod_timestamps List of timestamps
|
||||
*/
|
||||
public static function cache(array $mod_files, array $mod_timestamps = []): void
|
||||
{
|
||||
if (empty($mod_files) || !is_array($mod_files)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace each files in array by its last modification timestamp
|
||||
array_walk($mod_files, function (&$mod_timestamp) {
|
||||
$mod_timestamp = filemtime($mod_timestamp);
|
||||
});
|
||||
|
||||
// Merge both array of timestamps
|
||||
$timestamps = array_merge($mod_timestamps, $mod_files);
|
||||
|
||||
// Sort (reverse) the resulting timestamps: most recent first [0]
|
||||
rsort($timestamps);
|
||||
|
||||
$now = time();
|
||||
$timestamp = min($timestamps[0], $now);
|
||||
|
||||
$since = null;
|
||||
if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
|
||||
$since = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
|
||||
$since = preg_replace('/^(.*)(Mon|Tue|Wed|Thu|Fri|Sat|Sun)(.*)(GMT)(.*)/', '$2$3 GMT', $since);
|
||||
$since = strtotime($since);
|
||||
$since = ($since <= $now) ? $since : null;
|
||||
}
|
||||
|
||||
# Common headers list
|
||||
$headers[] = 'Last-Modified: ' . gmdate('D, d M Y H:i:s', $timestamp) . ' GMT';
|
||||
$headers[] = 'Cache-Control: must-revalidate, max-age=' . abs((int) self::$cache_max_age);
|
||||
$headers[] = 'Pragma:';
|
||||
|
||||
if ($since >= $timestamp) {
|
||||
self::head(304, 'Not Modified');
|
||||
foreach ($headers as $header) {
|
||||
header($header);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Date: ' . gmdate('D, d M Y H:i:s', $now) . ' GMT');
|
||||
foreach ($headers as $header) {
|
||||
header($header);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP Etag
|
||||
*
|
||||
* Sends HTTP cache headers (304) according to a list of etags in client request.
|
||||
*/
|
||||
public static function etag(...$args): void
|
||||
{
|
||||
if (empty($args)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We create an etag from all arguments (given arrays are flattened)
|
||||
$args = iterator_to_array(new \RecursiveIteratorIterator(new \RecursiveArrayIterator($args)), false);
|
||||
$etag = '"' . md5(implode('', $args)) . '"';
|
||||
|
||||
header('ETag: ' . $etag);
|
||||
|
||||
# Do we have a previously sent content?
|
||||
if (!empty($_SERVER['HTTP_IF_NONE_MATCH'])) {
|
||||
foreach (explode(',', $_SERVER['HTTP_IF_NONE_MATCH']) as $i) {
|
||||
if (stripslashes(trim($i)) == $etag) {
|
||||
self::head(304, 'Not Modified');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP Header
|
||||
*
|
||||
* Sends an HTTP code and message to client.
|
||||
*
|
||||
* @param int $code HTTP code
|
||||
* @param string $msg Message
|
||||
*/
|
||||
public static function head(int $code, $msg = null): void
|
||||
{
|
||||
$status_mode = preg_match('/cgi/', PHP_SAPI);
|
||||
|
||||
if (!$msg) {
|
||||
$msg_codes = [
|
||||
100 => 'Continue',
|
||||
101 => 'Switching Protocols',
|
||||
200 => 'OK',
|
||||
201 => 'Created',
|
||||
202 => 'Accepted',
|
||||
203 => 'Non-Authoritative Information',
|
||||
204 => 'No Content',
|
||||
205 => 'Reset Content',
|
||||
206 => 'Partial Content',
|
||||
300 => 'Multiple Choices',
|
||||
301 => 'Moved Permanently',
|
||||
302 => 'Found',
|
||||
303 => 'See Other',
|
||||
304 => 'Not Modified',
|
||||
305 => 'Use Proxy',
|
||||
307 => 'Temporary Redirect',
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
402 => 'Payment Required',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
406 => 'Not Acceptable',
|
||||
407 => 'Proxy Authentication Required',
|
||||
408 => 'Request Timeout',
|
||||
409 => 'Conflict',
|
||||
410 => 'Gone',
|
||||
411 => 'Length Required',
|
||||
412 => 'Precondition Failed',
|
||||
413 => 'Request Entity Too Large',
|
||||
414 => 'Request-URI Too Long',
|
||||
415 => 'Unsupported Media Type',
|
||||
416 => 'Requested Range Not Satisfiable',
|
||||
417 => 'Expectation Failed',
|
||||
500 => 'Internal Server Error',
|
||||
501 => 'Not Implemented',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
504 => 'Gateway Timeout',
|
||||
505 => 'HTTP Version Not Supported',
|
||||
];
|
||||
|
||||
$msg = $msg_codes[$code] ?? '-';
|
||||
}
|
||||
|
||||
if ($status_mode) {
|
||||
header('Status: ' . $code . ' ' . $msg);
|
||||
} else {
|
||||
header($msg, true, $code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim request
|
||||
*
|
||||
* Trims every value in GET, POST, REQUEST and COOKIE vars.
|
||||
* Removes magic quotes if magic_quote_gpc is on.
|
||||
*/
|
||||
public static function trimRequest(): void
|
||||
{
|
||||
$cleanup = function (&$value) { $value = trim((string) $value); };
|
||||
|
||||
if (!empty($_GET)) {
|
||||
array_walk_recursive($_GET, $cleanup);
|
||||
}
|
||||
if (!empty($_POST)) {
|
||||
array_walk_recursive($_POST, $cleanup);
|
||||
}
|
||||
if (!empty($_REQUEST)) {
|
||||
array_walk_recursive($_REQUEST, $cleanup);
|
||||
}
|
||||
if (!empty($_COOKIE)) {
|
||||
array_walk_recursive($_COOKIE, $cleanup);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,351 @@
|
|||
<?php
|
||||
/**
|
||||
* @class text
|
||||
* @brief Text utilities
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Common
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class text
|
||||
{
|
||||
/**
|
||||
* Check email address
|
||||
*
|
||||
* Returns true if $email is a valid email address.
|
||||
*
|
||||
* @param string $email Email string
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isEmail(string $email): bool
|
||||
{
|
||||
return (filter_var($email, FILTER_VALIDATE_EMAIL) !== false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accents replacement
|
||||
*
|
||||
* Replaces some occidental accentuated characters by their ASCII
|
||||
* representation.
|
||||
*
|
||||
* @param string $str String to deaccent
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function deaccent(string $str): string
|
||||
{
|
||||
$pattern['A'] = '\x{00C0}-\x{00C5}';
|
||||
$pattern['AE'] = '\x{00C6}';
|
||||
$pattern['C'] = '\x{00C7}';
|
||||
$pattern['D'] = '\x{00D0}';
|
||||
$pattern['E'] = '\x{00C8}-\x{00CB}';
|
||||
$pattern['I'] = '\x{00CC}-\x{00CF}';
|
||||
$pattern['N'] = '\x{00D1}';
|
||||
$pattern['O'] = '\x{00D2}-\x{00D6}\x{00D8}';
|
||||
$pattern['OE'] = '\x{0152}';
|
||||
$pattern['S'] = '\x{0160}';
|
||||
$pattern['U'] = '\x{00D9}-\x{00DC}';
|
||||
$pattern['Y'] = '\x{00DD}';
|
||||
$pattern['Z'] = '\x{017D}';
|
||||
|
||||
$pattern['a'] = '\x{00E0}-\x{00E5}';
|
||||
$pattern['ae'] = '\x{00E6}';
|
||||
$pattern['c'] = '\x{00E7}';
|
||||
$pattern['d'] = '\x{00F0}';
|
||||
$pattern['e'] = '\x{00E8}-\x{00EB}';
|
||||
$pattern['i'] = '\x{00EC}-\x{00EF}';
|
||||
$pattern['n'] = '\x{00F1}';
|
||||
$pattern['o'] = '\x{00F2}-\x{00F6}\x{00F8}';
|
||||
$pattern['oe'] = '\x{0153}';
|
||||
$pattern['s'] = '\x{0161}';
|
||||
$pattern['u'] = '\x{00F9}-\x{00FC}';
|
||||
$pattern['y'] = '\x{00FD}\x{00FF}';
|
||||
$pattern['z'] = '\x{017E}';
|
||||
|
||||
$pattern['ss'] = '\x{00DF}';
|
||||
|
||||
foreach ($pattern as $r => $p) {
|
||||
$str = preg_replace('/[' . $p . ']/u', $r, $str);
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* String to URL
|
||||
*
|
||||
* Transforms a string to a proper URL.
|
||||
*
|
||||
* @param string $str String to transform
|
||||
* @param bool $with_slashes Keep slashes in URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function str2URL(string $str, bool $with_slashes = true): string
|
||||
{
|
||||
$str = self::deaccent($str);
|
||||
$str = preg_replace('/[^A-Za-z0-9_\s\'\:\/[\]-]/', '', $str);
|
||||
|
||||
return self::tidyURL($str, $with_slashes);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL cleanup
|
||||
*
|
||||
* @param string $str URL to tidy
|
||||
* @param bool $keep_slashes Keep slashes in URL
|
||||
* @param bool $keep_spaces Keep spaces in URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function tidyURL(string $str, bool $keep_slashes = true, bool $keep_spaces = false): string
|
||||
{
|
||||
$str = strip_tags($str);
|
||||
$str = str_replace(['?', '&', '#', '=', '+', '<', '>', '"', '%'], '', $str);
|
||||
$str = str_replace("'", ' ', $str);
|
||||
$str = preg_replace('/[\s]+/u', ' ', trim($str));
|
||||
|
||||
if (!$keep_slashes) {
|
||||
$str = str_replace('/', '-', $str);
|
||||
}
|
||||
|
||||
if (!$keep_spaces) {
|
||||
$str = str_replace(' ', '-', $str);
|
||||
}
|
||||
|
||||
$str = preg_replace('/\-+/', '-', $str);
|
||||
|
||||
# Remove path changes in URL
|
||||
$str = preg_replace('%^/%', '', $str);
|
||||
$str = preg_replace('%\.+/%', '', $str);
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cut string
|
||||
*
|
||||
* Returns a cuted string on spaced at given length $l.
|
||||
*
|
||||
* @param string $str String to cut
|
||||
* @param integer $length Length to keep
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function cutString(string $str, int $length): string
|
||||
{
|
||||
$s = preg_split('/([\s]+)/u', $str, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
|
||||
$res = '';
|
||||
$L = 0;
|
||||
|
||||
if (mb_strlen($s[0]) >= $length) {
|
||||
return mb_substr($s[0], 0, $length);
|
||||
}
|
||||
|
||||
foreach ($s as $v) {
|
||||
$L = $L + mb_strlen($v);
|
||||
|
||||
if ($L > $length) {
|
||||
break;
|
||||
}
|
||||
$res .= $v;
|
||||
}
|
||||
|
||||
return trim($res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split words
|
||||
*
|
||||
* Returns an array of words from a given string.
|
||||
*
|
||||
* @param string $str Words to split
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function splitWords(string $str): array
|
||||
{
|
||||
$non_word = '\x{0000}-\x{002F}\x{003A}-\x{0040}\x{005b}-\x{0060}\x{007B}-\x{007E}\x{00A0}-\x{00BF}\s';
|
||||
if (preg_match_all('/([^' . $non_word . ']{3,})/msu', html::clean($str), $match)) {
|
||||
foreach ($match[1] as $i => $v) {
|
||||
$match[1][$i] = mb_strtolower($v);
|
||||
}
|
||||
|
||||
return $match[1];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Encoding detection
|
||||
*
|
||||
* Returns the encoding (in lowercase) of given $str.
|
||||
*
|
||||
* @param string $str String
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function detectEncoding(string $str): string
|
||||
{
|
||||
return strtolower((string) mb_detect_encoding(
|
||||
$str,
|
||||
[
|
||||
'UTF-8',
|
||||
'ISO-8859-1',
|
||||
'ISO-8859-2',
|
||||
'ISO-8859-3',
|
||||
'ISO-8859-4',
|
||||
'ISO-8859-5',
|
||||
'ISO-8859-6',
|
||||
'ISO-8859-7',
|
||||
'ISO-8859-8',
|
||||
'ISO-8859-9',
|
||||
'ISO-8859-10',
|
||||
'ISO-8859-13',
|
||||
'ISO-8859-14',
|
||||
'ISO-8859-15',
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* UTF8 conversions
|
||||
*
|
||||
* Returns an UTF-8 converted string. If $encoding is not specified, the
|
||||
* function will try to detect encoding.
|
||||
*
|
||||
* @param string $str String to convert
|
||||
* @param string $encoding Optionnal "from" encoding
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function toUTF8(string $str, ?string $encoding = null): string
|
||||
{
|
||||
if (!$encoding) {
|
||||
$encoding = self::detectEncoding($str);
|
||||
}
|
||||
|
||||
if ($encoding !== 'utf-8') {
|
||||
$str = iconv($encoding, 'UTF-8', $str);
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find bad UTF8 tokens
|
||||
*
|
||||
* Locates the first bad byte in a UTF-8 string returning it's
|
||||
* byte index in the string
|
||||
* PCRE Pattern to locate bad bytes in a UTF-8 string
|
||||
* Comes from W3 FAQ: Multilingual Forms
|
||||
* Note: modified to include full ASCII range including control chars
|
||||
*
|
||||
* @copyright Harry Fuecks (http://phputf8.sourceforge.net <a href="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">GNU LGPL 2.1</a>)
|
||||
*
|
||||
* @param string $str String to search
|
||||
*
|
||||
* @return integer|false
|
||||
*/
|
||||
public static function utf8badFind(string $str)
|
||||
{
|
||||
$UTF8_BAD = '([\x00-\x7F]' . # ASCII (including control chars)
|
||||
'|[\xC2-\xDF][\x80-\xBF]' . # non-overlong 2-byte
|
||||
'|\xE0[\xA0-\xBF][\x80-\xBF]' . # excluding overlongs
|
||||
'|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}' . # straight 3-byte
|
||||
'|\xED[\x80-\x9F][\x80-\xBF]' . # excluding surrogates
|
||||
'|\xF0[\x90-\xBF][\x80-\xBF]{2}' . # planes 1-3
|
||||
'|[\xF1-\xF3][\x80-\xBF]{3}' . # planes 4-15
|
||||
'|\xF4[\x80-\x8F][\x80-\xBF]{2}' . # plane 16
|
||||
'|(.{1}))'; # invalid byte
|
||||
$pos = 0;
|
||||
|
||||
while (preg_match('/' . $UTF8_BAD . '/S', $str, $matches)) {
|
||||
$bytes = strlen($matches[0]);
|
||||
if (isset($matches[2])) {
|
||||
return $pos;
|
||||
}
|
||||
$pos += $bytes;
|
||||
$str = substr($str, $bytes);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* UTF-8 cleanup
|
||||
*
|
||||
* Replaces non UTF-8 bytes in $str by $repl.
|
||||
*
|
||||
* @copyright Harry Fuecks (http://phputf8.sourceforge.net <a href="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">GNU LGPL 2.1</a>)
|
||||
*
|
||||
* @param string $str String to clean
|
||||
* @param string $repl Replacement string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function cleanUTF8(string $str, string $repl = '?'): string
|
||||
{
|
||||
while (($bad_index = self::utf8badFind($str)) !== false) {
|
||||
$str = substr_replace($str, $repl, $bad_index, 1);
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* BOM removal (UTF-8 only)
|
||||
*
|
||||
* Removes BOM from the begining of a string if present.
|
||||
*
|
||||
* @param string $str String to clean
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function removeBOM(string $str): string
|
||||
{
|
||||
if (substr_count($str, "\xEF\xBB\xBF")) {
|
||||
return str_replace("\xEF\xBB\xBF", '', $str);
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quoted printable conversion
|
||||
*
|
||||
* Encodes given str to quoted printable
|
||||
*
|
||||
* @param string $str String to encode
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function QPEncode(string $str): string
|
||||
{
|
||||
$res = '';
|
||||
|
||||
foreach (preg_split("/\r?\n/msu", $str) as $line) {
|
||||
$l = '';
|
||||
preg_match_all('/./', $line, $m);
|
||||
|
||||
foreach ($m[0] as $c) {
|
||||
$a = ord($c);
|
||||
|
||||
if ($a < 32 || $a == 61 || $a > 126) {
|
||||
$c = sprintf('=%02X', $a);
|
||||
}
|
||||
|
||||
$l .= $c;
|
||||
}
|
||||
|
||||
$res .= $l . "\r\n";
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,407 @@
|
|||
Africa/Abidjan
|
||||
Africa/Accra
|
||||
Africa/Addis_Ababa
|
||||
Africa/Algiers
|
||||
Africa/Asmera
|
||||
Africa/Bamako
|
||||
Africa/Bangui
|
||||
Africa/Banjul
|
||||
Africa/Bissau
|
||||
Africa/Blantyre
|
||||
Africa/Brazzaville
|
||||
Africa/Bujumbura
|
||||
Africa/Cairo
|
||||
Africa/Casablanca
|
||||
Africa/Ceuta
|
||||
Africa/Conakry
|
||||
Africa/Dakar
|
||||
Africa/Dar_es_Salaam
|
||||
Africa/Djibouti
|
||||
Africa/Douala
|
||||
Africa/El_Aaiun
|
||||
Africa/Freetown
|
||||
Africa/Gaborone
|
||||
Africa/Harare
|
||||
Africa/Johannesburg
|
||||
Africa/Kampala
|
||||
Africa/Khartoum
|
||||
Africa/Kigali
|
||||
Africa/Kinshasa
|
||||
Africa/Lagos
|
||||
Africa/Libreville
|
||||
Africa/Lome
|
||||
Africa/Luanda
|
||||
Africa/Lubumbashi
|
||||
Africa/Lusaka
|
||||
Africa/Malabo
|
||||
Africa/Maputo
|
||||
Africa/Maseru
|
||||
Africa/Mbabane
|
||||
Africa/Mogadishu
|
||||
Africa/Monrovia
|
||||
Africa/Nairobi
|
||||
Africa/Ndjamena
|
||||
Africa/Niamey
|
||||
Africa/Nouakchott
|
||||
Africa/Ouagadougou
|
||||
Africa/Porto-Novo
|
||||
Africa/Sao_Tome
|
||||
Africa/Timbuktu
|
||||
Africa/Tripoli
|
||||
Africa/Tunis
|
||||
Africa/Windhoek
|
||||
|
||||
|
||||
America/Adak
|
||||
America/Anchorage
|
||||
America/Anguilla
|
||||
America/Antigua
|
||||
America/Araguaina
|
||||
America/Argentina/Buenos_Aires
|
||||
America/Argentina/Catamarca
|
||||
America/Argentina/ComodRivadavia
|
||||
America/Argentina/Cordoba
|
||||
America/Argentina/Jujuy
|
||||
America/Argentina/La_Rioja
|
||||
America/Argentina/Mendoza
|
||||
America/Argentina/Rio_Gallegos
|
||||
America/Argentina/San_Juan
|
||||
America/Argentina/Tucuman
|
||||
America/Argentina/Ushuaia
|
||||
America/Aruba
|
||||
America/Asuncion
|
||||
America/Bahia
|
||||
America/Barbados
|
||||
America/Belem
|
||||
America/Belize
|
||||
America/Boa_Vista
|
||||
America/Bogota
|
||||
America/Boise
|
||||
America/Cambridge_Bay
|
||||
America/Campo_Grande
|
||||
America/Cancun
|
||||
America/Caracas
|
||||
America/Cayenne
|
||||
America/Cayman
|
||||
America/Chicago
|
||||
America/Chihuahua
|
||||
America/Costa_Rica
|
||||
America/Cuiaba
|
||||
America/Curacao
|
||||
America/Danmarkshavn
|
||||
America/Dawson
|
||||
America/Dawson_Creek
|
||||
America/Denver
|
||||
America/Detroit
|
||||
America/Dominica
|
||||
America/Edmonton
|
||||
America/Eirunepe
|
||||
America/El_Salvador
|
||||
America/Fortaleza
|
||||
America/Glace_Bay
|
||||
America/Godthab
|
||||
America/Goose_Bay
|
||||
America/Grand_Turk
|
||||
America/Grenada
|
||||
America/Guadeloupe
|
||||
America/Guatemala
|
||||
America/Guayaquil
|
||||
America/Guyana
|
||||
America/Halifax
|
||||
America/Havana
|
||||
America/Hermosillo
|
||||
America/Indiana/Indianapolis
|
||||
America/Indiana/Knox
|
||||
America/Indiana/Marengo
|
||||
America/Indiana/Vevay
|
||||
America/Indianapolis
|
||||
America/Inuvik
|
||||
America/Iqaluit
|
||||
America/Jamaica
|
||||
America/Juneau
|
||||
America/Kentucky/Louisville
|
||||
America/Kentucky/Monticello
|
||||
America/La_Paz
|
||||
America/Lima
|
||||
America/Los_Angeles
|
||||
America/Louisville
|
||||
America/Maceio
|
||||
America/Managua
|
||||
America/Manaus
|
||||
America/Martinique
|
||||
America/Mazatlan
|
||||
America/Menominee
|
||||
America/Merida
|
||||
America/Mexico_City
|
||||
America/Miquelon
|
||||
America/Monterrey
|
||||
America/Montevideo
|
||||
America/Montreal
|
||||
America/Montserrat
|
||||
America/Nassau
|
||||
America/New_York
|
||||
America/Nipigon
|
||||
America/Nome
|
||||
America/Noronha
|
||||
America/North_Dakota/Center
|
||||
America/Panama
|
||||
America/Pangnirtung
|
||||
America/Paramaribo
|
||||
America/Phoenix
|
||||
America/Port-au-Prince
|
||||
America/Port_of_Spain
|
||||
America/Porto_Velho
|
||||
America/Puerto_Rico
|
||||
America/Rainy_River
|
||||
America/Rankin_Inlet
|
||||
America/Recife
|
||||
America/Regina
|
||||
America/Rio_Branco
|
||||
America/Santiago
|
||||
America/Santo_Domingo
|
||||
America/Sao_Paulo
|
||||
America/Scoresbysund
|
||||
America/Shiprock
|
||||
America/St_Johns
|
||||
America/St_Kitts
|
||||
America/St_Lucia
|
||||
America/St_Thomas
|
||||
America/St_Vincent
|
||||
America/Swift_Current
|
||||
America/Tegucigalpa
|
||||
America/Thule
|
||||
America/Thunder_Bay
|
||||
America/Tijuana
|
||||
America/Toronto
|
||||
America/Tortola
|
||||
America/Vancouver
|
||||
America/Whitehorse
|
||||
America/Winnipeg
|
||||
America/Yakutat
|
||||
America/Yellowknife
|
||||
|
||||
|
||||
Antarctica/Casey
|
||||
Antarctica/Davis
|
||||
Antarctica/DumontDUrville
|
||||
Antarctica/Mawson
|
||||
Antarctica/McMurdo
|
||||
Antarctica/Palmer
|
||||
Antarctica/Rothera
|
||||
Antarctica/South_Pole
|
||||
Antarctica/Syowa
|
||||
Antarctica/Vostok
|
||||
|
||||
|
||||
Arctic/Longyearbyen
|
||||
|
||||
|
||||
Asia/Aden
|
||||
Asia/Almaty
|
||||
Asia/Amman
|
||||
Asia/Anadyr
|
||||
Asia/Aqtau
|
||||
Asia/Aqtobe
|
||||
Asia/Ashgabat
|
||||
Asia/Baghdad
|
||||
Asia/Bahrain
|
||||
Asia/Baku
|
||||
Asia/Bangkok
|
||||
Asia/Beirut
|
||||
Asia/Bishkek
|
||||
Asia/Brunei
|
||||
Asia/Calcutta
|
||||
Asia/Choibalsan
|
||||
Asia/Chongqing
|
||||
Asia/Colombo
|
||||
Asia/Damascus
|
||||
Asia/Dhaka
|
||||
Asia/Dili
|
||||
Asia/Dubai
|
||||
Asia/Dushanbe
|
||||
Asia/Gaza
|
||||
Asia/Harbin
|
||||
Asia/Hong_Kong
|
||||
Asia/Hovd
|
||||
Asia/Irkutsk
|
||||
Asia/Istanbul
|
||||
Asia/Jakarta
|
||||
Asia/Jayapura
|
||||
Asia/Jerusalem
|
||||
Asia/Kabul
|
||||
Asia/Kamchatka
|
||||
Asia/Karachi
|
||||
Asia/Kashgar
|
||||
Asia/Katmandu
|
||||
Asia/Krasnoyarsk
|
||||
Asia/Kuala_Lumpur
|
||||
Asia/Kuching
|
||||
Asia/Kuwait
|
||||
Asia/Macau
|
||||
Asia/Magadan
|
||||
Asia/Makassar
|
||||
Asia/Manila
|
||||
Asia/Muscat
|
||||
Asia/Nicosia
|
||||
Asia/Novosibirsk
|
||||
Asia/Omsk
|
||||
Asia/Oral
|
||||
Asia/Phnom_Penh
|
||||
Asia/Pontianak
|
||||
Asia/Pyongyang
|
||||
Asia/Qatar
|
||||
Asia/Qyzylorda
|
||||
Asia/Rangoon
|
||||
Asia/Riyadh
|
||||
Asia/Saigon
|
||||
Asia/Sakhalin
|
||||
Asia/Samarkand
|
||||
Asia/Seoul
|
||||
Asia/Shanghai
|
||||
Asia/Singapore
|
||||
Asia/Taipei
|
||||
Asia/Tashkent
|
||||
Asia/Tbilisi
|
||||
Asia/Tehran
|
||||
Asia/Thimphu
|
||||
Asia/Tokyo
|
||||
Asia/Ulaanbaatar
|
||||
Asia/Urumqi
|
||||
Asia/Vientiane
|
||||
Asia/Vladivostok
|
||||
Asia/Yakutsk
|
||||
Asia/Yekaterinburg
|
||||
Asia/Yerevan
|
||||
|
||||
|
||||
Atlantic/Azores
|
||||
Atlantic/Bermuda
|
||||
Atlantic/Canary
|
||||
Atlantic/Cape_Verde
|
||||
Atlantic/Faeroe
|
||||
Atlantic/Jan_Mayen
|
||||
Atlantic/Madeira
|
||||
Atlantic/Reykjavik
|
||||
Atlantic/South_Georgia
|
||||
Atlantic/St_Helena
|
||||
Atlantic/Stanley
|
||||
|
||||
|
||||
Australia/Adelaide
|
||||
Australia/Brisbane
|
||||
Australia/Broken_Hill
|
||||
Australia/Darwin
|
||||
Australia/Hobart
|
||||
Australia/Lindeman
|
||||
Australia/Lord_Howe
|
||||
Australia/Melbourne
|
||||
Australia/Perth
|
||||
Australia/Sydney
|
||||
|
||||
|
||||
Europe/Amsterdam
|
||||
Europe/Andorra
|
||||
Europe/Athens
|
||||
Europe/Belfast
|
||||
Europe/Belgrade
|
||||
Europe/Berlin
|
||||
Europe/Bratislava
|
||||
Europe/Brussels
|
||||
Europe/Bucharest
|
||||
Europe/Budapest
|
||||
Europe/Chisinau
|
||||
Europe/Copenhagen
|
||||
Europe/Dublin
|
||||
Europe/Gibraltar
|
||||
Europe/Helsinki
|
||||
Europe/Istanbul
|
||||
Europe/Kaliningrad
|
||||
Europe/Kiev
|
||||
Europe/Lisbon
|
||||
Europe/Ljubljana
|
||||
Europe/London
|
||||
Europe/Luxembourg
|
||||
Europe/Madrid
|
||||
Europe/Malta
|
||||
Europe/Mariehamn
|
||||
Europe/Minsk
|
||||
Europe/Monaco
|
||||
Europe/Moscow
|
||||
Europe/Nicosia
|
||||
Europe/Oslo
|
||||
Europe/Paris
|
||||
Europe/Prague
|
||||
Europe/Riga
|
||||
Europe/Rome
|
||||
Europe/Samara
|
||||
Europe/San_Marino
|
||||
Europe/Sarajevo
|
||||
Europe/Simferopol
|
||||
Europe/Skopje
|
||||
Europe/Sofia
|
||||
Europe/Stockholm
|
||||
Europe/Tallinn
|
||||
Europe/Tirane
|
||||
Europe/Uzhgorod
|
||||
Europe/Vaduz
|
||||
Europe/Vatican
|
||||
Europe/Vienna
|
||||
Europe/Vilnius
|
||||
Europe/Warsaw
|
||||
Europe/Zagreb
|
||||
Europe/Zaporozhye
|
||||
Europe/Zurich
|
||||
|
||||
|
||||
Indian/Antananarivo
|
||||
Indian/Chagos
|
||||
Indian/Christmas
|
||||
Indian/Cocos
|
||||
Indian/Comoro
|
||||
Indian/Kerguelen
|
||||
Indian/Mahe
|
||||
Indian/Maldives
|
||||
Indian/Mauritius
|
||||
Indian/Mayotte
|
||||
Indian/Reunion
|
||||
|
||||
|
||||
Pacific/Apia
|
||||
Pacific/Auckland
|
||||
Pacific/Chatham
|
||||
Pacific/Easter
|
||||
Pacific/Efate
|
||||
Pacific/Enderbury
|
||||
Pacific/Fakaofo
|
||||
Pacific/Fiji
|
||||
Pacific/Funafuti
|
||||
Pacific/Galapagos
|
||||
Pacific/Gambier
|
||||
Pacific/Guadalcanal
|
||||
Pacific/Guam
|
||||
Pacific/Honolulu
|
||||
Pacific/Johnston
|
||||
Pacific/Kiritimati
|
||||
Pacific/Kosrae
|
||||
Pacific/Kwajalein
|
||||
Pacific/Majuro
|
||||
Pacific/Marquesas
|
||||
Pacific/Midway
|
||||
Pacific/Nauru
|
||||
Pacific/Niue
|
||||
Pacific/Norfolk
|
||||
Pacific/Noumea
|
||||
Pacific/Pago_Pago
|
||||
Pacific/Palau
|
||||
Pacific/Pitcairn
|
||||
Pacific/Ponape
|
||||
Pacific/Port_Moresby
|
||||
Pacific/Rarotonga
|
||||
Pacific/Saipan
|
||||
Pacific/Tahiti
|
||||
Pacific/Tarawa
|
||||
Pacific/Tongatapu
|
||||
Pacific/Truk
|
||||
Pacific/Wake
|
||||
Pacific/Wallis
|
||||
Pacific/Yap
|
|
@ -0,0 +1,260 @@
|
|||
<?php
|
||||
/**
|
||||
* @class cursor
|
||||
* @brief DBLayer Cursor
|
||||
*
|
||||
* This class implements facilities to insert or update in a table.
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage DBLayer
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class cursor
|
||||
{
|
||||
/**
|
||||
* @var dbLayer
|
||||
*/
|
||||
private $__con;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $__data = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $__table;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Init cursor object on a given table. Note that you can init it with
|
||||
* {@link dbLayer::openCursor() openCursor()} method of your connection object.
|
||||
*
|
||||
* Example:
|
||||
* <code>
|
||||
* <?php
|
||||
* $cur = $con->openCursor('table');
|
||||
* $cur->field1 = 1;
|
||||
* $cur->field2 = 'foo';
|
||||
* $cur->insert(); // Insert field ...
|
||||
*
|
||||
* $cur->update('WHERE field3 = 4'); // ... or update field
|
||||
* ?>
|
||||
* </code>
|
||||
*
|
||||
* @see dbLayer::openCursor()
|
||||
*
|
||||
* @param dbLayer $con Connection object
|
||||
* @param string $table Table name
|
||||
*/
|
||||
public function __construct(dbLayer $con, string $table)
|
||||
{
|
||||
$this->__con = &$con;
|
||||
$this->setTable($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set table
|
||||
*
|
||||
* Changes working table and resets data
|
||||
*
|
||||
* @param string $table Table name
|
||||
*/
|
||||
public function setTable(string $table): void
|
||||
{
|
||||
$this->__table = $table;
|
||||
$this->__data = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set field
|
||||
*
|
||||
* Set value <var>$value</var> to a field named <var>$name</var>. Value could be
|
||||
* an string, an integer, a float, a null value or an array.
|
||||
*
|
||||
* If value is an array, its first value will be interpreted as a SQL
|
||||
* command. String values will be automatically escaped.
|
||||
*
|
||||
* @see __set()
|
||||
*
|
||||
* @param string $name Field name
|
||||
* @param mixed $value Field value
|
||||
*/
|
||||
public function setField(string $name, $value): void
|
||||
{
|
||||
$this->__data[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset field
|
||||
*
|
||||
* Remove a field from data set.
|
||||
*
|
||||
* @param string $name Field name
|
||||
*/
|
||||
public function unsetField(string $name): void
|
||||
{
|
||||
unset($this->__data[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Field exists
|
||||
*
|
||||
* @return boolean true if field named <var>$name</var> exists
|
||||
*/
|
||||
public function isField(string $name): bool
|
||||
{
|
||||
return isset($this->__data[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Field value
|
||||
*
|
||||
* @see __get()
|
||||
*
|
||||
* @return mixed value for a field named <var>$name</var>
|
||||
*/
|
||||
public function getField(string $name)
|
||||
{
|
||||
if (isset($this->__data[$name])) {
|
||||
return $this->__data[$name];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Field
|
||||
*
|
||||
* Magic alias for {@link setField()}
|
||||
*/
|
||||
public function __set(string $name, $value): void
|
||||
{
|
||||
$this->setField($name, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Field value
|
||||
*
|
||||
* Magic alias for {@link getField()}
|
||||
*
|
||||
* @return mixed value for a field named <var>$n</var>
|
||||
*/
|
||||
public function __get(string $name)
|
||||
{
|
||||
return $this->getField($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty data set
|
||||
*
|
||||
* Removes all data from data set
|
||||
*/
|
||||
public function clean(): void
|
||||
{
|
||||
$this->__data = [];
|
||||
}
|
||||
|
||||
private function formatFields(): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach ($this->__data as $k => $v) {
|
||||
$k = $this->__con->escapeSystem($k);
|
||||
|
||||
if (is_null($v)) {
|
||||
$data[$k] = 'NULL';
|
||||
} elseif (is_string($v)) {
|
||||
$data[$k] = "'" . $this->__con->escape($v) . "'";
|
||||
} elseif (is_array($v)) {
|
||||
$data[$k] = is_string($v[0]) ? "'" . $this->__con->escape($v[0]) . "'" : $v[0];
|
||||
} else {
|
||||
$data[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get insert query
|
||||
*
|
||||
* Returns the generated INSERT query
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getInsert(): string
|
||||
{
|
||||
$data = $this->formatFields();
|
||||
|
||||
return 'INSERT INTO ' . $this->__con->escapeSystem($this->__table) . " (\n" .
|
||||
implode(",\n", array_keys($data)) . "\n) VALUES (\n" .
|
||||
implode(",\n", array_values($data)) . "\n) ";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get update query
|
||||
*
|
||||
* Returns the generated UPDATE query
|
||||
*
|
||||
* @param string $where WHERE condition
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUpdate(string $where): string
|
||||
{
|
||||
$data = $this->formatFields();
|
||||
$fields = [];
|
||||
|
||||
$updReq = 'UPDATE ' . $this->__con->escapeSystem($this->__table) . " SET \n";
|
||||
|
||||
foreach ($data as $k => $v) {
|
||||
$fields[] = $k . ' = ' . $v . '';
|
||||
}
|
||||
|
||||
$updReq .= implode(",\n", $fields);
|
||||
$updReq .= "\n" . $where;
|
||||
|
||||
return $updReq;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute insert query
|
||||
*
|
||||
* Executes the generated INSERT query
|
||||
*/
|
||||
public function insert(): bool
|
||||
{
|
||||
if (!$this->__table) {
|
||||
throw new Exception('No table name.');
|
||||
}
|
||||
|
||||
$insReq = $this->getInsert();
|
||||
|
||||
$this->__con->execute($insReq);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute update query
|
||||
*
|
||||
* Executes the generated UPDATE query
|
||||
*
|
||||
* @param string $where WHERE condition
|
||||
*/
|
||||
public function update(string $where): bool
|
||||
{
|
||||
if (!$this->__table) {
|
||||
throw new Exception('No table name.');
|
||||
}
|
||||
|
||||
$updReq = $this->getUpdate($where);
|
||||
|
||||
$this->__con->execute($updReq);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,486 @@
|
|||
<?php
|
||||
/**
|
||||
* @class mysqliConnection
|
||||
* @brief MySQLi Database Driver
|
||||
*
|
||||
* See the {@link dbLayer} documentation for common methods.
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage DBLayer
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class mysqliConnection extends dbLayer implements i_dbLayer
|
||||
{
|
||||
/**
|
||||
* Enables weak locks if true
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $weak_locks = true;
|
||||
|
||||
/**
|
||||
* Driver name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $__driver = 'mysqli';
|
||||
|
||||
/**
|
||||
* SQL Syntax supported
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $__syntax = 'mysql';
|
||||
|
||||
/**
|
||||
* Open a DB connection
|
||||
*
|
||||
* @param string $host The host
|
||||
* @param string $user The user
|
||||
* @param string $password The password
|
||||
* @param string $database The database
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function db_connect(string $host, string $user, string $password, string $database)
|
||||
{
|
||||
if (!function_exists('mysqli_connect')) {
|
||||
throw new Exception('PHP MySQLi functions are not available');
|
||||
}
|
||||
|
||||
$port = abs((int) ini_get('mysqli.default_port'));
|
||||
$socket = '';
|
||||
if (strpos($host, ':') !== false) {
|
||||
// Port or socket given
|
||||
$bits = explode(':', $host);
|
||||
$host = array_shift($bits);
|
||||
$socket = array_shift($bits);
|
||||
if (abs((int) $socket) > 0) {
|
||||
// TCP/IP connection on given port
|
||||
$port = abs((int) $socket);
|
||||
$socket = '';
|
||||
} else {
|
||||
// Socket connection
|
||||
$port = 0;
|
||||
}
|
||||
}
|
||||
if (($link = @mysqli_connect($host, $user, $password, $database, $port, $socket)) === false) {
|
||||
throw new Exception('Unable to connect to database');
|
||||
}
|
||||
|
||||
$this->db_post_connect($link);
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a persistant DB connection
|
||||
*
|
||||
* @param string $host The host
|
||||
* @param string $user The user
|
||||
* @param string $password The password
|
||||
* @param string $database The database
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function db_pconnect(string $host, string $user, string $password, string $database)
|
||||
{
|
||||
// No pconnect wtih mysqli, below code is for comatibility
|
||||
return $this->db_connect($host, $user, $password, $database);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post connection helper
|
||||
*
|
||||
* @param mixed $handle The DB handle
|
||||
*/
|
||||
private function db_post_connect($handle)
|
||||
{
|
||||
if (version_compare($this->db_version($handle), '4.1', '>=')) {
|
||||
$this->db_query($handle, 'SET NAMES utf8');
|
||||
$this->db_query($handle, 'SET CHARACTER SET utf8');
|
||||
$this->db_query($handle, "SET COLLATION_CONNECTION = 'utf8_general_ci'");
|
||||
$this->db_query($handle, "SET COLLATION_SERVER = 'utf8_general_ci'");
|
||||
$this->db_query($handle, "SET CHARACTER_SET_SERVER = 'utf8'");
|
||||
if (version_compare($this->db_version($handle), '8.0', '<')) {
|
||||
// Setting CHARACTER_SET_DATABASE is obosolete for MySQL 8.0+
|
||||
$this->db_query($handle, "SET CHARACTER_SET_DATABASE = 'utf8'");
|
||||
}
|
||||
$handle->set_charset('utf8');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close DB connection
|
||||
*
|
||||
* @param mixed $handle The DB handle
|
||||
*/
|
||||
public function db_close($handle)
|
||||
{
|
||||
if ($handle instanceof MySQLi) {
|
||||
$handle->close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DB version
|
||||
*
|
||||
* @param mixed $handle The handle
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function db_version($handle): string
|
||||
{
|
||||
if ($handle instanceof MySQLi) {
|
||||
$v = $handle->server_version;
|
||||
|
||||
return sprintf('%s.%s.%s', ($v - ($v % 10000)) / 10000, ($v - ($v % 100)) % 10000 / 100, $v % 100);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a DB query
|
||||
*
|
||||
* @param mixed $handle The handle
|
||||
* @param string $query The query
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function db_query($handle, string $query)
|
||||
{
|
||||
if ($handle instanceof MySQLi) {
|
||||
$res = @$handle->query($query);
|
||||
if ($res === false) {
|
||||
throw new Exception($this->db_last_error($handle));
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* db_query() alias
|
||||
*
|
||||
* @param mixed $handle The handle
|
||||
* @param string $query The query
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function db_exec($handle, string $query)
|
||||
{
|
||||
return $this->db_query($handle, $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of fields in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function db_num_fields($res): int
|
||||
{
|
||||
return $res instanceof MySQLi_Result ? $res->field_count : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of rows in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function db_num_rows($res): int
|
||||
{
|
||||
return $res instanceof MySQLi_Result ? $res->num_rows : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field name in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
* @param int $position The position
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function db_field_name($res, int $position): string
|
||||
{
|
||||
if ($res instanceof MySQLi_Result) {
|
||||
$res->field_seek($position);
|
||||
$finfo = $res->fetch_field();
|
||||
|
||||
return $finfo->name; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field type in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
* @param int $position The position
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function db_field_type($res, int $position): string
|
||||
{
|
||||
if ($res instanceof MySQLi_Result) {
|
||||
$res->field_seek($position);
|
||||
$finfo = $res->fetch_field();
|
||||
|
||||
return $this->_convert_types($finfo->type); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch result data
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function db_fetch_assoc($res)
|
||||
{
|
||||
if ($res instanceof MySQLi_Result) {
|
||||
$v = $res->fetch_assoc();
|
||||
|
||||
return ($v === null) ? false : $v;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
* @param int $row The row
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function db_result_seek($res, int $row): bool
|
||||
{
|
||||
return $res instanceof MySQLi_Result ? $res->data_seek($row) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of affected rows in last INSERT, DELETE or UPDATE query
|
||||
*
|
||||
* @param mixed $handle The DB handle
|
||||
* @param mixed $res The resource
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function db_changes($handle, $res): int
|
||||
{
|
||||
return $handle instanceof MySQLi ? (int) $handle->affected_rows : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last query error, if any
|
||||
*
|
||||
* @param mixed $handle The handle
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function db_last_error($handle)
|
||||
{
|
||||
if ($handle instanceof MySQLi && ($e = $handle->error)) {
|
||||
return $e . ' (' . $handle->errno . ')';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a string (to be used in a SQL query)
|
||||
*
|
||||
* @param mixed $str The string
|
||||
* @param mixed $handle The DB handle
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function db_escape_string($str, $handle = null): string
|
||||
{
|
||||
return $handle instanceof MySQLi ? $handle->real_escape_string((string) $str) : addslashes($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks a table
|
||||
*
|
||||
* @param string $table The table
|
||||
*/
|
||||
public function db_write_lock(string $table): void
|
||||
{
|
||||
try {
|
||||
$this->execute('LOCK TABLES ' . $this->escapeSystem($table) . ' WRITE');
|
||||
} catch (Exception $e) {
|
||||
# As lock is a privilege in MySQL, we can avoid errors with weak_locks static var
|
||||
if (!self::$weak_locks) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock tables
|
||||
*/
|
||||
public function db_unlock(): void
|
||||
{
|
||||
try {
|
||||
$this->execute('UNLOCK TABLES');
|
||||
} catch (Exception $e) {
|
||||
if (!self::$weak_locks) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize a table
|
||||
*
|
||||
* @param string $table The table
|
||||
*/
|
||||
public function vacuum(string $table): void
|
||||
{
|
||||
$this->execute('OPTIMIZE TABLE ' . $this->escapeSystem($table));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a date to be used in SQL query
|
||||
*
|
||||
* @param string $field The field
|
||||
* @param string $pattern The pattern
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dateFormat(string $field, string $pattern): string
|
||||
{
|
||||
$pattern = str_replace('%M', '%i', $pattern);
|
||||
|
||||
return 'DATE_FORMAT(' . $field . ',' . "'" . $this->escape($pattern) . "') ";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ORDER BY fragment to be used in a SQL query
|
||||
*
|
||||
* @param mixed ...$args The arguments
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function orderBy(...$args): string
|
||||
{
|
||||
$default = [
|
||||
'order' => '',
|
||||
'collate' => false,
|
||||
];
|
||||
foreach ($args as $v) {
|
||||
if (is_string($v)) {
|
||||
$res[] = $v;
|
||||
} elseif (is_array($v) && !empty($v['field'])) {
|
||||
$v = array_merge($default, $v);
|
||||
$v['order'] = (strtoupper($v['order']) == 'DESC' ? 'DESC' : '');
|
||||
$res[] = $v['field'] . ($v['collate'] ? ' COLLATE utf8_unicode_ci' : '') . ' ' . $v['order'];
|
||||
}
|
||||
}
|
||||
|
||||
return empty($res) ? '' : ' ORDER BY ' . implode(',', $res) . ' ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fields concerned by lexical sort
|
||||
*
|
||||
* @param mixed ...$args The arguments
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function lexFields(...$args): string
|
||||
{
|
||||
$fmt = '%s COLLATE utf8_unicode_ci';
|
||||
foreach ($args as $v) {
|
||||
if (is_string($v)) {
|
||||
$res[] = sprintf($fmt, $v);
|
||||
} elseif (is_array($v)) {
|
||||
$res = array_map(fn ($i) => sprintf($fmt, $i), $v);
|
||||
}
|
||||
}
|
||||
|
||||
return empty($res) ? '' : implode(',', $res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a CONCAT fragment
|
||||
*
|
||||
* @param mixed ...$args The arguments
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function concat(...$args): string
|
||||
{
|
||||
return 'CONCAT(' . implode(',', $args) . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a string
|
||||
*
|
||||
* @param string $str The string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeSystem(string $str): string
|
||||
{
|
||||
return '`' . $str . '`';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type label
|
||||
*
|
||||
* @param string $id The identifier
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _convert_types(string $id)
|
||||
{
|
||||
$id2type = [
|
||||
'1' => 'int',
|
||||
'2' => 'int',
|
||||
'3' => 'int',
|
||||
'8' => 'int',
|
||||
'9' => 'int',
|
||||
|
||||
'16' => 'int', //BIT type recognized as unknown with mysql adapter
|
||||
|
||||
'4' => 'real',
|
||||
'5' => 'real',
|
||||
'246' => 'real',
|
||||
|
||||
'253' => 'string',
|
||||
'254' => 'string',
|
||||
|
||||
'10' => 'date',
|
||||
'11' => 'time',
|
||||
'12' => 'datetime',
|
||||
'13' => 'year',
|
||||
|
||||
'7' => 'timestamp',
|
||||
|
||||
'252' => 'blob',
|
||||
|
||||
];
|
||||
|
||||
return $id2type[$id] ?? 'unknown';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
<?php
|
||||
/**
|
||||
* @class mysqlimb4Connection
|
||||
* @brief MySQLi utf8-mb4 Database Driver
|
||||
*
|
||||
* See the {@link dbLayer} documentation for common methods.
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage DBLayer
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* Need to load parent class before extending it, may be use another technique in the future?
|
||||
*/
|
||||
require_once __DIR__ . '/class.mysqli.php';
|
||||
|
||||
class mysqlimb4Connection extends mysqliConnection
|
||||
{
|
||||
/**
|
||||
* Driver name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $__driver = 'mysqlimb4';
|
||||
|
||||
/**
|
||||
* Open a DB connection
|
||||
*
|
||||
* @param string $host The host
|
||||
* @param string $user The user
|
||||
* @param string $password The password
|
||||
* @param string $database The database
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function db_connect(string $host, string $user, string $password, string $database)
|
||||
{
|
||||
if (!function_exists('mysqli_connect')) {
|
||||
throw new Exception('PHP MySQLi functions are not available');
|
||||
}
|
||||
|
||||
$port = abs((int) ini_get('mysqli.default_port'));
|
||||
$socket = '';
|
||||
if (strpos($host, ':') !== false) {
|
||||
// Port or socket given
|
||||
$bits = explode(':', $host);
|
||||
$host = array_shift($bits);
|
||||
$socket = array_shift($bits);
|
||||
if (abs((int) $socket) > 0) {
|
||||
// TCP/IP connection on given port
|
||||
$port = abs((int) $socket);
|
||||
$socket = '';
|
||||
} else {
|
||||
// Socket connection
|
||||
$port = 0;
|
||||
}
|
||||
}
|
||||
if (($link = @mysqli_connect($host, $user, $password, $database, $port, $socket)) === false) {
|
||||
throw new Exception('Unable to connect to database');
|
||||
}
|
||||
|
||||
$this->db_post_connect($link);
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a persistant DB connection
|
||||
*
|
||||
* @param string $host The host
|
||||
* @param string $user The user
|
||||
* @param string $password The password
|
||||
* @param string $database The database
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function db_pconnect(string $host, string $user, string $password, string $database)
|
||||
{
|
||||
// No pconnect wtih mysqli, below code is for comatibility
|
||||
return $this->db_connect($host, $user, $password, $database);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post connection helper
|
||||
*
|
||||
* @param mixed $handle The DB handle
|
||||
*/
|
||||
private function db_post_connect($handle): void
|
||||
{
|
||||
if (version_compare($this->db_version($handle), '5.7.7', '>=')) {
|
||||
$this->db_query($handle, 'SET NAMES utf8mb4');
|
||||
$this->db_query($handle, 'SET CHARACTER SET utf8mb4');
|
||||
$this->db_query($handle, "SET COLLATION_CONNECTION = 'utf8mb4_unicode_ci'");
|
||||
$this->db_query($handle, "SET COLLATION_SERVER = 'utf8mb4_unicode_ci'");
|
||||
$this->db_query($handle, "SET CHARACTER_SET_SERVER = 'utf8mb4'");
|
||||
if (version_compare($this->db_version($handle), '8.0', '<')) {
|
||||
// Setting CHARACTER_SET_DATABASE is obosolete for MySQL 8.0+
|
||||
$this->db_query($handle, "SET CHARACTER_SET_DATABASE = 'utf8mb4'");
|
||||
}
|
||||
$handle->set_charset('utf8mb4');
|
||||
} else {
|
||||
throw new Exception('Unable to connect to an utf8mb4 database');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ORDER BY fragment to be used in a SQL query
|
||||
*
|
||||
* @param mixed ...$args The arguments
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function orderBy(...$args): string
|
||||
{
|
||||
$default = [
|
||||
'order' => '',
|
||||
'collate' => false,
|
||||
];
|
||||
foreach ($args as $v) {
|
||||
if (is_string($v)) {
|
||||
$res[] = $v;
|
||||
} elseif (is_array($v) && !empty($v['field'])) {
|
||||
$v = array_merge($default, $v);
|
||||
$v['order'] = (strtoupper($v['order']) == 'DESC' ? 'DESC' : '');
|
||||
$res[] = $v['field'] . ($v['collate'] ? ' COLLATE utf8mb4_unicode_ci' : '') . ' ' . $v['order'];
|
||||
}
|
||||
}
|
||||
|
||||
return empty($res) ? '' : ' ORDER BY ' . implode(',', $res) . ' ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fields concerned by lexical sort
|
||||
*
|
||||
* @param mixed ...$args The arguments
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function lexFields(...$args): string
|
||||
{
|
||||
$fmt = '%s COLLATE utf8mb4_unicode_ci';
|
||||
foreach ($args as $v) {
|
||||
if (is_string($v)) {
|
||||
$res[] = sprintf($fmt, $v);
|
||||
} elseif (is_array($v)) {
|
||||
$res = array_map(fn ($i) => sprintf($fmt, $i), $v);
|
||||
}
|
||||
}
|
||||
|
||||
return empty($res) ? '' : implode(',', $res);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,488 @@
|
|||
<?php
|
||||
/**
|
||||
* @class pgsqlConnection
|
||||
* @brief PostgreSQL Database Driver
|
||||
*
|
||||
* See the {@link dbLayer} documentation for common methods.
|
||||
*
|
||||
* This class adds a method for PostgreSQL only: {@link callFunction()}.
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage DBLayer
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class pgsqlConnection extends dbLayer implements i_dbLayer
|
||||
{
|
||||
protected $__driver = 'pgsql';
|
||||
protected $__syntax = 'postgresql';
|
||||
protected $utf8_unicode_ci = null;
|
||||
|
||||
/**
|
||||
* Gets the PostgreSQL connection string.
|
||||
*
|
||||
* @param string $host The host
|
||||
* @param string $user The user
|
||||
* @param string $password The password
|
||||
* @param string $database The database
|
||||
*
|
||||
* @return string The connection string.
|
||||
*/
|
||||
private function get_connection_string(string $host, string $user, string $password, string $database): string
|
||||
{
|
||||
$str = '';
|
||||
$port = false;
|
||||
|
||||
if ($host) {
|
||||
if (strpos($host, ':') !== false) {
|
||||
$bits = explode(':', $host);
|
||||
$host = array_shift($bits);
|
||||
$port = abs((int) array_shift($bits));
|
||||
}
|
||||
$str .= "host = '" . addslashes($host) . "' ";
|
||||
|
||||
if ($port) {
|
||||
$str .= 'port = ' . $port . ' ';
|
||||
}
|
||||
}
|
||||
if ($user) {
|
||||
$str .= "user = '" . addslashes($user) . "' ";
|
||||
}
|
||||
if ($password) {
|
||||
$str .= "password = '" . addslashes($password) . "' ";
|
||||
}
|
||||
if ($database) {
|
||||
$str .= "dbname = '" . addslashes($database) . "' ";
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a DB connection
|
||||
*
|
||||
* @param string $host The host
|
||||
* @param string $user The user
|
||||
* @param string $password The password
|
||||
* @param string $database The database
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function db_connect(string $host, string $user, string $password, string $database)
|
||||
{
|
||||
if (!function_exists('pg_connect')) {
|
||||
throw new Exception('PHP PostgreSQL functions are not available');
|
||||
}
|
||||
|
||||
$str = $this->get_connection_string($host, $user, $password, $database);
|
||||
|
||||
if (($link = @pg_connect($str)) === false) {
|
||||
throw new Exception('Unable to connect to database');
|
||||
}
|
||||
|
||||
$this->db_post_connect($link);
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a persistant DB connection
|
||||
*
|
||||
* @param string $host The host
|
||||
* @param string $user The user
|
||||
* @param string $password The password
|
||||
* @param string $database The database
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function db_pconnect(string $host, string $user, string $password, string $database)
|
||||
{
|
||||
if (!function_exists('pg_pconnect')) {
|
||||
throw new Exception('PHP PostgreSQL functions are not available');
|
||||
}
|
||||
|
||||
$str = $this->get_connection_string($host, $user, $password, $database);
|
||||
|
||||
if (($link = @pg_pconnect($str)) === false) {
|
||||
throw new Exception('Unable to connect to database');
|
||||
}
|
||||
|
||||
$this->db_post_connect($link);
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post connection helper
|
||||
*
|
||||
* @param mixed $handle The DB handle
|
||||
*/
|
||||
private function db_post_connect($handle): void
|
||||
{
|
||||
if (version_compare($this->db_version($handle), '9.1') >= 0) {
|
||||
// Only for PostgreSQL 9.1+
|
||||
$result = $this->db_query($handle, "SELECT * FROM pg_collation WHERE (collcollate LIKE '%.utf8')");
|
||||
if ($this->db_num_rows($result) > 0) {
|
||||
$this->db_result_seek($result, 0);
|
||||
$row = $this->db_fetch_assoc($result);
|
||||
$this->utf8_unicode_ci = '"' . $row['collname'] . '"';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close DB connection
|
||||
*
|
||||
* @param mixed $handle The DB handle
|
||||
*/
|
||||
public function db_close($handle): void
|
||||
{
|
||||
if (is_resource($handle) || (class_exists('PgSql\Connection') && $handle instanceof PgSql\Connection)) {
|
||||
pg_close($handle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DB version
|
||||
*
|
||||
* @param mixed $handle The handle
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function db_version($handle): string
|
||||
{
|
||||
if (is_resource($handle) || (class_exists('PgSql\Connection') && $handle instanceof PgSql\Connection)) {
|
||||
return pg_parameter_status($handle, 'server_version');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a DB query
|
||||
*
|
||||
* @param mixed $handle The handle
|
||||
* @param string $query The query
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function db_query($handle, string $query)
|
||||
{
|
||||
if (is_resource($handle) || (class_exists('PgSql\Connection') && $handle instanceof PgSql\Connection)) {
|
||||
$res = @pg_query($handle, $query);
|
||||
if ($res === false) {
|
||||
throw new Exception($this->db_last_error($handle));
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* db_query() alias
|
||||
*
|
||||
* @param mixed $handle The handle
|
||||
* @param string $query The query
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function db_exec($handle, string $query)
|
||||
{
|
||||
return $this->db_query($handle, $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of fields in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function db_num_fields($res): int
|
||||
{
|
||||
if (is_resource($res) || (class_exists('PgSql\Result') && $res instanceof PgSql\Result)) {
|
||||
return pg_num_fields($res);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of rows in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function db_num_rows($res): int
|
||||
{
|
||||
if (is_resource($res) || (class_exists('PgSql\Result') && $res instanceof PgSql\Result)) {
|
||||
return pg_num_rows($res);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field name in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
* @param int $position The position
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function db_field_name($res, int $position): string
|
||||
{
|
||||
if (is_resource($res) || (class_exists('PgSql\Result') && $res instanceof PgSql\Result)) {
|
||||
return pg_field_name($res, $position);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field type in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
* @param int $position The position
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function db_field_type($res, int $position): string
|
||||
{
|
||||
if (is_resource($res) || (class_exists('PgSql\Result') && $res instanceof PgSql\Result)) {
|
||||
return pg_field_type($res, $position);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch result data
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function db_fetch_assoc($res)
|
||||
{
|
||||
if (is_resource($res) || (class_exists('PgSql\Result') && $res instanceof PgSql\Result)) {
|
||||
return pg_fetch_assoc($res);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
* @param int $row The row
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function db_result_seek($res, int $row): bool
|
||||
{
|
||||
if (is_resource($res) || (class_exists('PgSql\Result') && $res instanceof PgSql\Result)) {
|
||||
return pg_result_seek($res, (int) $row);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of affected rows in last INSERT, DELETE or UPDATE query
|
||||
*
|
||||
* @param mixed $handle The DB handle
|
||||
* @param mixed $res The resource
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function db_changes($handle, $res): int
|
||||
{
|
||||
if (is_resource($res) || (class_exists('PgSql\Result') && $res instanceof PgSql\Result)) {
|
||||
return pg_affected_rows($res);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last query error, if any
|
||||
*
|
||||
* @param mixed $handle The handle
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function db_last_error($handle)
|
||||
{
|
||||
if (is_resource($handle) || (class_exists('PgSql\Connection') && $handle instanceof PgSql\Connection)) {
|
||||
return pg_last_error($handle);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a string (to be used in a SQL query)
|
||||
*
|
||||
* @param mixed $str The string
|
||||
* @param mixed $handle The DB handle
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function db_escape_string($str, $handle = null): string
|
||||
{
|
||||
if (is_resource($handle) || (class_exists('PgSql\Connection') && $handle instanceof PgSql\Connection)) {
|
||||
return pg_escape_string($handle, $str);
|
||||
}
|
||||
|
||||
return addslashes($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks a table
|
||||
*
|
||||
* @param string $table The table
|
||||
*/
|
||||
public function db_write_lock(string $table): void
|
||||
{
|
||||
$this->execute('BEGIN');
|
||||
$this->execute('LOCK TABLE ' . $this->escapeSystem($table) . ' IN EXCLUSIVE MODE');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock tables
|
||||
*/
|
||||
public function db_unlock(): void
|
||||
{
|
||||
$this->execute('END');
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize a table
|
||||
*
|
||||
* @param string $table The table
|
||||
*/
|
||||
public function vacuum(string $table): void
|
||||
{
|
||||
$this->execute('VACUUM FULL ' . $this->escapeSystem($table));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a date to be used in SQL query
|
||||
*
|
||||
* @param string $field The field
|
||||
* @param string $pattern The pattern
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dateFormat(string $field, string $pattern): string
|
||||
{
|
||||
$rep = [
|
||||
'%d' => 'DD',
|
||||
'%H' => 'HH24',
|
||||
'%M' => 'MI',
|
||||
'%m' => 'MM',
|
||||
'%S' => 'SS',
|
||||
'%Y' => 'YYYY',
|
||||
];
|
||||
|
||||
$pattern = str_replace(array_keys($rep), array_values($rep), $pattern);
|
||||
|
||||
return 'TO_CHAR(' . $field . ',' . "'" . $this->escape($pattern) . "') ";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ORDER BY fragment to be used in a SQL query
|
||||
*
|
||||
* @param mixed ...$args The arguments
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function orderBy(...$args): string
|
||||
{
|
||||
$default = [
|
||||
'order' => '',
|
||||
'collate' => false,
|
||||
];
|
||||
foreach ($args as $v) {
|
||||
if (is_string($v)) {
|
||||
$res[] = $v;
|
||||
} elseif (is_array($v) && !empty($v['field'])) {
|
||||
$v = array_merge($default, $v);
|
||||
$v['order'] = (strtoupper($v['order']) == 'DESC' ? 'DESC' : '');
|
||||
if ($v['collate']) {
|
||||
if ($this->utf8_unicode_ci) {
|
||||
$res[] = $v['field'] . ' COLLATE ' . $this->utf8_unicode_ci . ' ' . $v['order'];
|
||||
} else {
|
||||
$res[] = 'LOWER(' . $v['field'] . ') ' . $v['order'];
|
||||
}
|
||||
} else {
|
||||
$res[] = $v['field'] . ' ' . $v['order'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return empty($res) ? '' : ' ORDER BY ' . implode(',', $res) . ' ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fields concerned by lexical sort
|
||||
*
|
||||
* @param mixed ...$args The arguments
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function lexFields(...$args): string
|
||||
{
|
||||
$fmt = $this->utf8_unicode_ci ? '%s COLLATE ' . $this->utf8_unicode_ci : 'LOWER(%s)';
|
||||
foreach ($args as $v) {
|
||||
if (is_string($v)) {
|
||||
$res[] = sprintf($fmt, $v);
|
||||
} elseif (is_array($v)) {
|
||||
$res = array_map(fn ($i) => sprintf($fmt, $i), $v);
|
||||
}
|
||||
}
|
||||
|
||||
return empty($res) ? '' : implode(',', $res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function call
|
||||
*
|
||||
* Calls a PostgreSQL function an returns the result as a {@link record}.
|
||||
* After <var>$name</var>, you can add any parameters you want to append
|
||||
* them to the PostgreSQL function. You don't need to escape string in
|
||||
* arguments.
|
||||
*
|
||||
* @param string $name Function name
|
||||
*
|
||||
* @return record
|
||||
*/
|
||||
public function callFunction(string $name, ...$data): record
|
||||
{
|
||||
foreach ($data as $k => $v) {
|
||||
if (is_null($v)) {
|
||||
$data[$k] = 'NULL';
|
||||
} elseif (is_string($v)) {
|
||||
$data[$k] = "'" . $this->escape($v) . "'";
|
||||
} elseif (is_array($v)) {
|
||||
$data[$k] = $v[0];
|
||||
} else {
|
||||
$data[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
$req = 'SELECT ' . $name . "(\n" .
|
||||
implode(",\n", array_values($data)) .
|
||||
"\n) ";
|
||||
|
||||
return $this->select($req);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,459 @@
|
|||
<?php
|
||||
/**
|
||||
* @class sqliteConnection
|
||||
* @brief SQLite Database Driver
|
||||
*
|
||||
* See the {@link dbLayer} documentation for common methods.
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage DBLayer
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class sqliteConnection extends dbLayer implements i_dbLayer
|
||||
{
|
||||
protected $__driver = 'sqlite';
|
||||
protected $__syntax = 'sqlite';
|
||||
protected $utf8_unicode_ci = null;
|
||||
protected $vacuum = false;
|
||||
|
||||
/**
|
||||
* Open a DB connection
|
||||
*
|
||||
* @param string $host The host
|
||||
* @param string $user The user
|
||||
* @param string $password The password
|
||||
* @param string $database The database
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function db_connect(string $host, string $user, string $password, string $database)
|
||||
{
|
||||
if (!class_exists('PDO') || !in_array('sqlite', PDO::getAvailableDrivers())) {
|
||||
throw new Exception('PDO SQLite class is not available');
|
||||
}
|
||||
|
||||
$link = new PDO('sqlite:' . $database);
|
||||
$this->db_post_connect($link);
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a persistant DB connection
|
||||
*
|
||||
* @param string $host The host
|
||||
* @param string $user The user
|
||||
* @param string $password The password
|
||||
* @param string $database The database
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function db_pconnect(string $host, string $user, string $password, string $database)
|
||||
{
|
||||
if (!class_exists('PDO') || !in_array('sqlite', PDO::getAvailableDrivers())) {
|
||||
throw new Exception('PDO SQLite class is not available');
|
||||
}
|
||||
|
||||
$link = new PDO('sqlite:' . $database, null, null, [PDO::ATTR_PERSISTENT => true]);
|
||||
$this->db_post_connect($link);
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post connection helper
|
||||
*
|
||||
* @param mixed $handle The DB handle
|
||||
*/
|
||||
private function db_post_connect($handle): void
|
||||
{
|
||||
if ($handle instanceof PDO) {
|
||||
$this->db_exec($handle, 'PRAGMA short_column_names = 1');
|
||||
$this->db_exec($handle, 'PRAGMA encoding = "UTF-8"');
|
||||
$handle->sqliteCreateFunction('now', [$this, 'now'], 0);
|
||||
if (class_exists('Collator') && method_exists($handle, 'sqliteCreateCollation')) {
|
||||
$this->utf8_unicode_ci = new Collator('root');
|
||||
if (!$handle->sqliteCreateCollation('utf8_unicode_ci', [$this->utf8_unicode_ci, 'compare'])) {
|
||||
$this->utf8_unicode_ci = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close DB connection
|
||||
*
|
||||
* @param mixed $handle The DB handle
|
||||
*/
|
||||
public function db_close($handle): void
|
||||
{
|
||||
if ($handle instanceof PDO) {
|
||||
if ($this->vacuum) {
|
||||
$this->db_exec($handle, 'VACUUM');
|
||||
}
|
||||
$handle = null;
|
||||
$this->__link = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DB version
|
||||
*
|
||||
* @param mixed $handle The handle
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function db_version($handle): string
|
||||
{
|
||||
return $handle instanceof PDO ? $handle->getAttribute(PDO::ATTR_SERVER_VERSION) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query data in a staticRecord
|
||||
*
|
||||
* There is no other way than get all selected data in a staticRecord with SQlite
|
||||
*
|
||||
* @param string $sql The sql
|
||||
*
|
||||
* @return staticRecord The static record.
|
||||
*/
|
||||
public function select(string $sql): staticRecord
|
||||
{
|
||||
$result = $this->db_query($this->__link, $sql);
|
||||
$this->__last_result = &$result;
|
||||
|
||||
$info = [];
|
||||
$info['con'] = &$this;
|
||||
$info['cols'] = $this->db_num_fields($result);
|
||||
$info['info'] = [];
|
||||
|
||||
for ($i = 0; $i < $info['cols']; $i++) {
|
||||
$info['info']['name'][] = $this->db_field_name($result, $i);
|
||||
$info['info']['type'][] = $this->db_field_type($result, $i);
|
||||
}
|
||||
|
||||
$data = [];
|
||||
while ($r = $result->fetch(PDO::FETCH_ASSOC)) { // @phpstan-ignore-line
|
||||
$R = [];
|
||||
foreach ($r as $k => $v) {
|
||||
$k = preg_replace('/^(.*)\./', '', $k);
|
||||
$R[$k] = $v;
|
||||
$R[] = &$R[$k];
|
||||
}
|
||||
$data[] = $R;
|
||||
}
|
||||
|
||||
$info['rows'] = count($data);
|
||||
$result->closeCursor(); // @phpstan-ignore-line
|
||||
|
||||
return new staticRecord($data, $info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a DB query
|
||||
*
|
||||
* @param mixed $handle The handle
|
||||
* @param string $query The query
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function db_query($handle, string $query)
|
||||
{
|
||||
if ($handle instanceof PDO) {
|
||||
$res = $handle->query($query);
|
||||
if ($res === false) {
|
||||
throw new Exception($this->db_last_error($handle));
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* db_query() alias
|
||||
*
|
||||
* @param mixed $handle The handle
|
||||
* @param string $query The query
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function db_exec($handle, string $query)
|
||||
{
|
||||
return $this->db_query($handle, $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of fields in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function db_num_fields($res): int
|
||||
{
|
||||
return $res instanceof PDOStatement ? $res->columnCount() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of rows in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function db_num_rows($res): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field name in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
* @param int $position The position
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function db_field_name($res, int $position): string
|
||||
{
|
||||
if ($res instanceof PDOStatement) {
|
||||
$m = $res->getColumnMeta($position);
|
||||
|
||||
return preg_replace('/^.+\./', '', $m['name']); # we said short_column_names = 1
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field type in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
* @param int $position The position
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function db_field_type($res, int $position): string
|
||||
{
|
||||
if ($res instanceof PDOStatement) {
|
||||
$m = $res->getColumnMeta($position);
|
||||
switch ($m['pdo_type']) {
|
||||
case PDO::PARAM_BOOL:
|
||||
return 'boolean';
|
||||
case PDO::PARAM_NULL:
|
||||
return 'null';
|
||||
case PDO::PARAM_INT:
|
||||
return 'integer';
|
||||
default:
|
||||
return 'varchar';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch result data
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function db_fetch_assoc($res)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek in result
|
||||
*
|
||||
* @param mixed $res The resource
|
||||
* @param int $row The row
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function db_result_seek($res, $row): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of affected rows in last INSERT, DELETE or UPDATE query
|
||||
*
|
||||
* @param mixed $handle The DB handle
|
||||
* @param mixed $res The resource
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function db_changes($handle, $res): int
|
||||
{
|
||||
return $res instanceof PDOStatement ? $res->rowCount() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last query error, if any
|
||||
*
|
||||
* @param mixed $handle The handle
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function db_last_error($handle)
|
||||
{
|
||||
if ($handle instanceof PDO) {
|
||||
$err = $handle->errorInfo();
|
||||
|
||||
return $err[2] . ' (' . $err[1] . ')';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a string (to be used in a SQL query)
|
||||
*
|
||||
* @param mixed $str The string
|
||||
* @param mixed $handle The DB handle
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function db_escape_string($str, $handle = null): string
|
||||
{
|
||||
return $handle instanceof PDO ? trim($handle->quote($str), "'") : $str;
|
||||
}
|
||||
|
||||
public function escapeSystem(string $str): string
|
||||
{
|
||||
return "'" . $this->escape($str) . "'";
|
||||
}
|
||||
|
||||
public function begin(): void
|
||||
{
|
||||
if ($this->__link instanceof PDO) {
|
||||
$this->__link->beginTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public function commit(): void
|
||||
{
|
||||
if ($this->__link instanceof PDO) {
|
||||
$this->__link->commit();
|
||||
}
|
||||
}
|
||||
|
||||
public function rollback(): void
|
||||
{
|
||||
if ($this->__link instanceof PDO) {
|
||||
$this->__link->rollBack();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks a table
|
||||
*
|
||||
* @param string $table The table
|
||||
*/
|
||||
public function db_write_lock(string $table): void
|
||||
{
|
||||
$this->execute('BEGIN EXCLUSIVE TRANSACTION');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock tables
|
||||
*/
|
||||
public function db_unlock(): void
|
||||
{
|
||||
$this->execute('END');
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize a table
|
||||
*
|
||||
* @param string $table The table
|
||||
*/
|
||||
public function vacuum(string $table): void
|
||||
{
|
||||
$this->vacuum = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a date to be used in SQL query
|
||||
*
|
||||
* @param string $field The field
|
||||
* @param string $pattern The pattern
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dateFormat(string $field, string $pattern): string
|
||||
{
|
||||
return "strftime('" . $this->escape($pattern) . "'," . $field . ') ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ORDER BY fragment to be used in a SQL query
|
||||
*
|
||||
* @param mixed ...$args The arguments
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function orderBy(...$args): string
|
||||
{
|
||||
$default = [
|
||||
'order' => '',
|
||||
'collate' => false,
|
||||
];
|
||||
foreach ($args as $v) {
|
||||
if (is_string($v)) {
|
||||
$res[] = $v;
|
||||
} elseif (is_array($v) && !empty($v['field'])) {
|
||||
$v = array_merge($default, $v);
|
||||
$v['order'] = (strtoupper($v['order']) == 'DESC' ? 'DESC' : '');
|
||||
if ($v['collate']) {
|
||||
if ($this->utf8_unicode_ci instanceof Collator) {
|
||||
$res[] = $v['field'] . ' COLLATE utf8_unicode_ci ' . $v['order'];
|
||||
} else {
|
||||
$res[] = 'LOWER(' . $v['field'] . ') ' . $v['order'];
|
||||
}
|
||||
} else {
|
||||
$res[] = $v['field'] . ' ' . $v['order'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return empty($res) ? '' : ' ORDER BY ' . implode(',', $res) . ' ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fields concerned by lexical sort
|
||||
*
|
||||
* @param mixed ...$args The arguments
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function lexFields(...$args): string
|
||||
{
|
||||
$fmt = $this->utf8_unicode_ci instanceof Collator ? '%s COLLATE utf8_unicode_ci' : 'LOWER(%s)';
|
||||
foreach ($args as $v) {
|
||||
if (is_string($v)) {
|
||||
$res[] = sprintf($fmt, $v);
|
||||
} elseif (is_array($v)) {
|
||||
$res = array_map(fn ($i) => sprintf($fmt, $i), $v);
|
||||
}
|
||||
}
|
||||
|
||||
return empty($res) ? '' : implode(',', $res);
|
||||
}
|
||||
|
||||
# Internal SQLite function that adds NOW() SQL function.
|
||||
public function now()
|
||||
{
|
||||
return date('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,551 @@
|
|||
<?php
|
||||
/**
|
||||
* @interface i_dbSchema
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage DBSchema
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
interface i_dbSchema
|
||||
{
|
||||
/**
|
||||
* This method should return an array of all tables in database for the current connection.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function db_get_tables(): array;
|
||||
|
||||
/**
|
||||
* This method should return an associative array of columns in given table
|
||||
* <var>$table</var> with column names in keys. Each line value is an array
|
||||
* with following values:
|
||||
*
|
||||
* - [type] data type (string)
|
||||
* - [len] data length (integer or null)
|
||||
* - [null] is null? (boolean)
|
||||
* - [default] default value (string)
|
||||
*
|
||||
* @param string $table Table name
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function db_get_columns(string $table): array;
|
||||
|
||||
/**
|
||||
* This method should return an array of keys in given table
|
||||
* <var>$table</var>. Each line value is an array with following values:
|
||||
*
|
||||
* - [name] index name (string)
|
||||
* - [primary] primary key (boolean)
|
||||
* - [unique] unique key (boolean)
|
||||
* - [cols] columns (array)
|
||||
*
|
||||
* @param string $table Table name
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function db_get_keys(string $table): array;
|
||||
|
||||
/**
|
||||
* This method should return an array of indexes in given table
|
||||
* <var>$table</var>. Each line value is an array with following values:
|
||||
*
|
||||
* - [name] index name (string)
|
||||
* - [type] index type (string)
|
||||
* - [cols] columns (array)
|
||||
*
|
||||
* @param string $table Table name
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function db_get_indexes(string $table): array;
|
||||
|
||||
/**
|
||||
* This method should return an array of foreign keys in given table
|
||||
* <var>$table</var>. Each line value is an array with following values:
|
||||
*
|
||||
* - [name] key name (string)
|
||||
* - [c_cols] child columns (array)
|
||||
* - [p_table] parent table (string)
|
||||
* - [p_cols] parent columns (array)
|
||||
* - [update] on update statement (string)
|
||||
* - [delete] on delete statement (string)
|
||||
*
|
||||
* @param string $table Table name
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function db_get_references(string $table): array;
|
||||
|
||||
/**
|
||||
* Create table
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_table(string $name, array $fields): void;
|
||||
|
||||
/**
|
||||
* Create a field
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param int|null $len The length
|
||||
* @param bool $null The null
|
||||
* @param mixed $default The default
|
||||
*/
|
||||
public function db_create_field(string $table, string $name, string $type, ?int $len, bool $null, $default): void;
|
||||
|
||||
/**
|
||||
* Create primary index
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_primary(string $table, string $name, array $fields): void;
|
||||
|
||||
/**
|
||||
* Create unique field
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_unique(string $table, string $name, array $fields): void;
|
||||
|
||||
/**
|
||||
* Create index
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_index(string $table, string $name, string $type, array $fields): void;
|
||||
|
||||
/**
|
||||
* Create reference
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param string $table The table
|
||||
* @param array $fields The fields
|
||||
* @param string $foreign_table The foreign table
|
||||
* @param array $foreign_fields The foreign fields
|
||||
* @param bool|string $update The update
|
||||
* @param bool|string $delete The delete
|
||||
*/
|
||||
public function db_create_reference(string $name, string $table, array $fields, string $foreign_table, array $foreign_fields, $update, $delete): void;
|
||||
|
||||
/**
|
||||
* Modify field
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param int|null $len The length
|
||||
* @param bool $null The null
|
||||
* @param mixed $default The default
|
||||
*/
|
||||
public function db_alter_field(string $table, string $name, string $type, ?int $len, bool $null, $default): void;
|
||||
|
||||
/**
|
||||
* Modify primary index
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The new name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_alter_primary(string $table, string $name, string $newname, array $fields): void;
|
||||
|
||||
/**
|
||||
* Modify unique field
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The new name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_alter_unique(string $table, string $name, string $newname, array $fields): void;
|
||||
|
||||
/**
|
||||
* Modify index
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The new name
|
||||
* @param string $type The type
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_alter_index(string $table, string $name, string $newname, string $type, array $fields): void;
|
||||
|
||||
/**
|
||||
* Modify reference
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param string $newname The new name
|
||||
* @param string $table The table
|
||||
* @param array $fields The fields
|
||||
* @param string $foreign_table The foreign table
|
||||
* @param array $foreign_fields The foreign fields
|
||||
* @param bool|string $update The update
|
||||
* @param bool|string $delete The delete
|
||||
*/
|
||||
public function db_alter_reference(string $name, string $newname, string $table, array $fields, string $foreign_table, array $foreign_fields, $update, $delete): void;
|
||||
|
||||
/**
|
||||
* Drop unique
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
*/
|
||||
public function db_drop_unique(string $table, string $name): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class dbSchema
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage DBSchema
|
||||
*/
|
||||
class dbSchema
|
||||
{
|
||||
/**
|
||||
* @var mixed DB handle
|
||||
*/
|
||||
protected $con;
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $con The DB handle
|
||||
*/
|
||||
public function __construct($con)
|
||||
{
|
||||
$this->con = &$con;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the driver.
|
||||
*
|
||||
* @param mixed $con The DB handle
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function init($con)
|
||||
{
|
||||
$driver = $con->driver();
|
||||
$driver_class = $driver . 'Schema';
|
||||
|
||||
if (!class_exists($driver_class)) {
|
||||
if (file_exists(__DIR__ . '/class.' . $driver . '.dbschema.php')) {
|
||||
require __DIR__ . '/class.' . $driver . '.dbschema.php';
|
||||
} else {
|
||||
trigger_error('Unable to load DB schema layer for ' . $driver, E_USER_ERROR);
|
||||
exit(1); // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
|
||||
return new $driver_class($con);
|
||||
}
|
||||
|
||||
/**
|
||||
* Database data type to universal data type conversion.
|
||||
*
|
||||
* @param string $type Type name
|
||||
* @param int $len Field length (in/out)
|
||||
* @param mixed $default Default field value (in/out)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dbt2udt(string $type, ?int &$len, &$default): string
|
||||
{
|
||||
$map = [
|
||||
'bool' => 'boolean',
|
||||
'int2' => 'smallint',
|
||||
'int' => 'integer',
|
||||
'int4' => 'integer',
|
||||
'int8' => 'bigint',
|
||||
'float4' => 'real',
|
||||
'double precision' => 'float',
|
||||
'float8' => 'float',
|
||||
'decimal' => 'numeric',
|
||||
'character varying' => 'varchar',
|
||||
'character' => 'char',
|
||||
];
|
||||
|
||||
return $map[$type] ?? $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal data type to database data tye conversion.
|
||||
*
|
||||
* @param string $type Type name
|
||||
* @param integer $len Field length (in/out)
|
||||
* @param string $default Default field value (in/out)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function udt2dbt(string $type, ?int &$len, &$default): string
|
||||
{
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all table names.
|
||||
*
|
||||
* @see i_dbSchema::db_get_tables
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getTables(): array
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
return $this->db_get_tables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of columns (name and type) of a given table.
|
||||
*
|
||||
* @see i_dbSchema::db_get_columns
|
||||
*
|
||||
* @param string $table Table name
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getColumns(string $table): array
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
return $this->db_get_columns($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of index of a given table.
|
||||
*
|
||||
* @see i_dbSchema::db_get_keys
|
||||
*
|
||||
* @param string $table Table name
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getKeys(string $table): array
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
return $this->db_get_keys($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of indexes of a given table.
|
||||
*
|
||||
* @see i_dbSchema::db_get_index
|
||||
*
|
||||
* @param string $table Table name
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getIndexes(string $table): array
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
return $this->db_get_indexes($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of foreign keys of a given table.
|
||||
*
|
||||
* @see i_dbSchema::db_get_references
|
||||
*
|
||||
* @param string $table Table name
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getReferences(string $table): array
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
return $this->db_get_references($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table.
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function createTable(string $name, array $fields): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->db_create_table($name, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a field.
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param int|null $len The length
|
||||
* @param bool $null The null
|
||||
* @param mixed $default The default value
|
||||
*/
|
||||
public function createField(string $table, string $name, string $type, ?int $len, bool $null, $default): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->db_create_field($table, $name, $type, $len, $null, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a primary key.
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function createPrimary(string $table, string $name, array $fields): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->db_create_primary($table, $name, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unique key.
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function createUnique(string $table, string $name, array $fields): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->db_create_unique($table, $name, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an index.
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function createIndex(string $table, string $name, string $type, array $fields): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->db_create_index($table, $name, $type, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reference.
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param string $table The table
|
||||
* @param array $fields The fields
|
||||
* @param string $foreign_table The foreign table
|
||||
* @param array $foreign_fields The foreign fields
|
||||
* @param string|bool $update The update
|
||||
* @param string|bool $delete The delete
|
||||
*/
|
||||
public function createReference(string $name, string $table, array $fields, string $foreign_table, array $foreign_fields, $update, $delete): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->db_create_reference($name, $table, $fields, $foreign_table, $foreign_fields, $update, $delete);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a field
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param int|null $len The length
|
||||
* @param bool $null The null
|
||||
* @param mixed $default The default value
|
||||
*/
|
||||
public function alterField(string $table, string $name, string $type, ?int $len, bool $null, $default): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->db_alter_field($table, $name, $type, $len, $null, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a primary key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function alterPrimary(string $table, string $name, string $newname, array $fields): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->db_alter_primary($table, $name, $newname, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a unique key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function alterUnique(string $table, string $name, string $newname, array $fields): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->db_alter_unique($table, $name, $newname, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify an index
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param string $type The type
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function alterIndex(string $table, string $name, string $newname, string $type, array $fields): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->db_alter_index($table, $name, $newname, $type, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a reference (foreign key)
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param string $table The table
|
||||
* @param array $fields The fields
|
||||
* @param string $foreign_table The foreign table
|
||||
* @param array $foreign_fields The foreign fields
|
||||
* @param string|bool $update The update
|
||||
* @param string|bool $delete The delete
|
||||
*/
|
||||
public function alterReference(string $name, string $newname, string $table, array $fields, string $foreign_table, array $foreign_fields, $update, $delete): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->db_alter_reference($name, $newname, $table, $fields, $foreign_table, $foreign_fields, $update, $delete);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a unique key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
*/
|
||||
public function dropUnique(string $table, string $name): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->db_drop_unique($table, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush stack
|
||||
*/
|
||||
public function flushStack()
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,829 @@
|
|||
<?php
|
||||
/**
|
||||
* @class dbStruct
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage DBSchema
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class dbStruct
|
||||
{
|
||||
/**
|
||||
* @var mixed instance
|
||||
*/
|
||||
protected $con;
|
||||
|
||||
/**
|
||||
* @var string DB table prefix
|
||||
*/
|
||||
protected $prefix;
|
||||
|
||||
/**
|
||||
* Stack of DB tables
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $tables = [];
|
||||
|
||||
/**
|
||||
* Stack of References (foreign keys)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $references = [];
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $con The DB handle
|
||||
* @param string $prefix The DB table prefix
|
||||
*/
|
||||
public function __construct($con, string $prefix = '')
|
||||
{
|
||||
$this->con = &$con;
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function driver(): string
|
||||
{
|
||||
return $this->con->driver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new table
|
||||
*
|
||||
* @param string $name The name
|
||||
*
|
||||
* @return dbStructTable The database structure table.
|
||||
*/
|
||||
public function table(string $name): dbStructTable
|
||||
{
|
||||
$this->tables[$name] = new dbStructTable($name);
|
||||
|
||||
return $this->tables[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the specified table (create it if necessary).
|
||||
*
|
||||
* @param string $name The table name
|
||||
*
|
||||
* @return dbStructTable The database structure table.
|
||||
*/
|
||||
public function __get(string $name): dbStructTable
|
||||
{
|
||||
if (!isset($this->tables[$name])) {
|
||||
return $this->table($name);
|
||||
}
|
||||
|
||||
return $this->tables[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate dbSchema instance from database structure
|
||||
*/
|
||||
public function reverse(): void
|
||||
{
|
||||
$schema = dbSchema::init($this->con);
|
||||
|
||||
# Get tables
|
||||
$tables = $schema->getTables();
|
||||
|
||||
foreach ($tables as $table_name) {
|
||||
if ($this->prefix && strpos($table_name, $this->prefix) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$table = $this->table($table_name);
|
||||
|
||||
# Get fields
|
||||
$fields = $schema->getColumns($table_name);
|
||||
|
||||
foreach ($fields as $field_name => $field) {
|
||||
$type = $schema->dbt2udt($field['type'], $field['len'], $field['default']);
|
||||
$table->field($field_name, $type, $field['len'], $field['null'], $field['default'], true);
|
||||
}
|
||||
|
||||
# Get keys
|
||||
$keys = $schema->getKeys($table_name);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$fields = $key['cols'];
|
||||
|
||||
if ($key['primary']) {
|
||||
$table->primary($key['name'], ...$fields);
|
||||
} elseif ($key['unique']) {
|
||||
$table->unique($key['name'], ...$fields);
|
||||
}
|
||||
}
|
||||
|
||||
# Get indexes
|
||||
$indexes = $schema->getIndexes($table_name);
|
||||
foreach ($indexes as $index) {
|
||||
$table->index($index['name'], $index['type'], ...$index['cols']);
|
||||
}
|
||||
|
||||
# Get foreign keys
|
||||
$references = $schema->getReferences($table_name);
|
||||
foreach ($references as $reference) {
|
||||
$table->reference($reference['name'], $reference['c_cols'], $reference['p_table'], $reference['p_cols'], $reference['update'], $reference['delete']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize this schema taken from database with $schema.
|
||||
*
|
||||
* @param dbStruct $s Structure to synchronize with
|
||||
*/
|
||||
public function synchronize(dbStruct $s)
|
||||
{
|
||||
$this->tables = [];
|
||||
$this->reverse();
|
||||
|
||||
if (!($s instanceof self)) {
|
||||
throw new Exception('Invalid database schema');
|
||||
}
|
||||
|
||||
$tables = $s->getTables();
|
||||
|
||||
$table_create = [];
|
||||
$key_create = [];
|
||||
$index_create = [];
|
||||
$reference_create = [];
|
||||
|
||||
$field_create = [];
|
||||
$field_update = [];
|
||||
$key_update = [];
|
||||
$index_update = [];
|
||||
$reference_update = [];
|
||||
|
||||
$got_work = false;
|
||||
|
||||
$schema = dbSchema::init($this->con);
|
||||
|
||||
foreach ($tables as $tname => $t) {
|
||||
if (!$this->tableExists($tname)) {
|
||||
# Table does not exist, create table
|
||||
$table_create[$tname] = $t->getFields();
|
||||
|
||||
# Add keys, indexes and references
|
||||
$keys = $t->getKeys();
|
||||
$indexes = $t->getIndexes();
|
||||
$references = $t->getReferences();
|
||||
|
||||
foreach ($keys as $k => $v) {
|
||||
$key_create[$tname][$this->prefix . $k] = $v;
|
||||
}
|
||||
foreach ($indexes as $k => $v) {
|
||||
$index_create[$tname][$this->prefix . $k] = $v;
|
||||
}
|
||||
foreach ($references as $k => $v) {
|
||||
$v['p_table'] = $this->prefix . $v['p_table'];
|
||||
$reference_create[$tname][$this->prefix . $k] = $v;
|
||||
}
|
||||
|
||||
$got_work = true;
|
||||
} else { # Table exists
|
||||
# Check new fields to create
|
||||
$fields = $t->getFields();
|
||||
/* @phpstan-ignore-next-line */
|
||||
$db_fields = $this->tables[$tname]->getFields();
|
||||
foreach ($fields as $fname => $f) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
if (!$this->tables[$tname]->fieldExists($fname)) {
|
||||
# Field doest not exist, create it
|
||||
$field_create[$tname][$fname] = $f;
|
||||
$got_work = true;
|
||||
} elseif ($this->fieldsDiffer($db_fields[$fname], $f)) {
|
||||
# Field exists and differs from db version
|
||||
$field_update[$tname][$fname] = $f;
|
||||
$got_work = true;
|
||||
}
|
||||
}
|
||||
|
||||
# Check keys to add or upgrade
|
||||
$keys = $t->getKeys();
|
||||
/* @phpstan-ignore-next-line */
|
||||
$db_keys = $this->tables[$tname]->getKeys();
|
||||
|
||||
foreach ($keys as $kname => $k) {
|
||||
if ($k['type'] == 'primary' && $this->con->syntax() == 'mysql') {
|
||||
$kname = 'PRIMARY';
|
||||
} else {
|
||||
$kname = $this->prefix . $kname;
|
||||
}
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
$db_kname = $this->tables[$tname]->keyExists($kname, $k['type'], $k['cols']);
|
||||
if (!$db_kname) {
|
||||
# Key does not exist, create it
|
||||
$key_create[$tname][$kname] = $k;
|
||||
$got_work = true;
|
||||
} elseif ($this->keysDiffer($db_kname, $db_keys[$db_kname]['cols'], $kname, $k['cols'])) {
|
||||
# Key exists and differs from db version
|
||||
$key_update[$tname][$db_kname] = array_merge(['name' => $kname], $k);
|
||||
$got_work = true;
|
||||
}
|
||||
}
|
||||
|
||||
# Check index to add or upgrade
|
||||
$idx = $t->getIndexes();
|
||||
/* @phpstan-ignore-next-line */
|
||||
$db_idx = $this->tables[$tname]->getIndexes();
|
||||
|
||||
foreach ($idx as $iname => $i) {
|
||||
$iname = $this->prefix . $iname;
|
||||
/* @phpstan-ignore-next-line */
|
||||
$db_iname = $this->tables[$tname]->indexExists($iname, $i['type'], $i['cols']);
|
||||
|
||||
if (!$db_iname) {
|
||||
# Index does not exist, create it
|
||||
$index_create[$tname][$iname] = $i;
|
||||
$got_work = true;
|
||||
} elseif ($this->indexesDiffer($db_iname, $db_idx[$db_iname], $iname, $i)) {
|
||||
# Index exists and differs from db version
|
||||
$index_update[$tname][$db_iname] = array_merge(['name' => $iname], $i);
|
||||
$got_work = true;
|
||||
}
|
||||
}
|
||||
|
||||
# Check references to add or upgrade
|
||||
$ref = $t->getReferences();
|
||||
/* @phpstan-ignore-next-line */
|
||||
$db_ref = $this->tables[$tname]->getReferences();
|
||||
|
||||
foreach ($ref as $rname => $r) {
|
||||
$rname = $this->prefix . $rname;
|
||||
$r['p_table'] = $this->prefix . $r['p_table'];
|
||||
/* @phpstan-ignore-next-line */
|
||||
$db_rname = $this->tables[$tname]->referenceExists($rname, $r['c_cols'], $r['p_table'], $r['p_cols']);
|
||||
|
||||
if (!$db_rname) {
|
||||
# Reference does not exist, create it
|
||||
$reference_create[$tname][$rname] = $r;
|
||||
$got_work = true;
|
||||
} elseif ($this->referencesDiffer($db_rname, $db_ref[$db_rname], $rname, $r)) {
|
||||
$reference_update[$tname][$db_rname] = array_merge(['name' => $rname], $r);
|
||||
$got_work = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$got_work) {
|
||||
return;
|
||||
}
|
||||
|
||||
# Create tables
|
||||
foreach ($table_create as $table => $fields) {
|
||||
$schema->createTable($table, $fields);
|
||||
}
|
||||
|
||||
# Create new fields
|
||||
foreach ($field_create as $tname => $fields) {
|
||||
foreach ($fields as $fname => $f) {
|
||||
$schema->createField($tname, $fname, $f['type'], $f['len'], $f['null'], $f['default']);
|
||||
}
|
||||
}
|
||||
|
||||
# Update fields
|
||||
foreach ($field_update as $tname => $fields) {
|
||||
foreach ($fields as $fname => $f) {
|
||||
$schema->alterField($tname, $fname, $f['type'], $f['len'], $f['null'], $f['default']);
|
||||
}
|
||||
}
|
||||
|
||||
# Create new keys
|
||||
foreach ($key_create as $tname => $keys) {
|
||||
foreach ($keys as $kname => $k) {
|
||||
if ($k['type'] == 'primary') {
|
||||
$schema->createPrimary($tname, $kname, $k['cols']);
|
||||
} elseif ($k['type'] == 'unique') {
|
||||
$schema->createUnique($tname, $kname, $k['cols']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Update keys
|
||||
foreach ($key_update as $tname => $keys) {
|
||||
foreach ($keys as $kname => $k) {
|
||||
if ($k['type'] == 'primary') {
|
||||
$schema->alterPrimary($tname, $kname, $k['name'], $k['cols']);
|
||||
} elseif ($k['type'] == 'unique') {
|
||||
$schema->alterUnique($tname, $kname, $k['name'], $k['cols']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Create indexes
|
||||
foreach ($index_create as $tname => $index) {
|
||||
foreach ($index as $iname => $i) {
|
||||
$schema->createIndex($tname, $iname, $i['type'], $i['cols']);
|
||||
}
|
||||
}
|
||||
|
||||
# Update indexes
|
||||
foreach ($index_update as $tname => $index) {
|
||||
foreach ($index as $iname => $i) {
|
||||
$schema->alterIndex($tname, $iname, $i['name'], $i['type'], $i['cols']);
|
||||
}
|
||||
}
|
||||
|
||||
# Create references
|
||||
foreach ($reference_create as $tname => $ref) {
|
||||
foreach ($ref as $rname => $r) {
|
||||
$schema->createReference($rname, $tname, $r['c_cols'], $r['p_table'], $r['p_cols'], $r['update'], $r['delete']);
|
||||
}
|
||||
}
|
||||
|
||||
# Update references
|
||||
foreach ($reference_update as $tname => $ref) {
|
||||
foreach ($ref as $rname => $r) {
|
||||
$schema->alterReference($rname, $r['name'], $tname, $r['c_cols'], $r['p_table'], $r['p_cols'], $r['update'], $r['delete']);
|
||||
}
|
||||
}
|
||||
|
||||
# Flush execution stack
|
||||
$schema->flushStack();
|
||||
|
||||
return
|
||||
count($table_create) + count($key_create) + count($index_create) + count($reference_create) + count($field_create) + count($field_update) + count($key_update) + count($index_update) + count($reference_update);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tables.
|
||||
*
|
||||
* @return array The tables.
|
||||
*/
|
||||
public function getTables(): array
|
||||
{
|
||||
$tables = [];
|
||||
foreach ($this->tables as $table => $properties) {
|
||||
$tables[$this->prefix . $table] = $properties;
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if table exists.
|
||||
*
|
||||
* @param string $name The name
|
||||
*
|
||||
* @return bool True if table exists, False otherwise.
|
||||
*/
|
||||
public function tableExists(string $name): bool
|
||||
{
|
||||
return isset($this->tables[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two fields are the same
|
||||
*
|
||||
* @param array $dst_field The destination field
|
||||
* @param array $src_field The source field
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function fieldsDiffer(array $dst_field, array $src_field): bool
|
||||
{
|
||||
$d_type = $dst_field['type'];
|
||||
$d_len = (int) $dst_field['len'];
|
||||
$d_default = $dst_field['default'];
|
||||
$d_null = $dst_field['null'];
|
||||
|
||||
$s_type = $src_field['type'];
|
||||
$s_len = (int) $src_field['len'];
|
||||
$s_default = $src_field['default'];
|
||||
$s_null = $src_field['null'];
|
||||
|
||||
return $d_type != $s_type || $d_len != $s_len || $d_default != $s_default || $d_null != $s_null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two keys are the same
|
||||
*
|
||||
* @param string $dst_name The destination name
|
||||
* @param array $dst_fields The destination fields
|
||||
* @param string $src_name The source name
|
||||
* @param array $src_fields The source fields
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function keysDiffer(string $dst_name, array $dst_fields, string $src_name, array $src_fields): bool
|
||||
{
|
||||
return $dst_name != $src_name || $dst_fields != $src_fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two indexes are the same
|
||||
*
|
||||
* @param string $dst_name The destination name
|
||||
* @param array $dst_idx The destination index
|
||||
* @param string $src_name The source name
|
||||
* @param array $src_idc The source idc
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function indexesDiffer(string $dst_name, array $dst_idx, string $src_name, array $src_idc): bool
|
||||
{
|
||||
return $dst_name != $src_name || $dst_idx['cols'] != $src_idc['cols'] || $dst_idx['type'] != $src_idc['type'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two references are the same
|
||||
*
|
||||
* @param string $dst_name The destination name
|
||||
* @param array $dst_ref The destination reference
|
||||
* @param string $src_name The source name
|
||||
* @param array $src_ref The source reference
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function referencesDiffer(string $dst_name, array $dst_ref, string $src_name, array $src_ref): bool
|
||||
{
|
||||
return $dst_name != $src_name || $dst_ref['c_cols'] != $src_ref['c_cols'] || $dst_ref['p_table'] != $src_ref['p_table'] || $dst_ref['p_cols'] != $src_ref['p_cols'] || $dst_ref['update'] != $src_ref['update'] || $dst_ref['delete'] != $src_ref['delete'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class dbStructTable
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage DBSchema
|
||||
*/
|
||||
class dbStructTable
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $has_primary = false;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $fields = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $keys = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $indexes = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $references = [];
|
||||
|
||||
/**
|
||||
Universal data types supported by dbSchema
|
||||
|
||||
SMALLINT : signed 2 bytes integer
|
||||
INTEGER : signed 4 bytes integer
|
||||
BIGINT : signed 8 bytes integer
|
||||
REAL : signed 4 bytes floating point number
|
||||
FLOAT : signed 8 bytes floating point number
|
||||
NUMERIC : exact numeric type
|
||||
|
||||
DATE : Calendar date (day, month and year)
|
||||
TIME : Time of day
|
||||
TIMESTAMP : Date and time
|
||||
|
||||
CHAR : A fixed n-length character string
|
||||
VARCHAR : A variable length character string
|
||||
TEXT : A variable length of text
|
||||
*/
|
||||
protected $allowed_types = [
|
||||
'smallint', 'integer', 'bigint', 'real', 'float', 'numeric',
|
||||
'date', 'time', 'timestamp',
|
||||
'char', 'varchar', 'text',
|
||||
];
|
||||
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fields.
|
||||
*
|
||||
* @return array The fields.
|
||||
*/
|
||||
public function getFields(): array
|
||||
{
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the keys.
|
||||
*
|
||||
* @return array The keys.
|
||||
*/
|
||||
public function getKeys(): array
|
||||
{
|
||||
return $this->keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the indexes.
|
||||
*
|
||||
* @return array The indexes.
|
||||
*/
|
||||
public function getIndexes(): array
|
||||
{
|
||||
return $this->indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the references.
|
||||
*
|
||||
* @return array The references.
|
||||
*/
|
||||
public function getReferences(): array
|
||||
{
|
||||
return $this->references;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if field exists.
|
||||
*
|
||||
* @param string $name The name
|
||||
*
|
||||
* @return bool True if field exists, False otherwise.
|
||||
*/
|
||||
public function fieldExists(string $name): bool
|
||||
{
|
||||
return isset($this->fields[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if key exists.
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param array $fields The fields
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function keyExists(string $name, string $type, array $fields)
|
||||
{
|
||||
# Look for key with the same name
|
||||
if (isset($this->keys[$name])) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
# Look for key with the same columns list and type
|
||||
foreach ($this->keys as $key_name => $key) {
|
||||
if ($key['cols'] == $fields && $key['type'] == $type) {
|
||||
# Same columns and type, return new name
|
||||
return $key_name;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if index exists.
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param array $fields The fields
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function indexExists(string $name, string $type, array $fields)
|
||||
{
|
||||
# Look for key with the same name
|
||||
if (isset($this->indexes[$name])) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
# Look for index with the same columns list and type
|
||||
foreach ($this->indexes as $index_name => $index) {
|
||||
if ($index['cols'] == $fields && $index['type'] == $type) {
|
||||
# Same columns and type, return new name
|
||||
return $index_name;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if reference exists.
|
||||
*
|
||||
* @param string $name The reference name
|
||||
* @param array $local_fields The local fields
|
||||
* @param string $foreign_table The foreign table
|
||||
* @param array $foreign_fields The foreign fields
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function referenceExists(string $name, array $local_fields, string $foreign_table, array $foreign_fields)
|
||||
{
|
||||
if (isset($this->references[$name])) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
# Look for reference with same chil columns, parent table and columns
|
||||
foreach ($this->references as $reference_name => $reference) {
|
||||
if ($local_fields == $reference['c_cols'] && $foreign_table == $reference['p_table'] && $foreign_fields == $reference['p_cols']) {
|
||||
# Only name differs, return new name
|
||||
return $reference_name;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a table field
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param int|null $len The length
|
||||
* @param bool $null Null value allowed
|
||||
* @param mixed $default The default value
|
||||
* @param bool $to_null Set type to null if type unknown
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return dbStructTable|self
|
||||
*/
|
||||
public function field(string $name, string $type, ?int $len, bool $null = true, $default = false, bool $to_null = false)
|
||||
{
|
||||
$type = strtolower($type);
|
||||
|
||||
if (!in_array($type, $this->allowed_types)) {
|
||||
if ($to_null) {
|
||||
$type = null;
|
||||
} else {
|
||||
throw new Exception('Invalid data type ' . $type . ' in schema');
|
||||
}
|
||||
}
|
||||
|
||||
$this->fields[$name] = [
|
||||
'type' => $type,
|
||||
'len' => (int) $len,
|
||||
'default' => $default,
|
||||
'null' => (bool) $null,
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set field
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param mixed $properties The arguments
|
||||
*
|
||||
* @return dbStructTable|self
|
||||
*/
|
||||
public function __call(string $name, $properties): dbStructTable
|
||||
{
|
||||
return $this->field($name, ...$properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a primary index
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param mixed ...$fields The cols
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return dbStructTable|self
|
||||
*/
|
||||
public function primary(string $name, ...$fields): dbStructTable
|
||||
{
|
||||
if ($this->has_primary) {
|
||||
throw new Exception(sprintf('Table %s already has a primary key', $this->name));
|
||||
}
|
||||
|
||||
return $this->newKey('primary', $name, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an unique index
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param mixed ...$fields The fields
|
||||
*
|
||||
* @return dbStructTable|self
|
||||
*/
|
||||
public function unique(string $name, ...$fields): dbStructTable
|
||||
{
|
||||
return $this->newKey('unique', $name, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an index
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param mixed ...$fields The fields
|
||||
*
|
||||
* @return dbStructTable|self
|
||||
*/
|
||||
public function index(string $name, string $type, ...$fields): dbStructTable
|
||||
{
|
||||
$this->checkCols($fields);
|
||||
|
||||
$this->indexes[$name] = [
|
||||
'type' => strtolower($type),
|
||||
'cols' => $fields,
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a reference
|
||||
*
|
||||
* @param string $name The reference name
|
||||
* @param array|string $local_fields The local fields
|
||||
* @param string $foreign_table The foreign table
|
||||
* @param array|string $foreign_fields The foreign fields
|
||||
* @param bool|string $update The update
|
||||
* @param bool|string $delete The delete
|
||||
*/
|
||||
public function reference(string $name, $local_fields, string $foreign_table, $foreign_fields, $update = false, $delete = false): void
|
||||
{
|
||||
if (!is_array($foreign_fields)) {
|
||||
$foreign_fields = [$foreign_fields];
|
||||
}
|
||||
if (!is_array($local_fields)) {
|
||||
$local_fields = [$local_fields];
|
||||
}
|
||||
|
||||
$this->checkCols($local_fields);
|
||||
|
||||
$this->references[$name] = [
|
||||
'c_cols' => $local_fields,
|
||||
'p_table' => $foreign_table,
|
||||
'p_cols' => $foreign_fields,
|
||||
'update' => $update,
|
||||
'delete' => $delete,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new key (index)
|
||||
*
|
||||
* @param string $type The type
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*
|
||||
* @return dbStructTable|self
|
||||
*/
|
||||
protected function newKey(string $type, string $name, array $fields): dbStructTable
|
||||
{
|
||||
$this->checkCols($fields);
|
||||
|
||||
$this->keys[$name] = [
|
||||
'type' => $type,
|
||||
'cols' => $fields,
|
||||
];
|
||||
|
||||
if ($type == 'primary') {
|
||||
$this->has_primary = true;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ccheck if field(s) exists
|
||||
*
|
||||
* @param array $fields The fields
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function checkCols(array $fields): void
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (!preg_match('/^\(.*?\)$/', $field) && !isset($this->fields[$field])) {
|
||||
throw new Exception(sprintf('Field %s does not exist in table %s', $field, $this->name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,572 @@
|
|||
<?php
|
||||
/**
|
||||
* @class mysqlSchema
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage DBSchema
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class mysqliSchema extends dbSchema implements i_dbSchema
|
||||
{
|
||||
/**
|
||||
* Translate DB type to universal type
|
||||
*
|
||||
* @param string $type The type
|
||||
* @param int|null $len The length
|
||||
* @param mixed $default The default valule
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dbt2udt(string $type, ?int &$len, &$default): string
|
||||
{
|
||||
$type = parent::dbt2udt($type, $len, $default);
|
||||
|
||||
switch ($type) {
|
||||
case 'float':
|
||||
return 'real';
|
||||
case 'double':
|
||||
return 'float';
|
||||
case 'datetime':
|
||||
# DATETIME real type is TIMESTAMP
|
||||
if ($default == "'1970-01-01 00:00:00'") {
|
||||
# Bad hack
|
||||
$default = 'now()';
|
||||
}
|
||||
|
||||
return 'timestamp';
|
||||
case 'integer':
|
||||
case 'mediumint':
|
||||
if ($len == 11) {
|
||||
$len = 0;
|
||||
}
|
||||
|
||||
return 'integer';
|
||||
case 'bigint':
|
||||
if ($len == 20) {
|
||||
$len = 0;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'tinyint':
|
||||
case 'smallint':
|
||||
if ($len == 6) {
|
||||
$len = 0;
|
||||
}
|
||||
|
||||
return 'smallint';
|
||||
case 'numeric':
|
||||
$len = 0;
|
||||
|
||||
break;
|
||||
case 'tinytext':
|
||||
case 'longtext':
|
||||
return 'text';
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate universal type to DB type
|
||||
*
|
||||
* @param string $type The type
|
||||
* @param int|null $len The length
|
||||
* @param mixed $default The default value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function udt2dbt(string $type, ?int &$len, &$default): string
|
||||
{
|
||||
$type = parent::udt2dbt($type, $len, $default);
|
||||
|
||||
switch ($type) {
|
||||
case 'real':
|
||||
return 'float';
|
||||
case 'float':
|
||||
return 'double';
|
||||
case 'timestamp':
|
||||
if ($default == 'now()') {
|
||||
# MySQL does not support now() default value...
|
||||
$default = "'1970-01-01 00:00:00'";
|
||||
}
|
||||
|
||||
return 'datetime';
|
||||
case 'text':
|
||||
$len = 0;
|
||||
|
||||
return 'longtext';
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DB tables
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function db_get_tables(): array
|
||||
{
|
||||
$sql = 'SHOW TABLES';
|
||||
$rs = $this->con->select($sql);
|
||||
|
||||
$res = [];
|
||||
while ($rs->fetch()) {
|
||||
$res[] = $rs->f(0);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table fields
|
||||
*
|
||||
* @param string $table The table
|
||||
*
|
||||
* @return array Array of fields properties
|
||||
*/
|
||||
public function db_get_columns(string $table): array
|
||||
{
|
||||
$sql = 'SHOW COLUMNS FROM ' . $this->con->escapeSystem($table);
|
||||
$rs = $this->con->select($sql);
|
||||
|
||||
$res = [];
|
||||
while ($rs->fetch()) {
|
||||
$field = trim($rs->f('Field'));
|
||||
$type = trim($rs->f('Type'));
|
||||
$null = strtolower($rs->f('Null')) == 'yes';
|
||||
$default = $rs->f('Default');
|
||||
|
||||
$len = null;
|
||||
if (preg_match('/^(.+?)\(([\d,]+)\)$/si', $type, $m)) {
|
||||
$type = $m[1];
|
||||
$len = (int) $m[2];
|
||||
}
|
||||
|
||||
// $default from db is a string and is NULL in schema so upgrade failed.
|
||||
if (strtoupper((string) $default) == 'NULL') {
|
||||
$default = null;
|
||||
} elseif ($default != '' && !is_numeric($default)) {
|
||||
$default = "'" . $default . "'";
|
||||
}
|
||||
|
||||
$res[$field] = [
|
||||
'type' => $type,
|
||||
'len' => $len,
|
||||
'null' => $null,
|
||||
'default' => $default,
|
||||
];
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table keys
|
||||
*
|
||||
* @param string $table The table
|
||||
*
|
||||
* @return array Array of keys properties
|
||||
*/
|
||||
public function db_get_keys(string $table): array
|
||||
{
|
||||
$sql = 'SHOW INDEX FROM ' . $this->con->escapeSystem($table);
|
||||
$rs = $this->con->select($sql);
|
||||
|
||||
$t = [];
|
||||
$res = [];
|
||||
while ($rs->fetch()) {
|
||||
$key_name = $rs->f('Key_name');
|
||||
$unique = $rs->f('Non_unique') == 0;
|
||||
$seq = $rs->f('Seq_in_index');
|
||||
$col_name = $rs->f('Column_name');
|
||||
|
||||
if ($key_name == 'PRIMARY' || $unique) {
|
||||
$t[$key_name]['cols'][$seq] = $col_name;
|
||||
$t[$key_name]['unique'] = $unique;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($t as $name => $idx) {
|
||||
ksort($idx['cols']);
|
||||
|
||||
$res[] = [
|
||||
'name' => $name,
|
||||
'primary' => $name == 'PRIMARY',
|
||||
'unique' => $idx['unique'],
|
||||
'cols' => array_values($idx['cols']),
|
||||
];
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table's indexes
|
||||
*
|
||||
* @param string $table The table
|
||||
*
|
||||
* @return array Array of indexes properties
|
||||
*/
|
||||
public function db_get_indexes(string $table): array
|
||||
{
|
||||
$sql = 'SHOW INDEX FROM ' . $this->con->escapeSystem($table);
|
||||
$rs = $this->con->select($sql);
|
||||
|
||||
$t = [];
|
||||
$res = [];
|
||||
while ($rs->fetch()) {
|
||||
$key_name = $rs->f('Key_name');
|
||||
$unique = $rs->f('Non_unique') == 0;
|
||||
$seq = $rs->f('Seq_in_index');
|
||||
$col_name = $rs->f('Column_name');
|
||||
$type = $rs->f('Index_type');
|
||||
|
||||
if ($key_name != 'PRIMARY' && !$unique) {
|
||||
$t[$key_name]['cols'][$seq] = $col_name;
|
||||
$t[$key_name]['type'] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($t as $name => $idx) {
|
||||
ksort($idx['cols']);
|
||||
|
||||
$res[] = [
|
||||
'name' => $name,
|
||||
'type' => $idx['type'],
|
||||
'cols' => $idx['cols'],
|
||||
];
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get references
|
||||
*
|
||||
* @param string $table The table
|
||||
*
|
||||
* @return array Array of reference's properties
|
||||
*/
|
||||
public function db_get_references(string $table): array
|
||||
{
|
||||
$sql = 'SHOW CREATE TABLE ' . $this->con->escapeSystem($table);
|
||||
$rs = $this->con->select($sql);
|
||||
|
||||
$s = $rs->f(1);
|
||||
|
||||
$res = [];
|
||||
|
||||
$n = preg_match_all('/^\s*CONSTRAINT\s+`(.+?)`\s+FOREIGN\s+KEY\s+\((.+?)\)\s+REFERENCES\s+`(.+?)`\s+\((.+?)\)(.*?)$/msi', $s, $match);
|
||||
if ($n > 0) {
|
||||
foreach ($match[1] as $i => $name) {
|
||||
# Columns transformation
|
||||
$t_cols = str_replace('`', '', $match[2][$i]);
|
||||
$t_cols = explode(',', $t_cols);
|
||||
$r_cols = str_replace('`', '', $match[4][$i]);
|
||||
$r_cols = explode(',', $r_cols);
|
||||
|
||||
# ON UPDATE|DELETE
|
||||
$on = trim((string) $match[5][$i], ', ');
|
||||
$on_delete = null;
|
||||
$on_update = null;
|
||||
if ($on != '') {
|
||||
if (preg_match('/ON DELETE (.+?)(?:\s+ON|$)/msi', $on, $m)) {
|
||||
$on_delete = strtolower(trim((string) $m[1]));
|
||||
}
|
||||
if (preg_match('/ON UPDATE (.+?)(?:\s+ON|$)/msi', $on, $m)) {
|
||||
$on_update = strtolower(trim((string) $m[1]));
|
||||
}
|
||||
}
|
||||
|
||||
$res[] = [
|
||||
'name' => $name,
|
||||
'c_cols' => $t_cols,
|
||||
'p_table' => $match[3][$i],
|
||||
'p_cols' => $r_cols,
|
||||
'update' => $on_update,
|
||||
'delete' => $on_delete,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a table
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_table(string $name, array $fields): void
|
||||
{
|
||||
$a = [];
|
||||
|
||||
foreach ($fields as $n => $f) {
|
||||
$type = $f['type'];
|
||||
$len = (int) $f['len'];
|
||||
$default = $f['default'];
|
||||
$null = $f['null'];
|
||||
|
||||
$type = $this->udt2dbt($type, $len, $default);
|
||||
$len = $len > 0 ? '(' . $len . ')' : '';
|
||||
$null = $null ? 'NULL' : 'NOT NULL';
|
||||
|
||||
if ($default === null) {
|
||||
$default = 'DEFAULT NULL';
|
||||
} elseif ($default !== false) {
|
||||
$default = 'DEFAULT ' . $default . ' ';
|
||||
} else {
|
||||
$default = '';
|
||||
}
|
||||
|
||||
$a[] = $this->con->escapeSystem($n) . ' ' .
|
||||
$type . $len . ' ' . $null . ' ' . $default;
|
||||
}
|
||||
|
||||
$sql = 'CREATE TABLE ' . $this->con->escapeSystem($name) . " (\n" .
|
||||
implode(",\n", $a) .
|
||||
"\n) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin ";
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a field
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param int|null $len The length
|
||||
* @param bool $null The null
|
||||
* @param mixed $default The default
|
||||
*/
|
||||
public function db_create_field(string $table, string $name, string $type, ?int $len, bool $null, $default): void
|
||||
{
|
||||
$type = $this->udt2dbt($type, $len, $default);
|
||||
|
||||
if ($default === null) {
|
||||
$default = 'DEFAULT NULL';
|
||||
} elseif ($default !== false) {
|
||||
$default = 'DEFAULT ' . $default;
|
||||
} else {
|
||||
$default = '';
|
||||
}
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->con->escapeSystem($table) . ' ADD COLUMN ' . $this->con->escapeSystem($name) . ' ' . $type . ((int) $len > 0 ? '(' . (int) $len . ')' : '') . ' ' . ($null ? 'NULL' : 'NOT NULL') . ' ' . $default;
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a primary key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param array $fields The cols
|
||||
*/
|
||||
public function db_create_primary(string $table, string $name, array $fields): void
|
||||
{
|
||||
$c = array_map(fn ($field) => $this->con->escapeSystem($field), $fields);
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->con->escapeSystem($table) . ' ' .
|
||||
'ADD CONSTRAINT PRIMARY KEY (' . implode(',', $c) . ') ';
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_unique(string $table, string $name, array $fields): void
|
||||
{
|
||||
$c = array_map(fn ($field) => $this->con->escapeSystem($field), $fields);
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->con->escapeSystem($table) . ' ' .
|
||||
'ADD CONSTRAINT UNIQUE KEY ' . $this->con->escapeSystem($name) . ' ' .
|
||||
'(' . implode(',', $c) . ') ';
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an index
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_index(string $table, string $name, string $type, array $fields): void
|
||||
{
|
||||
$c = array_map(fn ($field) => $this->con->escapeSystem($field), $fields);
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->con->escapeSystem($table) . ' ' .
|
||||
'ADD INDEX ' . $this->con->escapeSystem($name) . ' USING ' . $type . ' ' .
|
||||
'(' . implode(',', $c) . ') ';
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a reference
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param string $table The table
|
||||
* @param array $fields The fields
|
||||
* @param string $foreign_table The foreign table
|
||||
* @param array $foreign_fields The foreign fields
|
||||
* @param string|bool $update The update
|
||||
* @param string|bool $delete The delete
|
||||
*/
|
||||
public function db_create_reference(string $name, string $table, array $fields, string $foreign_table, array $foreign_fields, $update, $delete): void
|
||||
{
|
||||
$c = array_map(fn ($field) => $this->con->escapeSystem($field), $fields);
|
||||
$p = array_map(fn ($field) => $this->con->escapeSystem($field), $foreign_fields);
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->con->escapeSystem($table) . ' ' .
|
||||
'ADD CONSTRAINT ' . $name . ' FOREIGN KEY ' .
|
||||
'(' . implode(',', $c) . ') ' .
|
||||
'REFERENCES ' . $this->con->escapeSystem($foreign_table) . ' ' .
|
||||
'(' . implode(',', $p) . ') ';
|
||||
|
||||
if ($update) {
|
||||
$sql .= 'ON UPDATE ' . $update . ' ';
|
||||
}
|
||||
if ($delete) {
|
||||
$sql .= 'ON DELETE ' . $delete . ' ';
|
||||
}
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a field
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param int|null $len The length
|
||||
* @param bool $null The null
|
||||
* @param mixed $default The default
|
||||
*/
|
||||
public function db_alter_field(string $table, string $name, string $type, ?int $len, bool $null, $default): void
|
||||
{
|
||||
$type = $this->udt2dbt($type, $len, $default);
|
||||
|
||||
if ($default === null) {
|
||||
$default = 'DEFAULT NULL';
|
||||
} elseif ($default !== false) {
|
||||
$default = 'DEFAULT ' . $default;
|
||||
} else {
|
||||
$default = '';
|
||||
}
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->con->escapeSystem($table) . ' ' .
|
||||
'CHANGE COLUMN ' . $this->con->escapeSystem($name) . ' ' . $this->con->escapeSystem($name) . ' ' . $type . ($len > 0 ? '(' . $len . ')' : '') . ' ' . ($null ? 'NULL' : 'NOT NULL') . ' ' . $default;
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a primary key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param array $fields The cols
|
||||
*/
|
||||
public function db_alter_primary(string $table, string $name, string $newname, array $fields): void
|
||||
{
|
||||
$c = array_map(fn ($field) => $this->con->escapeSystem($field), $fields);
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->con->escapeSystem($table) . ' ' .
|
||||
'DROP PRIMARY KEY, ADD PRIMARY KEY ' .
|
||||
'(' . implode(',', $c) . ') ';
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a unique key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_alter_unique(string $table, string $name, string $newname, array $fields): void
|
||||
{
|
||||
$c = array_map(fn ($field) => $this->con->escapeSystem($field), $fields);
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->con->escapeSystem($table) . ' ' .
|
||||
'DROP INDEX ' . $this->con->escapeSystem($name) . ', ' .
|
||||
'ADD UNIQUE ' . $this->con->escapeSystem($newname) . ' ' .
|
||||
'(' . implode(',', $c) . ') ';
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify an index
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param string $type The type
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_alter_index(string $table, string $name, string $newname, string $type, array $fields): void
|
||||
{
|
||||
$c = array_map(fn ($field) => $this->con->escapeSystem($field), $fields);
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->con->escapeSystem($table) . ' ' .
|
||||
'DROP INDEX ' . $this->con->escapeSystem($name) . ', ' .
|
||||
'ADD INDEX ' . $this->con->escapeSystem($newname) . ' ' .
|
||||
'USING ' . $type . ' ' .
|
||||
'(' . implode(',', $c) . ') ';
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a reference
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param string $table The table
|
||||
* @param array $fields The fields
|
||||
* @param string $foreign_table The foreign table
|
||||
* @param array $foreign_fields The foreign fields
|
||||
* @param bool|string $update The update
|
||||
* @param bool|string $delete The delete
|
||||
*/
|
||||
public function db_alter_reference(string $name, string $newname, string $table, array $fields, string $foreign_table, array $foreign_fields, $update, $delete): void
|
||||
{
|
||||
$sql = 'ALTER TABLE ' . $this->con->escapeSystem($table) . ' ' .
|
||||
'DROP FOREIGN KEY ' . $this->con->escapeSystem($name);
|
||||
|
||||
$this->con->execute($sql);
|
||||
$this->createReference($newname, $table, $fields, $foreign_table, $foreign_fields, $update, $delete);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a unique key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
*/
|
||||
public function db_drop_unique(string $table, string $name): void
|
||||
{
|
||||
$sql = 'ALTER TABLE ' . $this->con->escapeSystem($table) . ' ' .
|
||||
'DROP INDEX ' . $this->con->escapeSystem($name);
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
/**
|
||||
* @class mysqlimb4Schema
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage DBSchema
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
require_once 'class.mysqli.dbschema.php';
|
||||
|
||||
class mysqlimb4Schema extends mysqliSchema
|
||||
{
|
||||
/**
|
||||
* Create a table
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_table(string $name, array $fields): void
|
||||
{
|
||||
$a = [];
|
||||
|
||||
foreach ($fields as $n => $f) {
|
||||
$type = $f['type'];
|
||||
$len = (int) $f['len'];
|
||||
$default = $f['default'];
|
||||
$null = $f['null'];
|
||||
|
||||
$type = $this->udt2dbt($type, $len, $default);
|
||||
$len = $len > 0 ? '(' . $len . ')' : '';
|
||||
$null = $null ? 'NULL' : 'NOT NULL';
|
||||
|
||||
if ($default === null) {
|
||||
$default = 'DEFAULT NULL';
|
||||
} elseif ($default !== false) {
|
||||
$default = 'DEFAULT ' . $default . ' ';
|
||||
} else {
|
||||
$default = '';
|
||||
}
|
||||
|
||||
$a[] = $this->con->escapeSystem($n) . ' ' .
|
||||
$type . $len . ' ' . $null . ' ' . $default;
|
||||
}
|
||||
|
||||
$sql = 'CREATE TABLE ' . $this->con->escapeSystem($name) . " (\n" .
|
||||
implode(",\n", $a) .
|
||||
"\n) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,490 @@
|
|||
<?php
|
||||
/**
|
||||
* @class pgsqlSchema
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage DBSchema
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class pgsqlSchema extends dbSchema implements i_dbSchema
|
||||
{
|
||||
protected $ref_actions_map = [
|
||||
'a' => 'no action',
|
||||
'r' => 'restrict',
|
||||
'c' => 'cascade',
|
||||
'n' => 'set null',
|
||||
'd' => 'set default',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get DB tables
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function db_get_tables(): array
|
||||
{
|
||||
$sql = 'SELECT table_name ' .
|
||||
'FROM information_schema.tables ' .
|
||||
'WHERE table_schema = current_schema() ';
|
||||
|
||||
$rs = $this->con->select($sql);
|
||||
|
||||
$res = [];
|
||||
while ($rs->fetch()) {
|
||||
$res[] = $rs->f(0);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table's fields
|
||||
*
|
||||
* @param string $table The table
|
||||
*
|
||||
* @return array Array of fields properties
|
||||
*/
|
||||
public function db_get_columns(string $table): array
|
||||
{
|
||||
$sql = 'SELECT column_name, udt_name, character_maximum_length, ' .
|
||||
'is_nullable, column_default ' .
|
||||
'FROM information_schema.columns ' .
|
||||
"WHERE table_name = '" . $this->con->escape($table) . "' ";
|
||||
|
||||
$rs = $this->con->select($sql);
|
||||
|
||||
$res = [];
|
||||
while ($rs->fetch()) {
|
||||
$field = trim($rs->column_name);
|
||||
$type = trim($rs->udt_name);
|
||||
$null = strtolower($rs->is_nullable) == 'yes';
|
||||
$default = $rs->column_default;
|
||||
$len = $rs->character_maximum_length;
|
||||
|
||||
if ($len == '') {
|
||||
$len = null;
|
||||
}
|
||||
|
||||
$default = preg_replace('/::([\w\d\s]*)$/', '', $default);
|
||||
$default = preg_replace('/^\((-?\d*)\)$/', '$1', $default);
|
||||
|
||||
// $default from db is a string and is NULL in schema so upgrade failed.
|
||||
if (strtoupper((string) $default) == 'NULL') {
|
||||
$default = null;
|
||||
}
|
||||
|
||||
$res[$field] = [
|
||||
'type' => $type,
|
||||
'len' => $len,
|
||||
'null' => $null,
|
||||
'default' => $default,
|
||||
];
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tables keys
|
||||
*
|
||||
* @param string $table The table
|
||||
*
|
||||
* @return array Array of keys properties
|
||||
*/
|
||||
public function db_get_keys(string $table): array
|
||||
{
|
||||
$sql = 'SELECT DISTINCT ON(cls.relname) cls.oid, cls.relname as idxname, indisunique::integer, indisprimary::integer, ' .
|
||||
'indnatts, tab.relname as tabname, contype, amname ' .
|
||||
'FROM pg_index idx ' .
|
||||
'JOIN pg_class cls ON cls.oid=indexrelid ' .
|
||||
'JOIN pg_class tab ON tab.oid=indrelid ' .
|
||||
'LEFT OUTER JOIN pg_tablespace ta on ta.oid=cls.reltablespace ' .
|
||||
'JOIN pg_namespace n ON n.oid=tab.relnamespace ' .
|
||||
'JOIN pg_am am ON am.oid=cls.relam ' .
|
||||
"LEFT JOIN pg_depend dep ON (dep.classid = cls.tableoid AND dep.objid = cls.oid AND dep.refobjsubid = '0') " .
|
||||
'LEFT OUTER JOIN pg_constraint con ON (con.tableoid = dep.refclassid AND con.oid = dep.refobjid) ' .
|
||||
'LEFT OUTER JOIN pg_description des ON des.objoid=con.oid ' .
|
||||
'LEFT OUTER JOIN pg_description desp ON (desp.objoid=con.oid AND desp.objsubid = 0) ' .
|
||||
"WHERE tab.relname = '" . $this->con->escape($table) . "' " .
|
||||
"AND contype IN ('p','u') " .
|
||||
'ORDER BY cls.relname ';
|
||||
|
||||
$rs = $this->con->select($sql);
|
||||
|
||||
$res = [];
|
||||
while ($rs->fetch()) {
|
||||
$k = [
|
||||
'name' => $rs->idxname,
|
||||
'primary' => (bool) $rs->indisprimary,
|
||||
'unique' => (bool) $rs->indisunique,
|
||||
'cols' => [],
|
||||
];
|
||||
|
||||
for ($i = 1; $i <= $rs->indnatts; $i++) {
|
||||
$cols = $this->con->select('SELECT pg_get_indexdef(' . $rs->oid . '::oid, ' . $i . ', true);');
|
||||
$k['cols'][] = $cols->f(0);
|
||||
}
|
||||
|
||||
$res[] = $k;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table's indexes
|
||||
*
|
||||
* @param string $table The table
|
||||
*
|
||||
* @return array Array of indexes properties
|
||||
*/
|
||||
public function db_get_indexes(string $table): array
|
||||
{
|
||||
$sql = 'SELECT DISTINCT ON(cls.relname) cls.oid, cls.relname as idxname, n.nspname, ' .
|
||||
'indnatts, tab.relname as tabname, contype, amname ' .
|
||||
'FROM pg_index idx ' .
|
||||
'JOIN pg_class cls ON cls.oid=indexrelid ' .
|
||||
'JOIN pg_class tab ON tab.oid=indrelid ' .
|
||||
'LEFT OUTER JOIN pg_tablespace ta on ta.oid=cls.reltablespace ' .
|
||||
'JOIN pg_namespace n ON n.oid=tab.relnamespace ' .
|
||||
'JOIN pg_am am ON am.oid=cls.relam ' .
|
||||
"LEFT JOIN pg_depend dep ON (dep.classid = cls.tableoid AND dep.objid = cls.oid AND dep.refobjsubid = '0') " .
|
||||
'LEFT OUTER JOIN pg_constraint con ON (con.tableoid = dep.refclassid AND con.oid = dep.refobjid) ' .
|
||||
'LEFT OUTER JOIN pg_description des ON des.objoid=con.oid ' .
|
||||
'LEFT OUTER JOIN pg_description desp ON (desp.objoid=con.oid AND desp.objsubid = 0) ' .
|
||||
"WHERE tab.relname = '" . $this->con->escape($table) . "' " .
|
||||
'AND conname IS NULL ' .
|
||||
'ORDER BY cls.relname ';
|
||||
|
||||
$rs = $this->con->select($sql);
|
||||
|
||||
$res = [];
|
||||
while ($rs->fetch()) {
|
||||
$k = [
|
||||
'name' => $rs->idxname,
|
||||
'type' => $rs->amname,
|
||||
'cols' => [],
|
||||
];
|
||||
|
||||
for ($i = 1; $i <= $rs->indnatts; $i++) {
|
||||
$cols = $this->con->select('SELECT pg_get_indexdef(' . $rs->oid . '::oid, ' . $i . ', true);');
|
||||
$k['cols'][] = $cols->f(0);
|
||||
}
|
||||
|
||||
$res[] = $k;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get references
|
||||
*
|
||||
* @param string $table The table
|
||||
*
|
||||
* @return array Array of references properties
|
||||
*/
|
||||
public function db_get_references(string $table): array
|
||||
{
|
||||
$sql = 'SELECT ct.oid, conname, condeferrable, condeferred, confupdtype, ' .
|
||||
'confdeltype, confmatchtype, conkey, confkey, conrelid, confrelid, cl.relname as fktab, ' .
|
||||
'cr.relname as reftab ' .
|
||||
'FROM pg_constraint ct ' .
|
||||
'JOIN pg_class cl ON cl.oid=conrelid ' .
|
||||
'JOIN pg_namespace nl ON nl.oid=cl.relnamespace ' .
|
||||
'JOIN pg_class cr ON cr.oid=confrelid ' .
|
||||
'JOIN pg_namespace nr ON nr.oid=cr.relnamespace ' .
|
||||
"WHERE contype='f' " .
|
||||
"AND cl.relname = '" . $this->con->escape($table) . "' " .
|
||||
'ORDER BY conname ';
|
||||
|
||||
$rs = $this->con->select($sql);
|
||||
|
||||
$cols_sql = 'SELECT a1.attname as conattname, a2.attname as confattname ' .
|
||||
'FROM pg_attribute a1, pg_attribute a2 ' .
|
||||
'WHERE a1.attrelid=%1$s::oid AND a1.attnum=%2$s ' .
|
||||
'AND a2.attrelid=%3$s::oid AND a2.attnum=%4$s ';
|
||||
|
||||
$res = [];
|
||||
while ($rs->fetch()) {
|
||||
$conkey = preg_replace('/[^\d]/', '', $rs->conkey);
|
||||
$confkey = preg_replace('/[^\d]/', '', $rs->confkey);
|
||||
|
||||
$k = [
|
||||
'name' => $rs->conname,
|
||||
'c_cols' => [],
|
||||
'p_table' => $rs->reftab,
|
||||
'p_cols' => [],
|
||||
'update' => $this->ref_actions_map[$rs->confupdtype],
|
||||
'delete' => $this->ref_actions_map[$rs->confdeltype],
|
||||
];
|
||||
|
||||
$cols = $this->con->select(sprintf($cols_sql, $rs->conrelid, $conkey, $rs->confrelid, $confkey));
|
||||
while ($cols->fetch()) {
|
||||
$k['c_cols'][] = $cols->conattname;
|
||||
$k['p_cols'][] = $cols->confattname;
|
||||
}
|
||||
|
||||
$res[] = $k;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a table
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_table(string $name, array $fields): void
|
||||
{
|
||||
$a = [];
|
||||
|
||||
foreach ($fields as $n => $f) {
|
||||
$type = $f['type'];
|
||||
$len = (int) $f['len'];
|
||||
$default = $f['default'];
|
||||
$null = $f['null'];
|
||||
|
||||
$type = $this->udt2dbt($type, $len, $default);
|
||||
$len = $len > 0 ? '(' . $len . ')' : '';
|
||||
$null = $null ? 'NULL' : 'NOT NULL';
|
||||
|
||||
if ($default === null) {
|
||||
$default = 'DEFAULT NULL';
|
||||
} elseif ($default !== false) {
|
||||
$default = 'DEFAULT ' . $default . ' ';
|
||||
} else {
|
||||
$default = '';
|
||||
}
|
||||
|
||||
$a[] = $n . ' ' .
|
||||
$type . $len . ' ' . $null . ' ' . $default;
|
||||
}
|
||||
|
||||
$sql = 'CREATE TABLE ' . $this->con->escapeSystem($name) . " (\n" .
|
||||
implode(",\n", $a) .
|
||||
"\n)";
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a field
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param int|null $len The length
|
||||
* @param bool $null The null
|
||||
* @param mixed $default The default value
|
||||
*/
|
||||
public function db_create_field(string $table, string $name, string $type, ?int $len, bool $null, $default): void
|
||||
{
|
||||
$type = $this->udt2dbt($type, $len, $default);
|
||||
var_dump($default);
|
||||
if ($default === null) {
|
||||
$default = 'DEFAULT NULL';
|
||||
} elseif ($default !== false) {
|
||||
$default = 'DEFAULT ' . $default . ' ';
|
||||
} else {
|
||||
$default = '';
|
||||
}
|
||||
|
||||
$sql = 'ALTER TABLE ' . $table . ' ADD COLUMN ' . $name . ' ' . $type . ($len > 0 ? '(' . $len . ')' : '') . ' ' . ($null ? 'NULL' : 'NOT NULL') . ' ' . $default;
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create primary key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param array $fields The cols
|
||||
*/
|
||||
public function db_create_primary(string $table, string $name, array $fields): void
|
||||
{
|
||||
$sql = 'ALTER TABLE ' . $table . ' ' .
|
||||
'ADD CONSTRAINT ' . $name . ' PRIMARY KEY (' . implode(',', $fields) . ') ';
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_unique(string $table, string $name, array $fields): void
|
||||
{
|
||||
$sql = 'ALTER TABLE ' . $table . ' ' .
|
||||
'ADD CONSTRAINT ' . $name . ' UNIQUE (' . implode(',', $fields) . ') ';
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an index
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_index(string $table, string $name, string $type, array $fields): void
|
||||
{
|
||||
$sql = 'CREATE INDEX ' . $name . ' ON ' . $table . ' USING ' . $type .
|
||||
'(' . implode(',', $fields) . ') ';
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a reference
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param string $table The table
|
||||
* @param array $fields The fields
|
||||
* @param string $foreign_table The foreign table
|
||||
* @param array $foreign_fields The foreign fields
|
||||
* @param string|bool $update The update
|
||||
* @param string|bool $delete The delete
|
||||
*/
|
||||
public function db_create_reference(string $name, string $table, array $fields, string $foreign_table, array $foreign_fields, $update, $delete): void
|
||||
{
|
||||
$sql = 'ALTER TABLE ' . $table . ' ' .
|
||||
'ADD CONSTRAINT ' . $name . ' FOREIGN KEY ' .
|
||||
'(' . implode(',', $fields) . ') ' .
|
||||
'REFERENCES ' . $foreign_table . ' ' .
|
||||
'(' . implode(',', $foreign_fields) . ') ';
|
||||
|
||||
if ($update) {
|
||||
$sql .= 'ON UPDATE ' . $update . ' ';
|
||||
}
|
||||
if ($delete) {
|
||||
$sql .= 'ON DELETE ' . $delete . ' ';
|
||||
}
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a field
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param int|null $len The length
|
||||
* @param bool $null The null
|
||||
* @param mixed $default The default value
|
||||
*/
|
||||
public function db_alter_field(string $table, string $name, string $type, ?int $len, bool $null, $default): void
|
||||
{
|
||||
$type = $this->udt2dbt($type, $len, $default);
|
||||
|
||||
$sql = 'ALTER TABLE ' . $table . ' ALTER COLUMN ' . $name . ' TYPE ' . $type . ($len > 0 ? '(' . $len . ')' : '');
|
||||
$this->con->execute($sql);
|
||||
|
||||
if ($default === null) {
|
||||
$default = 'SET DEFAULT NULL';
|
||||
} elseif ($default !== false) {
|
||||
$default = 'SET DEFAULT ' . $default;
|
||||
} else {
|
||||
$default = 'DROP DEFAULT';
|
||||
}
|
||||
|
||||
$sql = 'ALTER TABLE ' . $table . ' ALTER COLUMN ' . $name . ' ' . $default;
|
||||
$this->con->execute($sql);
|
||||
|
||||
$null = $null ? 'DROP NOT NULL' : 'SET NOT NULL';
|
||||
$sql = 'ALTER TABLE ' . $table . ' ALTER COLUMN ' . $name . ' ' . $null;
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a primary key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_alter_primary(string $table, string $name, string $newname, array $fields): void
|
||||
{
|
||||
$sql = 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $name;
|
||||
$this->con->execute($sql);
|
||||
|
||||
$this->createPrimary($table, $newname, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a unique key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_alter_unique(string $table, string $name, string $newname, array $fields): void
|
||||
{
|
||||
$sql = 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $name;
|
||||
$this->con->execute($sql);
|
||||
|
||||
$this->createUnique($table, $newname, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify an index
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param string $type The type
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_alter_index(string $table, string $name, string $newname, string $type, array $fields): void
|
||||
{
|
||||
$sql = 'DROP INDEX ' . $name;
|
||||
$this->con->execute($sql);
|
||||
|
||||
$this->createIndex($table, $newname, $type, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a reference (foreign key)
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param string $table The table
|
||||
* @param array $fields The fields
|
||||
* @param string $foreign_table The foreign table
|
||||
* @param array $foreign_fields The foreign fields
|
||||
* @param bool|string $update The update
|
||||
* @param bool|string $delete The delete
|
||||
*/
|
||||
public function db_alter_reference(string $name, string $newname, string $table, array $fields, string $foreign_table, array $foreign_fields, $update, $delete): void
|
||||
{
|
||||
$sql = 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $name;
|
||||
$this->con->execute($sql);
|
||||
|
||||
$this->createReference($newname, $table, $fields, $foreign_table, $foreign_fields, $update, $delete);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a unique key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
*/
|
||||
public function db_drop_unique(string $table, string $name): void
|
||||
{
|
||||
$sql = 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $name;
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,637 @@
|
|||
<?php
|
||||
/**
|
||||
* @class sqliteSchema
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage DBSchema
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class sqliteSchema extends dbSchema implements i_dbSchema
|
||||
{
|
||||
private $table_hist = [];
|
||||
|
||||
private $table_stack = []; // Stack for tables creation
|
||||
private $x_stack = []; // Execution stack
|
||||
|
||||
/**
|
||||
* Translate DB type to universal type
|
||||
*
|
||||
* @param string $type The type
|
||||
* @param int|null $len The length
|
||||
* @param mixed $default The default value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dbt2udt(string $type, ?int &$len, &$default): string
|
||||
{
|
||||
$type = parent::dbt2udt($type, $len, $default);
|
||||
|
||||
switch ($type) {
|
||||
case 'float':
|
||||
return 'real';
|
||||
|
||||
case 'double':
|
||||
return 'float';
|
||||
|
||||
case 'timestamp':
|
||||
# DATETIME real type is TIMESTAMP
|
||||
if ($default === "'1970-01-01 00:00:00'") {
|
||||
# Bad hack
|
||||
$default = 'now()';
|
||||
}
|
||||
|
||||
return 'timestamp';
|
||||
|
||||
case 'integer':
|
||||
case 'mediumint':
|
||||
case 'bigint':
|
||||
case 'tinyint':
|
||||
case 'smallint':
|
||||
case 'numeric':
|
||||
return 'integer';
|
||||
|
||||
case 'tinytext':
|
||||
case 'longtext':
|
||||
return 'text';
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate universal type to DB type
|
||||
*
|
||||
* @param string $type The type
|
||||
* @param int $len The length
|
||||
* @param mixed $default The default value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function udt2dbt(string $type, ?int &$len, &$default): string
|
||||
{
|
||||
$type = parent::udt2dbt($type, $len, $default);
|
||||
|
||||
switch ($type) {
|
||||
case 'integer':
|
||||
case 'smallint':
|
||||
case 'bigint':
|
||||
return 'integer';
|
||||
|
||||
case 'real':
|
||||
case 'float:':
|
||||
return 'real';
|
||||
|
||||
case 'date':
|
||||
case 'time':
|
||||
return 'timestamp';
|
||||
|
||||
case 'timestamp':
|
||||
if ($default === 'now()') {
|
||||
# SQLite does not support now() default value...
|
||||
$default = "'1970-01-01 00:00:00'";
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
public function flushStack(): void
|
||||
{
|
||||
foreach ($this->table_stack as $table => $def) {
|
||||
$sql = 'CREATE TABLE ' . $table . " (\n" . implode(",\n", $def) . "\n)\n ";
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
foreach ($this->x_stack as $x) {
|
||||
$this->con->execute($x);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DB tables
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function db_get_tables(): array
|
||||
{
|
||||
$res = [];
|
||||
$sql = "SELECT * FROM sqlite_master WHERE type = 'table'";
|
||||
$rs = $this->con->select($sql);
|
||||
|
||||
$res = [];
|
||||
while ($rs->fetch()) {
|
||||
$res[] = $rs->tbl_name;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DB table's fields
|
||||
*
|
||||
* @param string $table The table name
|
||||
*
|
||||
* @return array Array of columns properties
|
||||
*/
|
||||
public function db_get_columns(string $table): array
|
||||
{
|
||||
$sql = 'PRAGMA table_info(' . $this->con->escapeSystem($table) . ')';
|
||||
$rs = $this->con->select($sql);
|
||||
|
||||
$res = [];
|
||||
while ($rs->fetch()) {
|
||||
$field = trim($rs->name);
|
||||
$type = trim($rs->type);
|
||||
$null = trim($rs->notnull) == 0;
|
||||
$default = trim($rs->dflt_value);
|
||||
|
||||
$len = null;
|
||||
if (preg_match('/^(.+?)\(([\d,]+)\)$/si', $type, $m)) {
|
||||
$type = $m[1];
|
||||
$len = (int) $m[2];
|
||||
}
|
||||
|
||||
$res[$field] = [
|
||||
'type' => $type,
|
||||
'len' => $len,
|
||||
'null' => $null,
|
||||
'default' => $default,
|
||||
];
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DB table's keys
|
||||
*
|
||||
* @param string $table The table name
|
||||
*
|
||||
* @return array Array of keys properties
|
||||
*/
|
||||
public function db_get_keys(string $table): array
|
||||
{
|
||||
$res = [];
|
||||
|
||||
# Get primary keys first
|
||||
$sql = "SELECT sql FROM sqlite_master WHERE type='table' AND name='" . $this->con->escape($table) . "'";
|
||||
$rs = $this->con->select($sql);
|
||||
|
||||
if ($rs->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
# Get primary keys
|
||||
$n = preg_match_all('/^\s*CONSTRAINT\s+([^,]+?)\s+PRIMARY\s+KEY\s+\((.+?)\)/msi', $rs->sql, $match);
|
||||
if ($n > 0) {
|
||||
foreach ($match[1] as $i => $name) {
|
||||
$cols = preg_split('/\s*,\s*/', $match[2][$i]);
|
||||
$res[] = [
|
||||
'name' => $name,
|
||||
'primary' => true,
|
||||
'unique' => false,
|
||||
'cols' => $cols,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
# Get unique keys
|
||||
$n = preg_match_all('/^\s*CONSTRAINT\s+([^,]+?)\s+UNIQUE\s+\((.+?)\)/msi', $rs->sql, $match);
|
||||
if ($n > 0) {
|
||||
foreach ($match[1] as $i => $name) {
|
||||
$cols = preg_split('/\s*,\s*/', $match[2][$i]);
|
||||
$res[] = [
|
||||
'name' => $name,
|
||||
'primary' => false,
|
||||
'unique' => true,
|
||||
'cols' => $cols,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DB table's indexes
|
||||
*
|
||||
* @param string $table The table name
|
||||
*
|
||||
* @return array Array of indexes properties
|
||||
*/
|
||||
public function db_get_indexes(string $table): array
|
||||
{
|
||||
$sql = 'PRAGMA index_list(' . $this->con->escapeSystem($table) . ')';
|
||||
$rs = $this->con->select($sql);
|
||||
|
||||
$res = [];
|
||||
while ($rs->fetch()) {
|
||||
if (preg_match('/^sqlite_/', $rs->name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$idx = $this->con->select('PRAGMA index_info(' . $this->con->escapeSystem($rs->name) . ')');
|
||||
$cols = [];
|
||||
while ($idx->fetch()) {
|
||||
$cols[] = $idx->name;
|
||||
}
|
||||
|
||||
$res[] = [
|
||||
'name' => $rs->name,
|
||||
'type' => 'btree',
|
||||
'cols' => $cols,
|
||||
];
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DB table's references
|
||||
*
|
||||
* @param string $table The table name
|
||||
*
|
||||
* @return array Array of references properties
|
||||
*/
|
||||
public function db_get_references(string $table): array
|
||||
{
|
||||
$sql = 'SELECT * FROM sqlite_master WHERE type=\'trigger\' AND tbl_name = \'%1$s\' AND name LIKE \'%2$s_%%\' ';
|
||||
$res = [];
|
||||
|
||||
# Find constraints on table
|
||||
$bir = $this->con->select(sprintf($sql, $this->con->escape($table), 'bir'));
|
||||
$bur = $this->con->select(sprintf($sql, $this->con->escape($table), 'bur'));
|
||||
|
||||
if ($bir->isEmpty() || $bur->isempty()) {
|
||||
return $res;
|
||||
}
|
||||
|
||||
while ($bir->fetch()) {
|
||||
# Find child column and parent table and column
|
||||
if (!preg_match('/FROM\s+(.+?)\s+WHERE\s+(.+?)\s+=\s+NEW\.(.+?)\s*?\) IS\s+NULL/msi', $bir->sql, $m)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$c_col = $m[3];
|
||||
$p_table = $m[1];
|
||||
$p_col = $m[2];
|
||||
|
||||
# Find on update
|
||||
$on_update = 'restrict';
|
||||
$aur = $this->con->select(sprintf($sql, $this->con->escape($p_table), 'aur'));
|
||||
while ($aur->fetch()) {
|
||||
if (!preg_match('/AFTER\s+UPDATE/msi', $aur->sql)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/UPDATE\s+' . $table . '\s+SET\s+' . $c_col . '\s*=\s*NEW.' . $p_col .
|
||||
'\s+WHERE\s+' . $c_col . '\s*=\s*OLD\.' . $p_col . '/msi', $aur->sql)) {
|
||||
$on_update = 'cascade';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (preg_match('/UPDATE\s+' . $table . '\s+SET\s+' . $c_col . '\s*=\s*NULL' .
|
||||
'\s+WHERE\s+' . $c_col . '\s*=\s*OLD\.' . $p_col . '/msi', $aur->sql)) {
|
||||
$on_update = 'set null';
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
# Find on delete
|
||||
$on_delete = 'restrict';
|
||||
$bdr = $this->con->select(sprintf($sql, $this->con->escape($p_table), 'bdr'));
|
||||
while ($bdr->fetch()) {
|
||||
if (!preg_match('/BEFORE\s+DELETE/msi', $bdr->sql)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/DELETE\s+FROM\s+' . $table . '\s+WHERE\s+' . $c_col . '\s*=\s*OLD\.' . $p_col . '/msi', $bdr->sql)) {
|
||||
$on_delete = 'cascade';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (preg_match('/UPDATE\s+' . $table . '\s+SET\s+' . $c_col . '\s*=\s*NULL' .
|
||||
'\s+WHERE\s+' . $c_col . '\s*=\s*OLD\.' . $p_col . '/msi', $bdr->sql)) {
|
||||
$on_update = 'set null';
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$res[] = [
|
||||
'name' => substr($bir->name, 4),
|
||||
'c_cols' => [$c_col],
|
||||
'p_table' => $p_table,
|
||||
'p_cols' => [$p_col],
|
||||
'update' => $on_update,
|
||||
'delete' => $on_delete,
|
||||
];
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create table
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_table(string $name, array $fields): void
|
||||
{
|
||||
$a = [];
|
||||
|
||||
foreach ($fields as $n => $f) {
|
||||
$type = $f['type'];
|
||||
$len = (int) $f['len'];
|
||||
$default = $f['default'];
|
||||
$null = $f['null'];
|
||||
|
||||
$type = $this->udt2dbt($type, $len, $default);
|
||||
$len = $len > 0 ? '(' . $len . ')' : '';
|
||||
$null = $null ? 'NULL' : 'NOT NULL';
|
||||
|
||||
if ($default === null) {
|
||||
$default = 'DEFAULT NULL';
|
||||
} elseif ($default !== false) {
|
||||
$default = 'DEFAULT ' . $default . ' ';
|
||||
} else {
|
||||
$default = '';
|
||||
}
|
||||
|
||||
$a[] = $n . ' ' . $type . $len . ' ' . $null . ' ' . $default;
|
||||
}
|
||||
|
||||
$this->table_stack[$name][] = implode(",\n", $a);
|
||||
$this->table_hist[$name] = $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a field
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param int|null $len The length
|
||||
* @param bool $null The null
|
||||
* @param mixed $default The default
|
||||
*/
|
||||
public function db_create_field(string $table, string $name, string $type, ?int $len, bool $null, $default): void
|
||||
{
|
||||
$type = $this->udt2dbt($type, $len, $default);
|
||||
|
||||
if ($default === null) {
|
||||
$default = 'DEFAULT NULL';
|
||||
} elseif ($default !== false) {
|
||||
$default = 'DEFAULT ' . $default . ' ';
|
||||
} else {
|
||||
$default = '';
|
||||
}
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->con->escapeSystem($table) . ' ADD COLUMN ' . $this->con->escapeSystem($name) . ' ' . $type . ($len > 0 ? '(' . $len . ')' : '') . ' ' . ($null ? 'NULL' : 'NOT NULL') . ' ' . $default;
|
||||
|
||||
$this->con->execute($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a primary key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_primary(string $table, string $name, array $fields): void
|
||||
{
|
||||
$this->table_stack[$table][] = 'CONSTRAINT ' . $name . ' PRIMARY KEY (' . implode(',', $fields) . ') ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_unique(string $table, string $name, array $fields): void
|
||||
{
|
||||
$this->table_stack[$table][] = 'CONSTRAINT ' . $name . ' UNIQUE (' . implode(',', $fields) . ') ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an index
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_create_index(string $table, string $name, string $type, array $fields): void
|
||||
{
|
||||
$this->x_stack[] = 'CREATE INDEX ' . $name . ' ON ' . $table . ' (' . implode(',', $fields) . ') ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create reference
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param string $table The table
|
||||
* @param array $fields The fields
|
||||
* @param string $foreign_table The foreign table
|
||||
* @param array $foreign_fields The foreign fields
|
||||
* @param bool|string $update The update
|
||||
* @param bool|string $delete The delete
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function db_create_reference(string $name, string $table, array $fields, string $foreign_table, array $foreign_fields, $update, $delete): void
|
||||
{
|
||||
if (!isset($this->table_hist[$table])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count($fields) > 1 || count($foreign_fields) > 1) {
|
||||
throw new Exception('SQLite UDBS does not support multiple columns foreign keys');
|
||||
}
|
||||
|
||||
$c_col = $fields[0];
|
||||
$p_col = $foreign_fields[0];
|
||||
|
||||
$update = $update !== false ? strtolower($update) : '';
|
||||
$delete = $delete !== false ? strtolower($delete) : '';
|
||||
|
||||
$cnull = $this->table_hist[$table][$c_col]['null'];
|
||||
|
||||
# Create constraint
|
||||
$this->x_stack[] = 'CREATE TRIGGER bir_' . $name . "\n" .
|
||||
'BEFORE INSERT ON ' . $table . "\n" .
|
||||
"FOR EACH ROW BEGIN\n" .
|
||||
' SELECT RAISE(ROLLBACK,\'insert on table "' . $table . '" violates foreign key constraint "' . $name . '"\')' . "\n" .
|
||||
' WHERE ' .
|
||||
($cnull ? 'NEW.' . $c_col . " IS NOT NULL\n AND " : '') .
|
||||
'(SELECT ' . $p_col . ' FROM ' . $foreign_table . ' WHERE ' . $p_col . ' = NEW.' . $c_col . ") IS NULL;\n" .
|
||||
"END;\n";
|
||||
|
||||
# Update constraint
|
||||
$this->x_stack[] = 'CREATE TRIGGER bur_' . $name . "\n" .
|
||||
'BEFORE UPDATE ON ' . $table . "\n" .
|
||||
"FOR EACH ROW BEGIN\n" .
|
||||
' SELECT RAISE(ROLLBACK,\'update on table "' . $table . '" violates foreign key constraint "' . $name . '"\')' . "\n" .
|
||||
' WHERE ' .
|
||||
($cnull ? 'NEW.' . $c_col . " IS NOT NULL\n AND " : '') .
|
||||
'(SELECT ' . $p_col . ' FROM ' . $foreign_table . ' WHERE ' . $p_col . ' = NEW.' . $c_col . ") IS NULL;\n" .
|
||||
"END;\n";
|
||||
|
||||
# ON UPDATE
|
||||
if ($update === 'cascade') {
|
||||
$this->x_stack[] = 'CREATE TRIGGER aur_' . $name . "\n" .
|
||||
'AFTER UPDATE ON ' . $foreign_table . "\n" .
|
||||
"FOR EACH ROW BEGIN\n" .
|
||||
' UPDATE ' . $table . ' SET ' . $c_col . ' = NEW.' . $p_col . ' WHERE ' . $c_col . ' = OLD.' . $p_col . ";\n" .
|
||||
"END;\n";
|
||||
} elseif ($update === 'set null') {
|
||||
$this->x_stack[] = 'CREATE TRIGGER aur_' . $name . "\n" .
|
||||
'AFTER UPDATE ON ' . $foreign_table . "\n" .
|
||||
"FOR EACH ROW BEGIN\n" .
|
||||
' UPDATE ' . $table . ' SET ' . $c_col . ' = NULL WHERE ' . $c_col . ' = OLD.' . $p_col . ";\n" .
|
||||
"END;\n";
|
||||
} else { # default on restrict
|
||||
$this->x_stack[] = 'CREATE TRIGGER burp_' . $name . "\n" .
|
||||
'BEFORE UPDATE ON ' . $foreign_table . "\n" .
|
||||
"FOR EACH ROW BEGIN\n" .
|
||||
' SELECT RAISE (ROLLBACK,\'update on table "' . $foreign_table . '" violates foreign key constraint "' . $name . '"\')' . "\n" .
|
||||
' WHERE (SELECT ' . $c_col . ' FROM ' . $table . ' WHERE ' . $c_col . ' = OLD.' . $p_col . ") IS NOT NULL;\n" .
|
||||
"END;\n";
|
||||
}
|
||||
|
||||
# ON DELETE
|
||||
if ($delete === 'cascade') {
|
||||
$this->x_stack[] = 'CREATE TRIGGER bdr_' . $name . "\n" .
|
||||
'BEFORE DELETE ON ' . $foreign_table . "\n" .
|
||||
"FOR EACH ROW BEGIN\n" .
|
||||
' DELETE FROM ' . $table . ' WHERE ' . $c_col . ' = OLD.' . $p_col . ";\n" .
|
||||
"END;\n";
|
||||
} elseif ($delete === 'set null') {
|
||||
$this->x_stack[] = 'CREATE TRIGGER bdr_' . $name . "\n" .
|
||||
'BEFORE DELETE ON ' . $foreign_table . "\n" .
|
||||
"FOR EACH ROW BEGIN\n" .
|
||||
' UPDATE ' . $table . ' SET ' . $c_col . ' = NULL WHERE ' . $c_col . ' = OLD.' . $p_col . ";\n" .
|
||||
"END;\n";
|
||||
} else {
|
||||
$this->x_stack[] = 'CREATE TRIGGER bdr_' . $name . "\n" .
|
||||
'BEFORE DELETE ON ' . $foreign_table . "\n" .
|
||||
"FOR EACH ROW BEGIN\n" .
|
||||
' SELECT RAISE (ROLLBACK,\'delete on table "' . $foreign_table . '" violates foreign key constraint "' . $name . '"\')' . "\n" .
|
||||
' WHERE (SELECT ' . $c_col . ' FROM ' . $table . ' WHERE ' . $c_col . ' = OLD.' . $p_col . ") IS NOT NULL;\n" .
|
||||
"END;\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a field
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $type The type
|
||||
* @param int|null $len The length
|
||||
* @param bool $null The null
|
||||
* @param mixed $default The default
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function db_alter_field(string $table, string $name, string $type, ?int $len, bool $null, $default): void
|
||||
{
|
||||
$type = $this->udt2dbt($type, $len, $default);
|
||||
if ($type != 'integer' && $type != 'text' && $type != 'timestamp') {
|
||||
throw new Exception('SQLite fields cannot be changed.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a primary key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param array $fields The fields
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function db_alter_primary(string $table, string $name, string $newname, array $fields): void
|
||||
{
|
||||
throw new Exception('SQLite primary key cannot be changed.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a unique key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param array $fields The fields
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function db_alter_unique(string $table, string $name, string $newname, array $fields): void
|
||||
{
|
||||
throw new Exception('SQLite unique index cannot be changed.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify an index
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param string $type The type
|
||||
* @param array $fields The fields
|
||||
*/
|
||||
public function db_alter_index(string $table, string $name, string $newname, string $type, array $fields): void
|
||||
{
|
||||
$this->con->execute('DROP INDEX IF EXISTS ' . $name);
|
||||
$this->con->execute('CREATE INDEX ' . $newname . ' ON ' . $table . ' (' . implode(',', $fields) . ') ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a reference (foreign key)
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param string $newname The newname
|
||||
* @param string $table The table
|
||||
* @param array $fields The fields
|
||||
* @param string $foreign_table The foreign table
|
||||
* @param array $foreign_fields The foreign fields
|
||||
* @param string|bool $update The update
|
||||
* @param string|bool $delete The delete
|
||||
*/
|
||||
public function db_alter_reference(string $name, string $newname, string $table, array $fields, string $foreign_table, array $foreign_fields, $update, $delete): void
|
||||
{
|
||||
$this->con->execute('DROP TRIGGER IF EXISTS bur_' . $name);
|
||||
$this->con->execute('DROP TRIGGER IF EXISTS burp_' . $name);
|
||||
$this->con->execute('DROP TRIGGER IF EXISTS bir_' . $name);
|
||||
$this->con->execute('DROP TRIGGER IF EXISTS aur_' . $name);
|
||||
$this->con->execute('DROP TRIGGER IF EXISTS bdr_' . $name);
|
||||
|
||||
$this->table_hist[$table] = $this->db_get_columns($table);
|
||||
$this->db_create_reference($newname, $table, $fields, $foreign_table, $foreign_fields, $update, $delete);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a unique key
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $name The name
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function db_drop_unique(string $table, string $name): void
|
||||
{
|
||||
throw new Exception('SQLite unique index cannot be removed.');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,359 @@
|
|||
<?php
|
||||
/**
|
||||
* @class diff
|
||||
* @brief Unified diff
|
||||
*
|
||||
* Diff utilities
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Diff
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class diff
|
||||
{
|
||||
// Constants
|
||||
|
||||
private const US_RANGE = "@@ -%s,%s +%s,%s @@\n";
|
||||
private const US_CTX = " %s\n";
|
||||
private const US_INS = "+%s\n";
|
||||
private const US_DEL = "-%s\n";
|
||||
|
||||
private const UP_RANGE = '/^@@ -([\d]+),([\d]+) \+([\d]+),([\d]+) @@/';
|
||||
private const UP_CTX = '/^ (.*)$/';
|
||||
private const UP_INS = '/^\+(.*)$/';
|
||||
private const UP_DEL = '/^-(.*)$/';
|
||||
|
||||
/**
|
||||
* Finds the shortest edit script using a fast algorithm taken from paper
|
||||
* "An O(ND) Difference Algorithm and Its Variations" by Eugene W.Myers,
|
||||
* 1986.
|
||||
*
|
||||
* @param array $src Original data
|
||||
* @param array $dst New data
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function SES(array $src, array $dst): array
|
||||
{
|
||||
$x = $y = $k = 0;
|
||||
|
||||
$cx = count($src);
|
||||
$cy = count($dst);
|
||||
|
||||
$stack = [];
|
||||
$V = [1 => 0];
|
||||
$end_reached = false;
|
||||
|
||||
# Find LCS length
|
||||
for ($D = 0; $D < $cx + $cy + 1 && !$end_reached; $D++) {
|
||||
for ($k = -$D; $k <= $D; $k += 2) {
|
||||
$x = ($k == -$D || $k != $D && $V[$k - 1] < $V[$k + 1])
|
||||
? $V[$k + 1] : $V[$k - 1] + 1;
|
||||
$y = $x - $k;
|
||||
|
||||
while ($x < $cx && $y < $cy && $src[$x] == $dst[$y]) {
|
||||
$x++;
|
||||
$y++;
|
||||
}
|
||||
|
||||
$V[$k] = $x;
|
||||
|
||||
if ($x == $cx && $y == $cy) {
|
||||
$end_reached = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
$stack[] = $V;
|
||||
}
|
||||
$D--;
|
||||
|
||||
# Recover edit path
|
||||
$res = [];
|
||||
for ($D = $D; $D > 0; $D--) {
|
||||
$V = array_pop($stack);
|
||||
$cx = $x;
|
||||
$cy = $y;
|
||||
|
||||
# Try right diagonal
|
||||
$k++;
|
||||
$x = array_key_exists($k, $V) ? $V[$k] : 0;
|
||||
$y = $x - $k;
|
||||
$y++;
|
||||
|
||||
while ($x < $cx && $y < $cy
|
||||
&& isset($src[$x]) && isset($dst[$y]) && $src[$x] == $dst[$y]) {
|
||||
$x++;
|
||||
$y++;
|
||||
}
|
||||
|
||||
if ($x == $cx && $y == $cy) {
|
||||
$x = $V[$k];
|
||||
$y = $x - $k;
|
||||
|
||||
$res[] = ['i', $x, $y];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
# Right diagonal wasn't the solution, use left diagonal
|
||||
$k -= 2;
|
||||
$x = $V[$k];
|
||||
$y = $x - $k;
|
||||
$res[] = ['d', $x, $y];
|
||||
}
|
||||
|
||||
return array_reverse($res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns unified diff from source $src to destination $dst.
|
||||
*
|
||||
* @param string $src Original data
|
||||
* @param string $dst New data
|
||||
* @param int $ctx Context length
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function uniDiff(string $src, string $dst, int $ctx = 2): string
|
||||
{
|
||||
[$src, $dst] = [explode("\n", $src), explode("\n", $dst)];
|
||||
|
||||
$ses = diff::SES($src, $dst);
|
||||
$res = '';
|
||||
|
||||
$pos_x = 0;
|
||||
$pos_y = 0;
|
||||
$old_lines = 0;
|
||||
$new_lines = 0;
|
||||
$buffer = '';
|
||||
|
||||
foreach ($ses as $cmd) {
|
||||
[$cmd, $x, $y] = [$cmd[0], $cmd[1], $cmd[2]];
|
||||
|
||||
# New chunk
|
||||
if ($x - $pos_x > 2 * $ctx || $pos_x == 0 && $x > $ctx) {
|
||||
# Footer for current chunk
|
||||
for ($i = 0; $buffer && $i < $ctx; $i++) {
|
||||
$buffer .= sprintf(self::US_CTX, $src[$pos_x + $i]);
|
||||
}
|
||||
|
||||
# Header for current chunk
|
||||
$res .= sprintf(
|
||||
self::US_RANGE,
|
||||
$pos_x + 1 - $old_lines,
|
||||
$old_lines + $i,
|
||||
$pos_y + 1 - $new_lines,
|
||||
$new_lines + $i
|
||||
) . $buffer;
|
||||
|
||||
$pos_x = $x;
|
||||
$pos_y = $y;
|
||||
$old_lines = 0;
|
||||
$new_lines = 0;
|
||||
$buffer = '';
|
||||
|
||||
# Header for next chunk
|
||||
for ($i = $ctx; $i > 0; $i--) {
|
||||
$buffer .= sprintf(self::US_CTX, $src[$pos_x - $i]);
|
||||
$old_lines++;
|
||||
$new_lines++;
|
||||
}
|
||||
}
|
||||
|
||||
# Context
|
||||
while ($x > $pos_x) {
|
||||
$old_lines++;
|
||||
$new_lines++;
|
||||
$buffer .= sprintf(self::US_CTX, $src[$pos_x]);
|
||||
$pos_x++;
|
||||
$pos_y++;
|
||||
}
|
||||
# Deletion
|
||||
if ($cmd == 'd') {
|
||||
$old_lines++;
|
||||
$buffer .= sprintf(self::US_DEL, $src[$x]);
|
||||
$pos_x++;
|
||||
}
|
||||
# Insertion
|
||||
elseif ($cmd == 'i') {
|
||||
$new_lines++;
|
||||
$buffer .= sprintf(self::US_INS, $dst[$y]);
|
||||
$pos_y++;
|
||||
}
|
||||
}
|
||||
|
||||
# Remaining chunk
|
||||
if ($buffer) {
|
||||
# Footer
|
||||
for ($i = 0; $i < $ctx; $i++) {
|
||||
if (!isset($src[$pos_x + $i])) {
|
||||
break;
|
||||
}
|
||||
$buffer .= sprintf(self::US_CTX, $src[$pos_x + $i]);
|
||||
}
|
||||
|
||||
# Header for current chunk
|
||||
$res .= sprintf(
|
||||
self::US_RANGE,
|
||||
$pos_x + 1 - $old_lines,
|
||||
$old_lines + $i,
|
||||
$pos_y + 1 - $new_lines,
|
||||
$new_lines + $i
|
||||
) . $buffer;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a unified patch to a piece of text.
|
||||
* Throws an exception on invalid or not applicable diff.
|
||||
*
|
||||
* @param string $src Source text
|
||||
* @param string $diff Patch to apply
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function uniPatch(string $src, string $diff): string
|
||||
{
|
||||
$dst = [];
|
||||
$src = explode("\n", $src);
|
||||
$diff = explode("\n", $diff);
|
||||
|
||||
$t = count($src);
|
||||
$old_length = $new_length = 0;
|
||||
|
||||
foreach ($diff as $line) {
|
||||
# New chunk
|
||||
if (preg_match(self::UP_RANGE, $line, $m)) {
|
||||
$m[1]--;
|
||||
$m[3]--;
|
||||
|
||||
if ($m[1] > $t) {
|
||||
throw new Exception(__('Bad range'));
|
||||
}
|
||||
|
||||
if ($t - count($src) > $m[1]) {
|
||||
throw new Exception(__('Invalid range'));
|
||||
}
|
||||
|
||||
while ($t - count($src) < $m[1]) {
|
||||
$dst[] = array_shift($src);
|
||||
}
|
||||
|
||||
if (count($dst) !== $m[3]) {
|
||||
throw new Exception(__('Invalid line number'));
|
||||
}
|
||||
|
||||
if ($old_length || $new_length) { // @phpstan-ignore-line
|
||||
throw new Exception(__('Chunk is out of range'));
|
||||
}
|
||||
|
||||
$old_length = (int) $m[2];
|
||||
$new_length = (int) $m[4];
|
||||
}
|
||||
# Context
|
||||
elseif (preg_match(self::UP_CTX, $line, $m)) {
|
||||
if (array_shift($src) !== $m[1]) {
|
||||
throw new Exception(__('Bad context'));
|
||||
}
|
||||
$dst[] = $m[1];
|
||||
$old_length--;
|
||||
$new_length--;
|
||||
}
|
||||
# Addition
|
||||
elseif (preg_match(self::UP_INS, $line, $m)) {
|
||||
$dst[] = $m[1];
|
||||
$new_length--;
|
||||
}
|
||||
# Deletion
|
||||
elseif (preg_match(self::UP_DEL, $line, $m)) {
|
||||
if (array_shift($src) !== $m[1]) {
|
||||
throw new Exception(__('Bad context (in deletion)'));
|
||||
}
|
||||
$old_length--;
|
||||
} elseif ($line == '') {
|
||||
continue;
|
||||
} else {
|
||||
throw new Exception(__('Invalid diff format'));
|
||||
}
|
||||
}
|
||||
|
||||
if ($old_length || $new_length) {
|
||||
throw new Exception(__('Chunk is out of range'));
|
||||
}
|
||||
|
||||
return implode("\n", array_merge($dst, $src));
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception on invalid unified diff.
|
||||
*
|
||||
* @param string $diff Diff text to check
|
||||
*/
|
||||
public static function uniCheck(string $diff): void
|
||||
{
|
||||
$diff = explode("\n", $diff);
|
||||
|
||||
$cur_line = 1;
|
||||
$ins_lines = 0;
|
||||
|
||||
# Chunk length
|
||||
$old_length = $new_length = 0;
|
||||
|
||||
foreach ($diff as $line) {
|
||||
# New chunk
|
||||
if (preg_match(self::UP_RANGE, $line, $m)) {
|
||||
if ($cur_line > $m[1]) {
|
||||
throw new Exception(__('Invalid range'));
|
||||
}
|
||||
while ($cur_line < $m[1]) {
|
||||
$ins_lines++;
|
||||
$cur_line++;
|
||||
}
|
||||
if ($ins_lines + 1 != $m[3]) {
|
||||
throw new Exception(__('Invalid line number'));
|
||||
}
|
||||
|
||||
if ($old_length || $new_length) {
|
||||
throw new Exception(__('Chunk is out of range'));
|
||||
}
|
||||
|
||||
$old_length = $m[2];
|
||||
$new_length = $m[4];
|
||||
}
|
||||
# Context
|
||||
elseif (preg_match(self::UP_CTX, $line, $m)) {
|
||||
$ins_lines++;
|
||||
$cur_line++;
|
||||
$old_length--;
|
||||
$new_length--;
|
||||
}
|
||||
# Addition
|
||||
elseif (preg_match(self::UP_INS, $line, $m)) {
|
||||
$ins_lines++;
|
||||
$new_length--;
|
||||
}
|
||||
# Deletion
|
||||
elseif (preg_match(self::UP_DEL, $line, $m)) {
|
||||
$cur_line++;
|
||||
$old_length--;
|
||||
}
|
||||
# Skip empty lines
|
||||
elseif ($line == '') {
|
||||
continue;
|
||||
}
|
||||
# Unrecognized diff format
|
||||
else {
|
||||
throw new Exception(__('Invalid diff format'));
|
||||
}
|
||||
}
|
||||
|
||||
if ($old_length || $new_length) {
|
||||
throw new Exception(__('Chunk is out of range'));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,363 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @class tidyDiff
|
||||
* @brief TIDY diff
|
||||
*
|
||||
* A TIDY diff representation
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Diff
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class tidyDiff
|
||||
{
|
||||
// Constants
|
||||
|
||||
private const UP_RANGE = '/^@@ -([\d]+),([\d]+) \+([\d]+),([\d]+) @@/m';
|
||||
private const UP_CTX = '/^ (.*)$/';
|
||||
private const UP_INS = '/^\+(.*)$/';
|
||||
private const UP_DEL = '/^-(.*)$/';
|
||||
|
||||
/**
|
||||
* Chunks array
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $__data = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Creates a diff representation from unified diff.
|
||||
*
|
||||
* @param string $udiff Unified diff
|
||||
* @param bool $inline_changes Find inline changes
|
||||
*/
|
||||
public function __construct(string $udiff, bool $inline_changes = false)
|
||||
{
|
||||
diff::uniCheck($udiff);
|
||||
|
||||
preg_match_all(self::UP_RANGE, $udiff, $context);
|
||||
|
||||
$chunks = preg_split(self::UP_RANGE, $udiff, -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
foreach ($chunks as $k => $chunk) {
|
||||
$tidy_chunk = new tidyDiffChunk();
|
||||
$tidy_chunk->setRange(
|
||||
(int) $context[1][$k],
|
||||
(int) $context[2][$k],
|
||||
(int) $context[3][$k],
|
||||
(int) $context[4][$k]
|
||||
);
|
||||
|
||||
$old_line = (int) $context[1][$k];
|
||||
$new_line = (int) $context[3][$k];
|
||||
|
||||
foreach (explode("\n", $chunk) as $line) {
|
||||
# context
|
||||
if (preg_match(self::UP_CTX, $line, $m)) {
|
||||
$tidy_chunk->addLine('context', [$old_line, $new_line], $m[1]);
|
||||
$old_line++;
|
||||
$new_line++;
|
||||
}
|
||||
# insertion
|
||||
if (preg_match(self::UP_INS, $line, $m)) {
|
||||
$tidy_chunk->addLine('insert', [$old_line, $new_line], $m[1]);
|
||||
$new_line++;
|
||||
}
|
||||
# deletion
|
||||
if (preg_match(self::UP_DEL, $line, $m)) {
|
||||
$tidy_chunk->addLine('delete', [$old_line, $new_line], $m[1]);
|
||||
$old_line++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($inline_changes) {
|
||||
$tidy_chunk->findInsideChanges();
|
||||
}
|
||||
|
||||
array_push($this->__data, $tidy_chunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All chunks
|
||||
*
|
||||
* Returns all chunks defined.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getChunks(): array
|
||||
{
|
||||
return $this->__data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class tidyDiffChunk
|
||||
* @brief TIDY diff chunk
|
||||
*
|
||||
* A diff chunk representation. Used by a TIDY diff.
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Diff
|
||||
*/
|
||||
class tidyDiffChunk
|
||||
{
|
||||
/**
|
||||
* Chunk information array
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $__info;
|
||||
|
||||
/**
|
||||
* Chunk data array
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $__data;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Creates and initializes a chunk representation for a TIDY diff.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->__info = [
|
||||
'context' => 0,
|
||||
'delete' => 0,
|
||||
'insert' => 0,
|
||||
'range' => [
|
||||
'start' => [],
|
||||
'end' => [],
|
||||
],
|
||||
];
|
||||
$this->__data = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set chunk range
|
||||
*
|
||||
* Sets chunk range in TIDY chunk object.
|
||||
*
|
||||
* @param int $line_start Old start line number
|
||||
* @param int $offest_start Old offset number
|
||||
* @param int $line_end new start line number
|
||||
* @param int $offset_end New offset number
|
||||
*/
|
||||
public function setRange(int $line_start, int $offest_start, int $line_end, int $offset_end): void
|
||||
{
|
||||
$this->__info['range']['start'] = [$line_start, $offest_start];
|
||||
$this->__info['range']['end'] = [$line_end, $offset_end];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add line
|
||||
*
|
||||
* Adds TIDY line object for TIDY chunk object.
|
||||
*
|
||||
* @param string $type Tine type
|
||||
* @param array $lines Line number for old and new context
|
||||
* @param string $content Line content
|
||||
*/
|
||||
public function addLine(string $type, array $lines, string $content): void
|
||||
{
|
||||
$tidy_line = new tidyDiffLine($type, $lines, $content);
|
||||
|
||||
array_push($this->__data, $tidy_line);
|
||||
$this->__info[$type]++;
|
||||
}
|
||||
|
||||
/**
|
||||
* All lines
|
||||
*
|
||||
* Returns all lines defined.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLines(): array
|
||||
{
|
||||
return $this->__data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk information
|
||||
*
|
||||
* Returns chunk information according to the given name, null otherwise.
|
||||
*
|
||||
* @param string $n Info name
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getInfo($n)
|
||||
{
|
||||
return array_key_exists($n, $this->__info) ? $this->__info[$n] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find changes
|
||||
*
|
||||
* Finds changes inside lines for each groups of diff lines. Wraps changes
|
||||
* by string \0 and \1
|
||||
*/
|
||||
public function findInsideChanges(): void
|
||||
{
|
||||
$groups = $this->getGroups();
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$middle = count($group) / 2;
|
||||
for ($i = 0; $i < $middle; $i++) {
|
||||
$from = $group[$i];
|
||||
$to = $group[$i + $middle];
|
||||
$threshold = $this->getChangeExtent($from->content, $to->content);
|
||||
|
||||
if ($threshold['start'] != 0 || $threshold['end'] != 0) {
|
||||
$start = $threshold['start'];
|
||||
$end = $threshold['end'] + strlen($from->content);
|
||||
$offset = $end - $start;
|
||||
$from->overwrite(
|
||||
substr($from->content, 0, $start) . '\0' .
|
||||
substr($from->content, $start, $offset) . '\1' .
|
||||
substr($from->content, $end, strlen($from->content))
|
||||
);
|
||||
$end = $threshold['end'] + strlen($to->content);
|
||||
$offset = $end - $start;
|
||||
$to->overwrite(
|
||||
substr($to->content, 0, $start) . '\0' .
|
||||
substr($to->content, $start, $offset) . '\1' .
|
||||
substr($to->content, $end, strlen($to->content))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getGroups(): array
|
||||
{
|
||||
$res = $group = [];
|
||||
$allowed_types = ['delete', 'insert'];
|
||||
$delete = $insert = 0;
|
||||
|
||||
foreach ($this->__data as $line) {
|
||||
if (in_array($line->type, $allowed_types)) {
|
||||
array_push($group, $line);
|
||||
${$line->type}++;
|
||||
} else {
|
||||
if ($delete === $insert && count($group) > 0) {
|
||||
array_push($res, $group);
|
||||
}
|
||||
$delete = $insert = 0;
|
||||
$group = [];
|
||||
}
|
||||
}
|
||||
if ($delete === $insert && count($group) > 0) {
|
||||
array_push($res, $group);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
private function getChangeExtent(string $str1, string $str2): array
|
||||
{
|
||||
$start = 0;
|
||||
$limit = min(strlen($str1), strlen($str2));
|
||||
while ($start < $limit && $str1[$start] === $str2[$start]) {
|
||||
$start++;
|
||||
}
|
||||
|
||||
$end = -1;
|
||||
$limit = $limit - $start;
|
||||
|
||||
while (-$end <= $limit && $str1[strlen($str1) + $end] === $str2[strlen($str2) + $end]) {
|
||||
$end--;
|
||||
}
|
||||
|
||||
return ['start' => $start, 'end' => $end + 1];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class tidyDiffLine
|
||||
* @brief TIDY diff line
|
||||
*
|
||||
* A diff line representation. Used by a TIDY chunk.
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Diff
|
||||
*/
|
||||
class tidyDiffLine
|
||||
{
|
||||
/**
|
||||
* Line type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* Line number for old and new context
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $lines;
|
||||
|
||||
/**
|
||||
* Line content
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Creates a line representation for a tidy chunk.
|
||||
*
|
||||
* @param string $type Tine type
|
||||
* @param array $lines Line number for old and new context
|
||||
* @param string $content Line content
|
||||
*/
|
||||
public function __construct(string $type, ?array $lines, ?string $content)
|
||||
{
|
||||
$allowed_type = ['context', 'delete', 'insert'];
|
||||
|
||||
if (in_array($type, $allowed_type) && is_array($lines) && is_string($content)) {
|
||||
$this->type = $type;
|
||||
$this->lines = $lines;
|
||||
$this->content = $content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic get
|
||||
*
|
||||
* Returns field content according to the given name, null otherwise.
|
||||
*
|
||||
* @param string $n Field name
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get(string $n)
|
||||
{
|
||||
return $this->{$n} ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite
|
||||
*
|
||||
* Overwrites content for the current line.
|
||||
*
|
||||
* @param string $content Line content
|
||||
*/
|
||||
public function overwrite(?string $content): void
|
||||
{
|
||||
if (is_string($content)) {
|
||||
$this->content = $content;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,729 @@
|
|||
<?php
|
||||
/**
|
||||
* @class filemanager
|
||||
* @brief Files management class
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Filemanager
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class filemanager
|
||||
{
|
||||
/**
|
||||
* Files manager root path
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $root;
|
||||
|
||||
/**
|
||||
* Files manager root URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $root_url;
|
||||
|
||||
/**
|
||||
* Working (current) directory
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $pwd;
|
||||
|
||||
/**
|
||||
* Array of regexps defining excluded items
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $exclude_list = [];
|
||||
|
||||
/**
|
||||
* Files exclusion regexp pattern
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $exclude_pattern = '';
|
||||
|
||||
/**
|
||||
* Current directory content array
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $dir = [
|
||||
'dirs' => [],
|
||||
'files' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* New filemanage istance. Note that filemanage is a jail in given root
|
||||
* path. You won't be able to access files outside {@link $root} path with
|
||||
* the object's methods.
|
||||
*
|
||||
* @param string $root Root path
|
||||
* @param string $root_url Root URL
|
||||
*/
|
||||
public function __construct(?string $root, ?string $root_url = '')
|
||||
{
|
||||
$this->root = $this->pwd = path::real($root);
|
||||
$this->root_url = $root_url;
|
||||
|
||||
if (!preg_match('#/$#', (string) $this->root_url)) {
|
||||
$this->root_url = $this->root_url . '/';
|
||||
}
|
||||
|
||||
if (!$this->root) {
|
||||
throw new Exception('Invalid root directory.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change directory
|
||||
*
|
||||
* Changes working directory. $dir is relative to instance {@link $root}
|
||||
* directory.
|
||||
*
|
||||
* @param string $dir Directory
|
||||
*/
|
||||
public function chdir(?string $dir): void
|
||||
{
|
||||
$realdir = path::real($this->root . '/' . path::clean($dir));
|
||||
if (!$realdir || !is_dir($realdir)) {
|
||||
throw new Exception('Invalid directory.');
|
||||
}
|
||||
|
||||
if ($this->isExclude($realdir)) {
|
||||
throw new Exception('Directory is excluded.');
|
||||
}
|
||||
|
||||
$this->pwd = $realdir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get working directory
|
||||
*
|
||||
* Returns working directory path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPwd(): string
|
||||
{
|
||||
return (string) $this->pwd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current directory is writable
|
||||
*
|
||||
* @return bool true if working directory is writable
|
||||
*/
|
||||
public function writable(): bool
|
||||
{
|
||||
if (!$this->pwd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return is_writable($this->pwd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add exclusion
|
||||
*
|
||||
* Appends an exclusion to exclusions list. $f should be a regexp.
|
||||
*
|
||||
* @see $exclude_list
|
||||
*
|
||||
* @param array|string $list Exclusion regexp
|
||||
*/
|
||||
public function addExclusion($list): void
|
||||
{
|
||||
if (is_array($list)) {
|
||||
foreach ($list as $item) {
|
||||
if (($res = path::real($item)) !== false) {
|
||||
$this->exclude_list[] = $res;
|
||||
}
|
||||
}
|
||||
} elseif (($res = path::real($list)) !== false) {
|
||||
$this->exclude_list[] = $res;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Path is excluded
|
||||
*
|
||||
* Returns true if path (file or directory) $path is excluded. $path is
|
||||
* relative to {@link $root} path.
|
||||
*
|
||||
* @see $exclude_list
|
||||
*
|
||||
* @param string $path Path to match
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isExclude(string $path): bool
|
||||
{
|
||||
foreach ($this->exclude_list as $item) {
|
||||
if (strpos($path, $item) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* File is excluded
|
||||
*
|
||||
* Returns true if file $file is excluded. $file is relative to {@link $root}
|
||||
* path.
|
||||
*
|
||||
* @see $exclude_pattern
|
||||
*
|
||||
* @param string $file File to match
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isFileExclude(string $file): bool
|
||||
{
|
||||
if (!$this->exclude_pattern) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return preg_match($this->exclude_pattern, $file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Item in jail
|
||||
*
|
||||
* Returns true if file or directory $path is in jail (ie. not outside the {@link $root} directory).
|
||||
*
|
||||
* @param string $path Path to match
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function inJail(string $path): bool
|
||||
{
|
||||
$path = path::real($path);
|
||||
|
||||
if ($path !== false) {
|
||||
return preg_match('|^' . preg_quote($this->root, '|') . '|', $path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* File in files
|
||||
*
|
||||
* Returns true if file $file is in files array of {@link $dir}.
|
||||
*
|
||||
* @param string $file File to match
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function inFiles(string $file): bool
|
||||
{
|
||||
foreach ($this->dir['files'] as $item) {
|
||||
if ($item->relname === $file) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory list
|
||||
*
|
||||
* Creates list of items in working directory and append it to {@link $dir}
|
||||
*
|
||||
* @uses sortHandler(), fileItem
|
||||
*/
|
||||
public function getDir(): void
|
||||
{
|
||||
$dir = path::clean($this->pwd);
|
||||
|
||||
$handle = @opendir($dir);
|
||||
if ($handle === false) {
|
||||
throw new Exception('Unable to read directory.');
|
||||
}
|
||||
|
||||
$directories = $files = [];
|
||||
|
||||
while (($file = readdir($handle)) !== false) {
|
||||
$filename = $dir . '/' . $file;
|
||||
|
||||
if ($this->inJail($filename) && !$this->isExclude($filename)) {
|
||||
if (is_dir($filename) && $file !== '.') {
|
||||
$directory = new fileItem($filename, $this->root, $this->root_url);
|
||||
if ($file === '..') {
|
||||
$directory->parent = true;
|
||||
}
|
||||
$directories[] = $directory;
|
||||
}
|
||||
|
||||
if (is_file($filename) && strpos($file, '.') !== 0 && !$this->isFileExclude($file)) {
|
||||
$files[] = new fileItem($filename, $this->root, $this->root_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
|
||||
$this->dir = [
|
||||
'dirs' => $directories,
|
||||
'files' => $files,
|
||||
];
|
||||
|
||||
usort($this->dir['dirs'], [$this, 'sortHandler']);
|
||||
usort($this->dir['files'], [$this, 'sortHandler']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Root directories
|
||||
*
|
||||
* Returns an array of directory under {@link $root} directory.
|
||||
*
|
||||
* @uses fileItem
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRootDirs(): array
|
||||
{
|
||||
$directories = files::getDirList($this->root);
|
||||
|
||||
$res = [];
|
||||
foreach ($directories['dirs'] as $directory) {
|
||||
$res[] = new fileItem($directory, $this->root, $this->root_url);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file
|
||||
*
|
||||
* Move <var>$tmp</var> file to its final destination <var>$dest</var> and
|
||||
* returns the destination file path.
|
||||
*
|
||||
* <var>$dest</var> should be in jail. This method will throw exception
|
||||
* if the file cannot be written.
|
||||
*
|
||||
* You should first verify upload status, with {@link files::uploadStatus()}
|
||||
* or PHP native functions.
|
||||
*
|
||||
* @see files::uploadStatus()
|
||||
*
|
||||
* @param string $tmp Temporary uploaded file path
|
||||
* @param string $dest Destination file
|
||||
* @param bool $overwrite Overwrite mode
|
||||
*
|
||||
* @return string Destination real path
|
||||
*/
|
||||
public function uploadFile(string $tmp, string $dest, bool $overwrite = false)
|
||||
{
|
||||
$dest = $this->pwd . '/' . path::clean($dest);
|
||||
|
||||
if ($this->isFileExclude($dest)) {
|
||||
throw new Exception(__('Uploading this file is not allowed.'));
|
||||
}
|
||||
|
||||
if (!$this->inJail(dirname($dest))) {
|
||||
throw new Exception(__('Destination directory is not in jail.'));
|
||||
}
|
||||
|
||||
if (!$overwrite && file_exists($dest)) {
|
||||
throw new Exception(__('File already exists.'));
|
||||
}
|
||||
|
||||
if (!is_writable(dirname($dest))) {
|
||||
throw new Exception(__('Cannot write in this directory.'));
|
||||
}
|
||||
|
||||
if (@move_uploaded_file($tmp, $dest) === false) {
|
||||
throw new Exception(__('An error occurred while writing the file.'));
|
||||
}
|
||||
|
||||
files::inheritChmod($dest);
|
||||
|
||||
return (string) path::real($dest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file by bits
|
||||
*
|
||||
* Creates a new file <var>$name</var> with contents of <var>$bits</var> and
|
||||
* return the destination file path.
|
||||
*
|
||||
* <var>$name</var> should be in jail. This method will throw exception
|
||||
* if file cannot be written.
|
||||
*
|
||||
* @param string $name Destination file
|
||||
* @param string $bits Destination file content
|
||||
*
|
||||
* @return string Destination real path
|
||||
*/
|
||||
public function uploadBits(string $name, string $bits): string
|
||||
{
|
||||
$dest = $this->pwd . '/' . path::clean($name);
|
||||
|
||||
if ($this->isFileExclude($dest)) {
|
||||
throw new Exception(__('Uploading this file is not allowed.'));
|
||||
}
|
||||
|
||||
if (!$this->inJail(dirname($dest))) {
|
||||
throw new Exception(__('Destination directory is not in jail.'));
|
||||
}
|
||||
|
||||
if (!is_writable(dirname($dest))) {
|
||||
throw new Exception(__('Cannot write in this directory.'));
|
||||
}
|
||||
|
||||
$fp = @fopen($dest, 'wb');
|
||||
if ($fp === false) {
|
||||
throw new Exception(__('An error occurred while writing the file.'));
|
||||
}
|
||||
|
||||
fwrite($fp, $bits);
|
||||
fclose($fp);
|
||||
files::inheritChmod($dest);
|
||||
|
||||
return (string) path::real($dest);
|
||||
}
|
||||
|
||||
/**
|
||||
* New directory
|
||||
*
|
||||
* Creates a new directory relative to working directory.
|
||||
*
|
||||
* @param string $name Directory name
|
||||
*/
|
||||
public function makeDir(?string $name): void
|
||||
{
|
||||
files::makeDir($this->pwd . '/' . path::clean($name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Move file
|
||||
*
|
||||
* Moves a file to a new destination. Both paths are relative to {@link $root}.
|
||||
*
|
||||
* @param string $src_path Source file path
|
||||
* @param string $dst_path Destination file path
|
||||
*/
|
||||
public function moveFile(?string $src_path, ?string $dst_path): void
|
||||
{
|
||||
$src_path = $this->root . '/' . path::clean($src_path);
|
||||
$dst_path = $this->root . '/' . path::clean($dst_path);
|
||||
|
||||
if (($src_path = path::real($src_path)) === false) {
|
||||
throw new Exception(__('Source file does not exist.'));
|
||||
}
|
||||
|
||||
$dest_dir = path::real(dirname($dst_path));
|
||||
|
||||
if (!$this->inJail($src_path)) {
|
||||
throw new Exception(__('File is not in jail.'));
|
||||
}
|
||||
if (!$this->inJail($dest_dir)) {
|
||||
throw new Exception(__('File is not in jail.'));
|
||||
}
|
||||
|
||||
if (!is_writable($dest_dir)) {
|
||||
throw new Exception(__('Destination directory is not writable.'));
|
||||
}
|
||||
|
||||
if (@rename($src_path, $dst_path) === false) {
|
||||
throw new Exception(__('Unable to rename file.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item
|
||||
*
|
||||
* Removes a file or directory which is relative to working directory.
|
||||
*
|
||||
* @param string $name Item to remove
|
||||
*/
|
||||
public function removeItem(?string $name): void
|
||||
{
|
||||
$file = (string) path::real($this->pwd . '/' . path::clean($name));
|
||||
|
||||
if (is_file($file)) {
|
||||
$this->removeFile($name);
|
||||
} elseif (is_dir($file)) {
|
||||
$this->removeDir($name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item
|
||||
*
|
||||
* Removes a file which is relative to working directory.
|
||||
*
|
||||
* @param string $file File to remove
|
||||
*/
|
||||
public function removeFile(?string $file): void
|
||||
{
|
||||
$path = (string) path::real($this->pwd . '/' . path::clean($file));
|
||||
|
||||
if (!$this->inJail($path)) {
|
||||
throw new Exception(__('File is not in jail.'));
|
||||
}
|
||||
|
||||
if (!files::isDeletable($path)) {
|
||||
throw new Exception(__('File cannot be removed.'));
|
||||
}
|
||||
|
||||
if (@unlink($path) === false) {
|
||||
throw new Exception(__('File cannot be removed.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item
|
||||
*
|
||||
* Removes a directory which is relative to working directory.
|
||||
*
|
||||
* @param string $directory Directory to remove
|
||||
*/
|
||||
public function removeDir(?string $directory): void
|
||||
{
|
||||
$path = (string) path::real($this->pwd . '/' . path::clean($directory));
|
||||
|
||||
if (!$this->inJail($path)) {
|
||||
throw new Exception(__('Directory is not in jail.'));
|
||||
}
|
||||
|
||||
if (!files::isDeletable($path)) {
|
||||
throw new Exception(__('Directory cannot be removed.'));
|
||||
}
|
||||
|
||||
if (@rmdir($path) === false) {
|
||||
throw new Exception(__('Directory cannot be removed.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SortHandler
|
||||
*
|
||||
* This method is called by {@link getDir()} to sort files. Can be overrided
|
||||
* in inherited classes.
|
||||
*
|
||||
* @param fileItem $a fileItem object
|
||||
* @param fileItem $b fileItem object
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function sortHandler(fileItem $a, fileItem $b): int
|
||||
{
|
||||
if ($a->parent && !$b->parent || !$a->parent && $b->parent) {
|
||||
return ($a->parent) ? -1 : 1;
|
||||
}
|
||||
|
||||
return strcasecmp($a->basename, $b->basename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class fileItem
|
||||
* @brief File item
|
||||
*
|
||||
* File item class used by {@link filemanager}. In this class {@link $file} could
|
||||
* be either a file or a directory.
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Filemanager
|
||||
*/
|
||||
class fileItem
|
||||
{
|
||||
/**
|
||||
* Complete path to file
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $file;
|
||||
|
||||
/**
|
||||
* File basename
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $basename;
|
||||
|
||||
/**
|
||||
* File directory name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $dir;
|
||||
|
||||
/**
|
||||
* File URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $file_url;
|
||||
|
||||
/**
|
||||
* File directory URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $dir_url;
|
||||
|
||||
/**
|
||||
* File extension
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $extension;
|
||||
|
||||
/**
|
||||
* File path relative to <var>$root</var> given in constructor
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $relname;
|
||||
|
||||
/**
|
||||
* Parent directory (ie. "..")
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $parent = false;
|
||||
|
||||
/**
|
||||
* File MimeType
|
||||
*
|
||||
* @see {@link files::getMimeType()}
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* File MimeType prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $type_prefix;
|
||||
|
||||
/**
|
||||
* File modification timestamp
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $mtime;
|
||||
|
||||
/**
|
||||
* File size
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $size;
|
||||
|
||||
/**
|
||||
* File permissions mode
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $mode;
|
||||
|
||||
/**
|
||||
* File owner ID
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $uid;
|
||||
|
||||
/**
|
||||
* File group ID
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $gid;
|
||||
|
||||
/**
|
||||
* True if file or directory is writable
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $w;
|
||||
|
||||
/**
|
||||
* True if file is a directory
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $d;
|
||||
|
||||
/**
|
||||
* True if file file is executable or directory is traversable
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $x;
|
||||
|
||||
/**
|
||||
* True if file is a file
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $f;
|
||||
|
||||
/**
|
||||
* True if file or directory is deletable
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $del;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Creates an instance of fileItem object.
|
||||
*
|
||||
* @param string $file Absolute file or directory path
|
||||
* @param string $root File root path
|
||||
* @param string $root_url File root URL
|
||||
*/
|
||||
public function __construct(string $file, ?string $root, ?string $root_url = '')
|
||||
{
|
||||
$file = path::real($file);
|
||||
$stat = stat($file);
|
||||
$path = path::info($file);
|
||||
|
||||
$rel = preg_replace('/^' . preg_quote($root, '/') . '\/?/', '', (string) $file);
|
||||
|
||||
// Properties
|
||||
$this->file = $file;
|
||||
$this->basename = $path['basename'];
|
||||
$this->dir = $path['dirname'];
|
||||
$this->relname = $rel;
|
||||
|
||||
// URL
|
||||
$this->file_url = $root_url . str_replace('%2F', '/', rawurlencode($rel));
|
||||
$this->dir_url = dirname($this->file_url);
|
||||
|
||||
// File type
|
||||
$this->extension = $path['extension'];
|
||||
$this->type = $this->d ? null : files::getMimeType($file);
|
||||
$this->type_prefix = preg_replace('/^(.+?)\/.+$/', '$1', (string) $this->type);
|
||||
|
||||
// Filesystem infos
|
||||
$this->mtime = $stat[9];
|
||||
$this->size = $stat[7];
|
||||
$this->mode = $stat[2];
|
||||
$this->uid = $stat[4];
|
||||
$this->gid = $stat[5];
|
||||
|
||||
// Flags
|
||||
$this->w = is_writable($file);
|
||||
$this->d = is_dir($file);
|
||||
$this->f = is_file($file);
|
||||
$this->x = $this->d ? file_exists($file . '/.') : false;
|
||||
$this->del = files::isDeletable($file);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,934 @@
|
|||
<?php
|
||||
/**
|
||||
* @class htmlFilter
|
||||
* @brief HTML code filter
|
||||
*
|
||||
* This class removes all unwanted tags and attributes from an HTML string.
|
||||
*
|
||||
* This was inspired by Ulf Harnhammar's Kses (http://sourceforge.net/projects/kses)
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage HTML
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class htmlFilter
|
||||
{
|
||||
/**
|
||||
* Parser handle
|
||||
*
|
||||
* @var mixed resource|XMLParser
|
||||
*/
|
||||
private $parser;
|
||||
|
||||
/**
|
||||
* HTML content
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $content;
|
||||
|
||||
/**
|
||||
* Current tag
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $tag;
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param bool $keep_aria Keep aria attributes
|
||||
* @param bool $keep_data Keep data elements
|
||||
* @param bool $keep_js Keep javascript elements
|
||||
*/
|
||||
public function __construct(bool $keep_aria = false, bool $keep_data = false, bool $keep_js = false)
|
||||
{
|
||||
$this->parser = xml_parser_create('UTF-8');
|
||||
xml_set_object($this->parser, $this);
|
||||
xml_set_element_handler($this->parser, [$this, 'tag_open'], [$this, 'tag_close']);
|
||||
xml_set_character_data_handler($this->parser, [$this, 'cdata']);
|
||||
xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, 0);
|
||||
|
||||
$this->removeTags(
|
||||
'applet',
|
||||
'base',
|
||||
'basefont',
|
||||
'body',
|
||||
'center',
|
||||
'dir',
|
||||
'font',
|
||||
'frame',
|
||||
'frameset',
|
||||
'head',
|
||||
'html',
|
||||
'isindex',
|
||||
'link',
|
||||
'menu',
|
||||
'menuitem',
|
||||
'meta',
|
||||
'noframes',
|
||||
'script',
|
||||
'noscript',
|
||||
'style'
|
||||
);
|
||||
|
||||
// Remove aria-* and data-* attributes if necessary (tidy extension does it, not ready for HTML5)
|
||||
if (!$keep_aria) {
|
||||
$this->removePatternAttributes('^aria-[\-\w]+$');
|
||||
$this->removeAttributes('role');
|
||||
}
|
||||
if (!$keep_data) {
|
||||
$this->removePatternAttributes('^data-[\-\w].*$');
|
||||
}
|
||||
|
||||
if (!$keep_js) {
|
||||
// Remove events attributes
|
||||
$this->removeArrayAttributes($this->event_attrs);
|
||||
// Remove inline JS in URI
|
||||
$this->removeHosts('javascript');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append hosts
|
||||
*
|
||||
* Appends hosts to remove from URI. Each method argument is a host. Example:
|
||||
*
|
||||
* <code>
|
||||
* <?php
|
||||
* $filter = new htmlFilter();
|
||||
* $filter->removeHosts('javascript');
|
||||
* ?>
|
||||
* </code>
|
||||
*
|
||||
* @param mixed ...$args The arguments
|
||||
*/
|
||||
public function removeHosts(...$args): void
|
||||
{
|
||||
foreach ($this->argsArray([...$args]) as $host) {
|
||||
$this->removed_hosts[] = $host;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append tags
|
||||
*
|
||||
* Appends tags to remove. Each method argument is a tag. Example:
|
||||
*
|
||||
* <code>
|
||||
* <?php
|
||||
* $filter = new htmlFilter();
|
||||
* $filter->removeTags('frame','script');
|
||||
* ?>
|
||||
* </code>
|
||||
*
|
||||
* @param mixed ...$args The arguments
|
||||
*/
|
||||
public function removeTags(...$args): void
|
||||
{
|
||||
foreach ($this->argsArray([...$args]) as $tag) {
|
||||
$this->removed_tags[] = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append attributes
|
||||
*
|
||||
* Appends attributes to remove. Each method argument is an attribute. Example:
|
||||
*
|
||||
* <code>
|
||||
* <?php
|
||||
* $filter = new htmlFilter();
|
||||
* $filter->removeAttributes('onclick','onunload');
|
||||
* ?>
|
||||
* </code>
|
||||
*
|
||||
* @param mixed ...$args The arguments
|
||||
*/
|
||||
public function removeAttributes(...$args): void
|
||||
{
|
||||
foreach ($this->argsArray([...$args]) as $a) {
|
||||
$this->removed_attrs[] = $a;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append array of attributes
|
||||
*
|
||||
* Appends attributes to remove. Example:
|
||||
*
|
||||
* <code>
|
||||
* <?php
|
||||
* $filter = new htmlFilter();
|
||||
* $filter->removeAttributes(['onload','onerror']);
|
||||
* ?>
|
||||
* </code>
|
||||
*
|
||||
* @param array $attrs The attributes
|
||||
*/
|
||||
public function removeArrayAttributes(array $attrs): void
|
||||
{
|
||||
foreach ($attrs as $a) {
|
||||
$this->removed_attrs[] = $a;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append attribute patterns
|
||||
*
|
||||
* Appends attribute patterns to remove. Each method argument is an attribute pattern. Example:
|
||||
*
|
||||
* <code>
|
||||
* <?php
|
||||
* $filter = new htmlFilter();
|
||||
* $filter->removeAttributes('data-.*');
|
||||
* ?>
|
||||
* </code>
|
||||
*
|
||||
* @param mixed ...$args The arguments
|
||||
*/
|
||||
public function removePatternAttributes(...$args): void
|
||||
{
|
||||
foreach ($this->argsArray([...$args]) as $a) {
|
||||
$this->removed_pattern_attrs[] = $a;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append attributes for tags
|
||||
*
|
||||
* Appends attributes to remove from specific tags. Each method argument is
|
||||
* an array of tags with attributes. Example:
|
||||
*
|
||||
* <code>
|
||||
* <?php
|
||||
* $filter = new htmlFilter();
|
||||
* $filter->removeTagAttributes(['a' => ['src','title']]);
|
||||
* ?>
|
||||
* </code>
|
||||
*
|
||||
* @param string $tag The tag
|
||||
* @param mixed ...$args The arguments
|
||||
*/
|
||||
public function removeTagAttributes(string $tag, ...$args): void
|
||||
{
|
||||
foreach ($this->argsArray([...$args]) as $a) {
|
||||
$this->removed_tag_attrs[$tag][] = $a;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Known tags
|
||||
*
|
||||
* Creates a list of known tags.
|
||||
*
|
||||
* @param array $tags Tags array
|
||||
*/
|
||||
public function setTags(array $tags): void
|
||||
{
|
||||
if (is_array($tags)) {
|
||||
$this->tags = $tags;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filter
|
||||
*
|
||||
* This method applies filter on given <var>$str</var> string. It will first
|
||||
* try to use tidy extension if exists and then apply the filter.
|
||||
*
|
||||
* @param string $str String to filter
|
||||
* @param boolean $tidy Use tidy extension if present
|
||||
*
|
||||
* @return string Filtered string
|
||||
*/
|
||||
public function apply(string $str, bool $tidy = true): string
|
||||
{
|
||||
if ($tidy && extension_loaded('tidy') && class_exists('tidy')) {
|
||||
$config = [
|
||||
'doctype' => 'strict',
|
||||
'drop-proprietary-attributes' => true,
|
||||
'escape-cdata' => true,
|
||||
'indent' => false,
|
||||
'join-classes' => false,
|
||||
'join-styles' => true,
|
||||
'lower-literals' => true,
|
||||
'output-xhtml' => true,
|
||||
'show-body-only' => true,
|
||||
'wrap' => 80,
|
||||
];
|
||||
|
||||
$str = '<p>tt</p>' . $str; // Fixes a big issue
|
||||
|
||||
$tidy = new tidy();
|
||||
$tidy->parseString($str, $config, 'utf8');
|
||||
$tidy->cleanRepair();
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
$str = (string) $tidy;
|
||||
|
||||
$str = preg_replace('#^<p>tt</p>\s?#', '', $str);
|
||||
} else {
|
||||
$str = $this->miniTidy($str);
|
||||
}
|
||||
|
||||
# Removing open comments, open CDATA and processing instructions
|
||||
$str = preg_replace('%<!--.*?-->%msu', '', $str);
|
||||
$str = str_replace('<!--', '', $str);
|
||||
$str = preg_replace('%<!\[CDATA\[.*?\]\]>%msu', '', $str);
|
||||
$str = str_replace('<![CDATA[', '', $str);
|
||||
|
||||
# Transform processing instructions
|
||||
$str = str_replace('<?', '>?', $str);
|
||||
$str = str_replace('?>', '?<', $str);
|
||||
|
||||
$str = html::decodeEntities($str, true);
|
||||
|
||||
$this->content = '';
|
||||
xml_parse($this->parser, '<all>' . $str . '</all>');
|
||||
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mini Tidy, used if tidy extension is not loaded (see above)
|
||||
*
|
||||
* @param string $str The string
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function miniTidy(string $str)
|
||||
{
|
||||
return preg_replace_callback('%(<(?!(\s*?/|!)).*?>)%msu', [$this, 'miniTidyFixTag'], $str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag (with its attributes) helper for miniTidy(), see above
|
||||
*
|
||||
* @param array $match The match
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function miniTidyFixTag(array $match)
|
||||
{
|
||||
return preg_replace_callback('%(=")(.*?)(")%msu', [$this, 'miniTidyFixAttr'], $match[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute (with its value) helper for miniTidyFixTag(), see above
|
||||
*
|
||||
* Escape entities in attributes value
|
||||
*
|
||||
* @param array $match The match
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function miniTidyFixAttr(array $match): string
|
||||
{
|
||||
return $match[1] . html::escapeHTML(html::decodeEntities($match[2])) . $match[3];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a (almost) flatten and cleaned array
|
||||
*
|
||||
* @param array $args The arguments
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function argsArray(array $args): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($args as $arg) {
|
||||
if (is_array($arg)) {
|
||||
$result = array_merge($result, $arg);
|
||||
} else {
|
||||
$result[] = (string) $arg;
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* xml_set_element_handler() open tag handler
|
||||
*
|
||||
* @param mixed $parser The parser (resource|XMLParser)
|
||||
* @param string $tag The tag
|
||||
* @param array $attrs The attributes
|
||||
*/
|
||||
private function tag_open($parser, string $tag, array $attrs): void
|
||||
{
|
||||
$this->tag = strtolower($tag);
|
||||
|
||||
if ($this->tag == 'all') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->allowedTag($this->tag)) {
|
||||
$this->content .= '<' . $tag . $this->getAttrs($tag, $attrs);
|
||||
|
||||
if (in_array($this->tag, $this->single_tags)) {
|
||||
$this->content .= ' />';
|
||||
} else {
|
||||
$this->content .= '>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* xml_set_element_handler() close tag handler
|
||||
*
|
||||
* @param mixed $parser The parser (resource|XMLParser)
|
||||
* @param string $tag The tag
|
||||
*/
|
||||
private function tag_close($parser, string $tag): void
|
||||
{
|
||||
if (!in_array($tag, $this->single_tags) && $this->allowedTag($tag)) {
|
||||
$this->content .= '</' . $tag . '>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* xml_set_character_data_handler() data handler
|
||||
*
|
||||
* @param mixed $parser The parser (resource|XMLParser)
|
||||
* @param string $cdata The cdata
|
||||
*/
|
||||
private function cdata($parser, string $cdata): void
|
||||
{
|
||||
$this->content .= html::escapeHTML($cdata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the allowed attributes.
|
||||
*
|
||||
* @param string $tag The tag
|
||||
* @param array $attrs The attributes
|
||||
*
|
||||
* @return string The attributes.
|
||||
*/
|
||||
private function getAttrs(string $tag, array $attrs): string
|
||||
{
|
||||
$res = '';
|
||||
foreach ($attrs as $n => $v) {
|
||||
if ($this->allowedAttr($tag, $n)) {
|
||||
$res .= $this->getAttr($n, $v);
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attribute with its value.
|
||||
*
|
||||
* @param string $attr The attribute
|
||||
* @param string $value The value
|
||||
*
|
||||
* @return string The attribute.
|
||||
*/
|
||||
private function getAttr(string $attr, string $value): string
|
||||
{
|
||||
$value = preg_replace('/\xad+/', '', $value);
|
||||
|
||||
if (in_array($attr, $this->uri_attrs)) {
|
||||
$value = $this->getURI($value);
|
||||
}
|
||||
|
||||
return ' ' . $attr . '="' . html::escapeHTML($value) . '"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize an URI value
|
||||
*
|
||||
* @param string $uri The uri
|
||||
*
|
||||
* @return string The uri.
|
||||
*/
|
||||
private function getURI(string $uri): string
|
||||
{
|
||||
// Trim URI
|
||||
$uri = trim($uri);
|
||||
// Remove escaped Unicode characters
|
||||
$uri = preg_replace('/\\\u[a-fA-F0-9]{4}/', '', $uri);
|
||||
// Sanitize and parse URL
|
||||
$uri = filter_var($uri, FILTER_SANITIZE_URL);
|
||||
$u = @parse_url($uri);
|
||||
|
||||
if (is_array($u) && (empty($u['scheme']) || in_array($u['scheme'], $this->allowed_schemes))) {
|
||||
if (empty($u['host']) || (!in_array($u['host'], $this->removed_hosts))) {
|
||||
return $uri;
|
||||
}
|
||||
}
|
||||
|
||||
return '#';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tag is allowed
|
||||
*
|
||||
* @param string $tag The tag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function allowedTag(string $tag): bool
|
||||
{
|
||||
return !in_array($tag, $this->removed_tags) && isset($this->tags[$tag]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tag's attribute is allowed
|
||||
*
|
||||
* @param string $tag The tag
|
||||
* @param string $attr The attribute
|
||||
*
|
||||
* @return bool ( description_of_the_return_value )
|
||||
*/
|
||||
private function allowedAttr(string $tag, string $attr): bool
|
||||
{
|
||||
if (in_array($attr, $this->removed_attrs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($this->removed_tag_attrs[$tag]) && in_array($attr, $this->removed_tag_attrs[$tag])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($this->tags[$tag]) || (!in_array($attr, $this->tags[$tag]) && !in_array($attr, $this->gen_attrs) && !in_array($attr, $this->event_attrs) && !$this->allowedPatternAttr($attr))) {
|
||||
// Not in tag allowed attributes and
|
||||
// Not in allowed generic attributes and
|
||||
// Not in allowed event attributes and
|
||||
// Not in allowed grep attributes
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tag's attribute is in allowed grep attributes
|
||||
*
|
||||
* @param string $attr The attribute
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function allowedPatternAttr(string $attr): bool
|
||||
{
|
||||
foreach ($this->removed_pattern_attrs as $pattern) {
|
||||
if (preg_match('/' . $pattern . '/u', $attr)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
foreach ($this->grep_attrs as $pattern) {
|
||||
if (preg_match('/' . $pattern . '/u', $attr)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Tags and attributes definitions
|
||||
* Source: https://developer.mozilla.org/fr/docs/Web/HTML/
|
||||
------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Stack of removed tags
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $removed_tags = [];
|
||||
|
||||
/**
|
||||
* Stack of removed attributes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $removed_attrs = [];
|
||||
|
||||
/**
|
||||
* Stack of removed attibutes (via pattern)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $removed_pattern_attrs = [];
|
||||
|
||||
/**
|
||||
* Stack of removed tags' attributes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $removed_tag_attrs = [];
|
||||
|
||||
/**
|
||||
* Stack of removed hosts
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $removed_hosts = [];
|
||||
|
||||
/**
|
||||
* List of allowed schemes (URI)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $allowed_schemes = [
|
||||
'data',
|
||||
'http',
|
||||
'https',
|
||||
'ftp',
|
||||
'mailto',
|
||||
'news',
|
||||
];
|
||||
|
||||
/**
|
||||
* List of attributes which allow URI value
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $uri_attrs = [
|
||||
'action',
|
||||
'background',
|
||||
'cite',
|
||||
'classid',
|
||||
'code',
|
||||
'codebase',
|
||||
'data',
|
||||
'download',
|
||||
'formaction',
|
||||
'href',
|
||||
'longdesc',
|
||||
'profile',
|
||||
'src',
|
||||
'usemap',
|
||||
];
|
||||
|
||||
/**
|
||||
* List of generic attributes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $gen_attrs = [
|
||||
'accesskey',
|
||||
'class',
|
||||
'contenteditable',
|
||||
'contextmenu',
|
||||
'dir',
|
||||
'draggable',
|
||||
'dropzone',
|
||||
'hidden',
|
||||
'id',
|
||||
'itemid',
|
||||
'itemprop',
|
||||
'itemref',
|
||||
'itemscope',
|
||||
'itemtype',
|
||||
'lang',
|
||||
'role',
|
||||
'slot',
|
||||
'spellcheck',
|
||||
'style',
|
||||
'tabindex',
|
||||
'title',
|
||||
'translate',
|
||||
'xml:base',
|
||||
'xml:lang', ];
|
||||
|
||||
/**
|
||||
* List of events attributes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $event_attrs = [
|
||||
'onabort',
|
||||
'onafterprint',
|
||||
'onautocomplete',
|
||||
'onautocompleteerror',
|
||||
'onbeforeprint',
|
||||
'onbeforeunload',
|
||||
'onblur',
|
||||
'oncancel',
|
||||
'oncanplay',
|
||||
'oncanplaythrough',
|
||||
'onchange',
|
||||
'onclick',
|
||||
'onclose',
|
||||
'oncontextmenu',
|
||||
'oncuechange',
|
||||
'ondblclick',
|
||||
'ondrag',
|
||||
'ondragend',
|
||||
'ondragenter',
|
||||
'ondragexit',
|
||||
'ondragleave',
|
||||
'ondragover',
|
||||
'ondragstart',
|
||||
'ondrop',
|
||||
'ondurationchange',
|
||||
'onemptied',
|
||||
'onended',
|
||||
'onerror',
|
||||
'onfocus',
|
||||
'onhashchange',
|
||||
'oninput',
|
||||
'oninvalid',
|
||||
'onkeydown',
|
||||
'onkeypress',
|
||||
'onkeyup',
|
||||
'onlanguagechange',
|
||||
'onload',
|
||||
'onloadeddata',
|
||||
'onloadedmetadata',
|
||||
'onloadstart',
|
||||
'onmessage',
|
||||
'onmousedown',
|
||||
'onmouseenter',
|
||||
'onmouseleave',
|
||||
'onmousemove',
|
||||
'onmouseout',
|
||||
'onmouseover',
|
||||
'onmouseup',
|
||||
'onmousewheel',
|
||||
'onoffline',
|
||||
'ononline',
|
||||
'onpause',
|
||||
'onplay',
|
||||
'onplaying',
|
||||
'onpopstate',
|
||||
'onprogress',
|
||||
'onratechange',
|
||||
'onredo',
|
||||
'onreset',
|
||||
'onresize',
|
||||
'onscroll',
|
||||
'onseeked',
|
||||
'onseeking',
|
||||
'onselect',
|
||||
'onshow',
|
||||
'onsort',
|
||||
'onstalled',
|
||||
'onstorage',
|
||||
'onsubmit',
|
||||
'onsuspend',
|
||||
'ontimeupdate',
|
||||
'ontoggle',
|
||||
'onundo',
|
||||
'onunload',
|
||||
'onvolumechange',
|
||||
'onwaiting',
|
||||
];
|
||||
|
||||
/**
|
||||
* List of pattern'ed attributes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $grep_attrs = [
|
||||
'^aria-[\-\w]+$',
|
||||
'^data-[\-\w].*$',
|
||||
];
|
||||
|
||||
/**
|
||||
* List of single tags
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $single_tags = [
|
||||
'area',
|
||||
'base',
|
||||
'basefont',
|
||||
'br',
|
||||
'col',
|
||||
'embed',
|
||||
'frame',
|
||||
'hr',
|
||||
'img',
|
||||
'input',
|
||||
'isindex',
|
||||
'keygen',
|
||||
'link',
|
||||
'meta',
|
||||
'param',
|
||||
'source',
|
||||
'track',
|
||||
'wbr',
|
||||
];
|
||||
|
||||
/**
|
||||
* List of tags and their attributes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $tags = [
|
||||
// A
|
||||
'a' => ['charset', 'coords', 'download', 'href', 'hreflang', 'name', 'ping', 'referrerpolicy',
|
||||
'rel', 'rev', 'shape', 'target', 'type', ],
|
||||
'abbr' => [],
|
||||
'acronym' => [],
|
||||
'address' => [],
|
||||
'applet' => ['align', 'alt', 'archive', 'code', 'codebase', 'datafld', 'datasrc', 'height', 'hspace',
|
||||
'mayscript', 'name', 'object', 'vspace', 'width', ],
|
||||
'area' => ['alt', 'coords', 'download', 'href', 'name', 'media', 'nohref', 'referrerpolicy', 'rel',
|
||||
'shape', 'target', 'type', ],
|
||||
'article' => [],
|
||||
'aside' => [],
|
||||
'audio' => ['autoplay', 'buffered', 'controls', 'loop', 'muted', 'played', 'preload', 'src', 'volume'],
|
||||
// B
|
||||
'b' => [],
|
||||
'base' => ['href', 'target'],
|
||||
'basefont' => ['color', 'face', 'size'],
|
||||
'bdi' => [],
|
||||
'bdo' => [],
|
||||
'big' => [],
|
||||
'blockquote' => ['cite'],
|
||||
'body' => ['alink', 'background', 'bgcolor', 'bottommargin', 'leftmargin', 'link', 'text', 'rightmargin',
|
||||
'text', 'topmargin', 'vlink', ],
|
||||
'br' => ['clear'],
|
||||
'button' => ['autofocus', 'autocomplete', 'disabled', 'form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'name', 'type', 'value'],
|
||||
// C
|
||||
'canvas' => ['height', 'width'],
|
||||
'caption' => ['align'],
|
||||
'center' => [],
|
||||
'cite' => [],
|
||||
'code' => [],
|
||||
'col' => ['align', 'bgcolor', 'char', 'charoff', 'span', 'valign', 'width'],
|
||||
'colgroup' => ['align', 'bgcolor', 'char', 'charoff', 'span', 'valign', 'width'],
|
||||
// D
|
||||
'data' => ['value'],
|
||||
'datalist' => [],
|
||||
'dd' => ['nowrap'],
|
||||
'del' => ['cite', 'datetime'],
|
||||
'details' => ['open'],
|
||||
'dfn' => [],
|
||||
'dialog' => ['open'],
|
||||
'dir' => ['compact'],
|
||||
'div' => ['align'],
|
||||
'dl' => [],
|
||||
'dt' => [],
|
||||
// E
|
||||
'em' => [],
|
||||
'embed' => ['height', 'src', 'type', 'width'],
|
||||
// F
|
||||
'fieldset' => ['disabled', 'form', 'name'],
|
||||
'figcaption' => [],
|
||||
'figure' => [],
|
||||
'font' => ['color', 'face', 'size'],
|
||||
'footer' => [],
|
||||
'form' => ['accept', 'accept-charset', 'action', 'autocapitalize', 'autocomplete', 'enctype', 'method',
|
||||
'name', 'novalidate', 'target', ],
|
||||
'frame' => ['frameborder', 'marginheight', 'marginwidth', 'name', 'noresize', 'scrolling', 'src'],
|
||||
'frameset' => ['cols', 'rows'],
|
||||
// G
|
||||
// H
|
||||
'h1' => ['align'],
|
||||
'h2' => ['align'],
|
||||
'h3' => ['align'],
|
||||
'h4' => ['align'],
|
||||
'h5' => ['align'],
|
||||
'h6' => ['align'],
|
||||
'head' => ['profile'],
|
||||
'hr' => ['align', 'color', 'noshade', 'size', 'width'],
|
||||
'html' => ['manifest', 'version', 'xmlns'],
|
||||
// I
|
||||
'i' => [],
|
||||
'iframe' => ['align', 'allowfullscreen', 'allowpaymentrequest', 'frameborder', 'height', 'longdesc',
|
||||
'marginheight', 'marginwidth', 'name', 'referrerpolicy', 'sandbox', 'scrolling', 'src', 'srcdoc', 'width', ],
|
||||
'img' => ['align', 'alt', 'border', 'crossorigin', 'decoding', 'height', 'hspace', 'ismap', 'longdesc',
|
||||
'name', 'referrerpolicy', 'sizes', 'src', 'srcset', 'usemap', 'vspace', 'width', ],
|
||||
'input' => ['accept', 'alt', 'autocomplete', 'autofocus', 'capture', 'checked', 'disabled', 'form',
|
||||
'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'height', 'inputmode', 'ismap',
|
||||
'list', 'max', 'maxlength', 'min', 'minlength', 'multiple', 'name', 'pattern', 'placeholder', 'readonly',
|
||||
'required', 'selectionDirection', 'selectionEnd', 'selectionStart', 'size', 'spellcheck', 'src', 'step', 'type',
|
||||
'usemap', 'value', 'width', ],
|
||||
'ins' => ['cite', 'datetime'],
|
||||
'isindex' => ['action', 'prompt'],
|
||||
// J
|
||||
// K
|
||||
'kbd' => [],
|
||||
'keygen' => ['autofocus', 'challenge', 'disabled', 'form', 'keytype', 'name'],
|
||||
// L
|
||||
'label' => ['for', 'form'],
|
||||
'legend' => [],
|
||||
'li' => ['type', 'value'],
|
||||
'link' => ['as', 'crossorigin', 'charset', 'disabled', 'href', 'hreflang', 'integrity', 'media', 'methods', 'prefetch', 'referrerpolicy', 'rel', 'rev', 'sizes', 'target', 'type'],
|
||||
// M
|
||||
'main' => [],
|
||||
'map' => ['name'],
|
||||
'mark' => [],
|
||||
'menu' => ['label', 'type'],
|
||||
'menuitem' => ['checked', 'command', 'default', 'disabled', 'icon', 'label', 'radiogroup', 'type'],
|
||||
'meta' => ['charset', 'content', 'http-equiv', 'name', 'scheme'],
|
||||
'meter' => ['form', 'high', 'low', 'max', 'min', 'optimum', 'value'],
|
||||
// N
|
||||
'nav' => [],
|
||||
'noframes' => [],
|
||||
'noscript' => [],
|
||||
// O
|
||||
'object' => ['archive', 'border', 'classid', 'codebase', 'codetype', 'data', 'declare', 'form', 'height',
|
||||
'hspace', 'name', 'standby', 'type', 'typemustmatch', 'usemap', 'width', ],
|
||||
'ol' => ['compact', 'reversed', 'start', 'type'],
|
||||
'optgroup' => ['disabled', 'label'],
|
||||
'option' => ['disabled', 'label', 'selected', 'value'],
|
||||
'output' => ['for', 'form', 'name'],
|
||||
// P
|
||||
'p' => ['align'],
|
||||
'param' => ['name', 'type', 'value', 'valuetype'],
|
||||
'picture' => [],
|
||||
'pre' => ['cols', 'width', 'wrap'],
|
||||
'progress' => ['max', 'value'],
|
||||
// Q
|
||||
'q' => ['cite'],
|
||||
// R
|
||||
'rp' => [],
|
||||
'rt' => [],
|
||||
'rtc' => [],
|
||||
'ruby' => [],
|
||||
// S
|
||||
's' => [],
|
||||
'samp' => [],
|
||||
'script' => ['async', 'charset', 'crossorigin', 'defer', 'integrity', 'language', 'nomodule', 'nonce',
|
||||
'src', 'type', ],
|
||||
'section' => [],
|
||||
'select' => ['autofocus', 'disabled', 'form', 'multiple', 'name', 'required', 'size'],
|
||||
'small' => [],
|
||||
'source' => ['media', 'sizes', 'src', 'srcset', 'type'],
|
||||
'span' => [],
|
||||
'strike' => [],
|
||||
'strong' => [],
|
||||
'style' => ['media', 'nonce', 'scoped', 'type'],
|
||||
'sub' => [],
|
||||
'summary' => [],
|
||||
'sup' => [],
|
||||
// T
|
||||
'table' => ['align', 'bgcolor', 'border', 'cellpadding', 'cellspacing', 'frame', 'rules', 'summary', 'width'],
|
||||
'tbody' => ['align', 'bgcolor', 'char', 'charoff', 'valign'],
|
||||
'td' => ['abbr', 'align', 'axis', 'bgcolor', 'char', 'charoff', 'colspan', 'headers', 'nowrap',
|
||||
'rowspan', 'scope', 'valign', 'width', ],
|
||||
'template' => [],
|
||||
'textarea' => ['autocomplete', 'autofocus', 'cols', 'disabled', 'form', 'maxlength', 'minlength', 'name',
|
||||
'placeholder', 'readonly', 'rows', 'spellcheck', 'wrap', ],
|
||||
'tfoot' => ['align', 'bgcolor', 'char', 'charoff', 'valign'],
|
||||
'th' => ['abbr', 'align', 'axis', 'bgcolor', 'char', 'charoff', 'colspan', 'headers', 'nowrap',
|
||||
'rowspan', 'scope', 'valign', 'width', ],
|
||||
'thead' => ['align', 'bgcolor', 'char', 'charoff', 'valign'],
|
||||
'time' => ['datetime'],
|
||||
'title' => [],
|
||||
'tr' => ['align', 'bgcolor', 'char', 'charoff', 'valign'],
|
||||
'track' => ['default', 'kind', 'label', 'src', 'srclang'],
|
||||
'tt' => [],
|
||||
// U
|
||||
'u' => [],
|
||||
'ul' => ['compact', 'type'],
|
||||
// V
|
||||
'var' => [],
|
||||
'video' => ['autoplay', 'buffered', 'controls', 'crossorigin', 'height', 'loop', 'muted', 'played',
|
||||
'playsinline', 'preload', 'poster', 'src', 'width', ],
|
||||
// W
|
||||
'wbr' => [],
|
||||
// X
|
||||
// Y
|
||||
// Z
|
||||
];
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formButton
|
||||
* @brief HTML Forms password field creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formButton extends formInput
|
||||
{
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $value The value
|
||||
*/
|
||||
public function __construct($id = null, ?string $value = null)
|
||||
{
|
||||
parent::__construct($id, 'button');
|
||||
if ($value !== null) {
|
||||
$this->value($value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formCheckbox
|
||||
* @brief HTML Forms checkbox button creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formCheckbox extends formInput
|
||||
{
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param bool $checked Is checked
|
||||
*/
|
||||
public function __construct($id = null, ?bool $checked = null)
|
||||
{
|
||||
parent::__construct($id, 'checkbox');
|
||||
if ($checked !== null) {
|
||||
$this->checked($checked);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formColor
|
||||
* @brief HTML Forms color field creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formColor extends formInput
|
||||
{
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $value The value
|
||||
*/
|
||||
public function __construct($id = null, ?string $value = null)
|
||||
{
|
||||
parent::__construct($id, 'color');
|
||||
$this
|
||||
->size(7)
|
||||
->maxlength(7);
|
||||
if ($value !== null) {
|
||||
$this->value($value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,414 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formComponent
|
||||
* @brief HTML Forms creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
abstract class formComponent
|
||||
{
|
||||
private $_type; // Component type
|
||||
private $_element; // HTML element
|
||||
private $_data; // Custom component properties (see __get() and __set())
|
||||
|
||||
public function __construct(?string $type = null, ?string $_element = null)
|
||||
{
|
||||
$this->_type = $type ?? __CLASS__;
|
||||
$this->_element = $_element;
|
||||
$this->_data = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Call statically new instance
|
||||
*
|
||||
* Use formXxx::init(...$args) to statically create a new instance
|
||||
*
|
||||
* @return object New formXxx instance
|
||||
*/
|
||||
public static function init(...$args)
|
||||
{
|
||||
$class = get_called_class();
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
return new $class(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic getter method
|
||||
*
|
||||
* @param string $property The property
|
||||
*
|
||||
* @return mixed property value if property exists or null
|
||||
*/
|
||||
public function __get(string $property)
|
||||
{
|
||||
return array_key_exists($property, $this->_data) ? $this->_data[$property] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic setter method
|
||||
*
|
||||
* @param string $property The property
|
||||
* @param mixed $value The value
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function __set(string $property, $value)
|
||||
{
|
||||
$this->_data[$property] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic isset method
|
||||
*
|
||||
* @param string $property The property
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset(string $property): bool
|
||||
{
|
||||
return isset($this->_data[$property]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic unset method
|
||||
*
|
||||
* @param string $property The property
|
||||
*/
|
||||
public function __unset(string $property): void
|
||||
{
|
||||
unset($this->_data[$property]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic call method
|
||||
*
|
||||
* If the method exists, call it and return it's return value
|
||||
* If not, if there is no argument ($argument empty array), assume that it's a get
|
||||
* If not, assume that's is a set (value = $argument[0])
|
||||
*
|
||||
* @param string $method The property
|
||||
* @param array $arguments The arguments
|
||||
*
|
||||
* @return mixed method called, property value (or null), self
|
||||
*/
|
||||
public function __call(string $method, $arguments)
|
||||
{
|
||||
// Cope with known methods
|
||||
if (method_exists($this, $method)) {
|
||||
return call_user_func_array([$this, $method], $arguments);
|
||||
}
|
||||
|
||||
// Unknown method
|
||||
if (!count($arguments)) {
|
||||
// No argument, assume its a get
|
||||
if (array_key_exists($method, $this->_data)) {
|
||||
return $this->_data[$method];
|
||||
}
|
||||
|
||||
return null; // @phpstan-ignore-line
|
||||
}
|
||||
// Argument here, assume its a set
|
||||
$this->_data[$method] = $arguments[0];
|
||||
|
||||
return $this; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic invoke method
|
||||
*
|
||||
* Return rendering of component
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __invoke(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of component
|
||||
*
|
||||
* @return string The type.
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type of component
|
||||
*
|
||||
* @param string $type The type
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setType(string $type)
|
||||
{
|
||||
$this->_type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the HTML element
|
||||
*
|
||||
* @return null|string The element.
|
||||
*/
|
||||
public function getElement(): ?string
|
||||
{
|
||||
return $this->_element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTML element
|
||||
*
|
||||
* @param string $element The element
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setElement(string $element)
|
||||
{
|
||||
$this->_element = $element;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the label.
|
||||
*
|
||||
* @param formLabel|null $label The label
|
||||
* @param int|null $position The position
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function attachLabel(?formLabel $label = null, ?int $position = null)
|
||||
{
|
||||
if ($label) {
|
||||
$this->label($label);
|
||||
$label->for($this->id);
|
||||
if ($position !== null) {
|
||||
$label->setPosition($position);
|
||||
}
|
||||
} elseif (isset($this->label)) {
|
||||
unset($this->label);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches the label from this component
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function detachLabel()
|
||||
{
|
||||
if (isset($this->label)) {
|
||||
unset($this->label);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the identifier (name/id).
|
||||
*
|
||||
* If the given identifier is a string, set name = id = given string
|
||||
* If it is an array of only one element, name = [first element]
|
||||
* Else name = [first element], id = [second element]
|
||||
*
|
||||
* @param string|array|null $identifier (string or array)
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setIdentifier($identifier)
|
||||
{
|
||||
if (is_string($identifier)) {
|
||||
$this->name = $identifier;
|
||||
$this->id = $identifier;
|
||||
} elseif (is_array($identifier)) {
|
||||
$this->name = $identifier[0];
|
||||
if (isset($identifier[1])) {
|
||||
$this->id = $identifier[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check mandatory attributes in properties, at least name or id must be present
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function checkMandatoryAttributes(): bool
|
||||
{
|
||||
// Check for mandatory info
|
||||
return (isset($this->name) || isset($this->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render common attributes
|
||||
*
|
||||
* $this->
|
||||
*
|
||||
* type => string type (may be used for input component).
|
||||
*
|
||||
* name => string name (required if id is not provided).
|
||||
* id => string id (required if name is not provided).
|
||||
*
|
||||
* value => string value.
|
||||
* default => string default value (will be used if value is not provided).
|
||||
* checked => boolean checked.
|
||||
*
|
||||
* accesskey => string accesskey (character(s) space separated).
|
||||
* autocomplete => string autocomplete type.
|
||||
* autofocus => boolean autofocus.
|
||||
* class => string (or array of string) class(es).
|
||||
* contenteditable => boolean content editable.
|
||||
* dir => string direction.
|
||||
* disabled => boolean disabled.
|
||||
* form => string form id.
|
||||
* lang => string lang.
|
||||
* list => string list id.
|
||||
* max => int max value.
|
||||
* maxlength => int max length.
|
||||
* min => int min value.
|
||||
* readonly => boolean readonly.
|
||||
* required => boolean required.
|
||||
* pattern => string pattern.
|
||||
* placeholder => string placeholder.
|
||||
* size => int size.
|
||||
* spellcheck => boolean spellcheck.
|
||||
* tabindex => int tabindex.
|
||||
* title => string title.
|
||||
*
|
||||
* data => array data.
|
||||
* [
|
||||
* key => string data id (rendered as data-<id>).
|
||||
* value => string data value.
|
||||
* ]
|
||||
*
|
||||
* extra => string (or array of string) extra HTML attributes.
|
||||
*
|
||||
* @param bool $includeValue Includes $this->value if exist (default = true)
|
||||
* should be set to false to textarea and may be some others
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function renderCommonAttributes(bool $includeValue = true): string
|
||||
{
|
||||
$render = '' .
|
||||
|
||||
// Type (used for input component)
|
||||
(isset($this->type) ?
|
||||
' type="' . $this->type . '"' : '') .
|
||||
|
||||
// Identifier
|
||||
(isset($this->name) ?
|
||||
' name="' . $this->name . '"' : '') .
|
||||
(isset($this->id) ?
|
||||
' id="' . $this->id . '"' : '') .
|
||||
|
||||
// Value
|
||||
// - $this->default will be used as value if exists and $this->value does not
|
||||
($includeValue && array_key_exists('value', $this->_data) ?
|
||||
' value="' . $this->value . '"' : '') .
|
||||
($includeValue && !array_key_exists('value', $this->_data) && array_key_exists('default', $this->_data) ?
|
||||
' value="' . $this->default . '"' : '') .
|
||||
(isset($this->checked) && $this->checked ?
|
||||
' checked' : '') .
|
||||
|
||||
// Common attributes
|
||||
(isset($this->accesskey) ?
|
||||
' accesskey="' . $this->accesskey . '"' : '') .
|
||||
(isset($this->autocapitalize) ?
|
||||
' autocapitalize="' . $this->autocapitalize . '"' : '') .
|
||||
(isset($this->autocomplete) ?
|
||||
' autocomplete="' . $this->autocomplete . '"' : '') .
|
||||
(isset($this->autocorrect) ?
|
||||
' autocorrect="' . $this->autocorrect . '"' : '') .
|
||||
(isset($this->autofocus) && $this->autofocus ?
|
||||
' autofocus' : '') .
|
||||
(isset($this->class) ?
|
||||
' class="' . (is_array($this->class) ? implode(' ', $this->class) : $this->class) . '"' : '') .
|
||||
(isset($this->contenteditable) && $this->contenteditable ?
|
||||
' contenteditable' : '') .
|
||||
(isset($this->dir) ?
|
||||
' dir="' . $this->dir . '"' : '') .
|
||||
(isset($this->disabled) && $this->disabled ?
|
||||
' disabled' : '') .
|
||||
(isset($this->form) ?
|
||||
' form="' . $this->form . '"' : '') .
|
||||
(isset($this->lang) ?
|
||||
' lang="' . $this->lang . '"' : '') .
|
||||
(isset($this->list) ?
|
||||
' list="' . $this->list . '"' : '') .
|
||||
(isset($this->max) ?
|
||||
' max="' . strval((int) $this->max) . '"' : '') .
|
||||
(isset($this->maxlength) ?
|
||||
' maxlength="' . strval((int) $this->maxlength) . '"' : '') .
|
||||
(isset($this->min) ?
|
||||
' min="' . strval((int) $this->min) . '"' : '') .
|
||||
(isset($this->pattern) ?
|
||||
' pattern="' . $this->pattern . '"' : '') .
|
||||
(isset($this->placeholder) ?
|
||||
' placeholder="' . $this->placeholder . '"' : '') .
|
||||
(isset($this->readonly) && $this->readonly ?
|
||||
' readonly' : '') .
|
||||
(isset($this->required) && $this->required ?
|
||||
' required' : '') .
|
||||
(isset($this->size) ?
|
||||
' size="' . strval((int) $this->size) . '"' : '') .
|
||||
(isset($this->spellcheck) ?
|
||||
' spellcheck="' . ($this->spellcheck ? 'true' : 'false') . '"' : '') .
|
||||
(isset($this->tabindex) ?
|
||||
' tabindex="' . strval((int) $this->tabindex) . '"' : '') .
|
||||
(isset($this->title) ?
|
||||
' title="' . $this->title . '"' : '') .
|
||||
|
||||
'';
|
||||
|
||||
if (isset($this->data) && is_array($this->data)) {
|
||||
// Data attributes
|
||||
foreach ($this->data as $key => $value) {
|
||||
$render .= ' data-' . $key . '="' . $value . '"';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->extra)) {
|
||||
// Extra HTML
|
||||
$render .= ' ' . (is_array($this->extra) ? implode(' ', $this->extra) : $this->extra);
|
||||
}
|
||||
|
||||
return $render;
|
||||
}
|
||||
|
||||
// Abstract methods
|
||||
|
||||
/**
|
||||
* Renders the object.
|
||||
*
|
||||
* Must be provided by classes which extends this class
|
||||
*/
|
||||
abstract protected function render(): string;
|
||||
|
||||
/**
|
||||
* Gets the default element.
|
||||
*
|
||||
* @return string The default HTML element.
|
||||
*/
|
||||
abstract protected function getDefaultElement(): string;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formDate
|
||||
* @brief HTML Forms date field creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formDate extends formInput
|
||||
{
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $value The value
|
||||
*/
|
||||
public function __construct($id = null, ?string $value = null)
|
||||
{
|
||||
parent::__construct($id, 'date');
|
||||
$this
|
||||
->size(10)
|
||||
->maxlength(10)
|
||||
->pattern('[0-9]{4}-[0-9]{2}-[0-9]{2}')
|
||||
->placeholder('1962-05-13');
|
||||
if ($value !== null) {
|
||||
$this->value($value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formDatetime
|
||||
* @brief HTML Forms datetime field creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formDatetime extends formInput
|
||||
{
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $value The value
|
||||
*/
|
||||
public function __construct($id = null, ?string $value = null)
|
||||
{
|
||||
parent::__construct($id, 'datetime-local');
|
||||
$this
|
||||
->size(16)
|
||||
->maxlength(16)
|
||||
->pattern('[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}')
|
||||
->placeholder('1962-05-13T14:45');
|
||||
if ($value !== null) {
|
||||
$this->value($value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formEmail
|
||||
* @brief HTML Forms email field creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formEmail extends formInput
|
||||
{
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $value The value
|
||||
*/
|
||||
public function __construct($id = null, ?string $value = null)
|
||||
{
|
||||
parent::__construct($id, 'email');
|
||||
if ($value !== null) {
|
||||
$this->value($value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formFieldset
|
||||
* @brief HTML Forms fieldset creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formFieldset extends formComponent
|
||||
{
|
||||
private const DEFAULT_ELEMENT = 'fieldset';
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $element The element
|
||||
*/
|
||||
public function __construct($id = null, ?string $element = null)
|
||||
{
|
||||
parent::__construct(__CLASS__, $element ?? self::DEFAULT_ELEMENT);
|
||||
if ($id !== null) {
|
||||
$this->setIdentifier($id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the legend to this fieldset.
|
||||
*
|
||||
* @param formLegend|null $legend The legend
|
||||
*/
|
||||
public function attachLegend(?formLegend $legend)
|
||||
{
|
||||
if ($legend) {
|
||||
$this->legend($legend);
|
||||
} elseif (isset($this->legend)) {
|
||||
unset($this->legend);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches the legend.
|
||||
*/
|
||||
public function detachLegend()
|
||||
{
|
||||
if (isset($this->legend)) {
|
||||
unset($this->legend);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTML component (including the associated legend if any).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render(): string
|
||||
{
|
||||
$buffer = '<' . ($this->getElement() ?? self::DEFAULT_ELEMENT) . $this->renderCommonAttributes() . '>' . "\n";
|
||||
|
||||
if (isset($this->legend)) {
|
||||
$buffer .= $this->legend->render();
|
||||
}
|
||||
|
||||
if (isset($this->fields) && is_array($this->fields)) {
|
||||
foreach ($this->fields as $field) {
|
||||
if (isset($this->legend) && $field->getDefaultElement() === 'legend') {
|
||||
// Do not put more than one legend in fieldset
|
||||
continue;
|
||||
}
|
||||
$buffer .= $field->render();
|
||||
}
|
||||
}
|
||||
|
||||
$buffer .= "\n" . '</' . ($this->getElement() ?? self::DEFAULT_ELEMENT) . '>' . "\n";
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default element.
|
||||
*
|
||||
* @return string The default element.
|
||||
*/
|
||||
public function getDefaultElement(): string
|
||||
{
|
||||
return self::DEFAULT_ELEMENT;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formFile
|
||||
* @brief HTML Forms file field creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formFile extends formInput
|
||||
{
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $value The value
|
||||
*/
|
||||
public function __construct($id = null, ?string $value = null)
|
||||
{
|
||||
parent::__construct($id, 'file');
|
||||
if ($value !== null) {
|
||||
$this->value($value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formForm
|
||||
* @brief HTML Forms form creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formForm extends formComponent
|
||||
{
|
||||
private const DEFAULT_ELEMENT = 'form';
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $element The element
|
||||
*/
|
||||
public function __construct($id = null, ?string $element = null)
|
||||
{
|
||||
parent::__construct(__CLASS__, $element ?? self::DEFAULT_ELEMENT);
|
||||
if ($id !== null) {
|
||||
$this->setIdentifier($id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTML component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render(?string $fieldFormat = null): string
|
||||
{
|
||||
if (!$this->checkMandatoryAttributes()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$buffer = '<' . ($this->getElement() ?? self::DEFAULT_ELEMENT) .
|
||||
(isset($this->action) ? ' action="' . $this->action . '"' : '') .
|
||||
(isset($this->method) ? ' method="' . $this->method . '"' : '') .
|
||||
$this->renderCommonAttributes() . '>' . "\n";
|
||||
|
||||
if (isset($this->fields) && is_array($this->fields)) {
|
||||
foreach ($this->fields as $field) {
|
||||
$buffer .= sprintf(($fieldFormat ?: '%s'), $field->render());
|
||||
}
|
||||
}
|
||||
|
||||
$buffer .= '</' . ($this->getElement() ?? self::DEFAULT_ELEMENT) . '>' . "\n";
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default element.
|
||||
*
|
||||
* @return string The default element.
|
||||
*/
|
||||
public function getDefaultElement(): string
|
||||
{
|
||||
return self::DEFAULT_ELEMENT;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formHidden
|
||||
* @brief HTML Forms hidden field creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formHidden extends formInput
|
||||
{
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $value The value
|
||||
*/
|
||||
public function __construct($id = null, ?string $value = null)
|
||||
{
|
||||
// Label should not be rendered for an input type="hidden"
|
||||
parent::__construct($id, 'hidden', false);
|
||||
if ($value !== null) {
|
||||
$this->value($value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formInput
|
||||
* @brief HTML Forms input field creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formInput extends formComponent
|
||||
{
|
||||
private const DEFAULT_ELEMENT = 'input';
|
||||
|
||||
/**
|
||||
* Should include the associated label if exist
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $renderLabel = true;
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $type The input type
|
||||
* @param bool $renderLabel Render label if present
|
||||
*/
|
||||
public function __construct($id = null, string $type = 'text', bool $renderLabel = true)
|
||||
{
|
||||
parent::__construct(__CLASS__, self::DEFAULT_ELEMENT);
|
||||
$this->type($type);
|
||||
$this->renderLabel = $renderLabel;
|
||||
if ($id !== null) {
|
||||
$this->setIdentifier($id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTML component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render(): string
|
||||
{
|
||||
if (!$this->checkMandatoryAttributes()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$buffer = '<' . ($this->getElement() ?? self::DEFAULT_ELEMENT) . $this->renderCommonAttributes() . '/>' . "\n";
|
||||
|
||||
if ($this->renderLabel && isset($this->label) && isset($this->id)) {
|
||||
$this->label->for = $this->id;
|
||||
$buffer = $this->label->render($buffer);
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default element.
|
||||
*
|
||||
* @return string The default element.
|
||||
*/
|
||||
public function getDefaultElement(): string
|
||||
{
|
||||
return self::DEFAULT_ELEMENT;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formLabel
|
||||
* @brief HTML Forms label creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formLabel extends formComponent
|
||||
{
|
||||
private const DEFAULT_ELEMENT = 'label';
|
||||
|
||||
// Position of linked component and position of text/label
|
||||
public const INSIDE_TEXT_BEFORE = 0;
|
||||
public const INSIDE_TEXT_AFTER = 1;
|
||||
public const OUTSIDE_LABEL_BEFORE = 2;
|
||||
public const OUTSIDE_LABEL_AFTER = 3;
|
||||
|
||||
// Aliases
|
||||
public const INSIDE_LABEL_BEFORE = 0;
|
||||
public const INSIDE_LABEL_AFTER = 1;
|
||||
public const OUTSIDE_TEXT_BEFORE = 2;
|
||||
public const OUTSIDE_TEXT_AFTER = 3;
|
||||
|
||||
/**
|
||||
* Position of linked component:
|
||||
* INSIDE_TEXT_BEFORE = inside label, label text before component
|
||||
* INSIDE_TEXT_AFTER = inside label, label text after component
|
||||
* OUTSIDE_LABEL_BEFORE = after label
|
||||
* OUTSIDE_LABEL_AFTER = before label
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $_position = self::INSIDE_TEXT_BEFORE;
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param string $text The text
|
||||
* @param int $position The position
|
||||
* @param null|string $id The identifier
|
||||
*/
|
||||
public function __construct(string $text = '', int $position = self::INSIDE_TEXT_BEFORE, ?string $id = null)
|
||||
{
|
||||
parent::__construct(__CLASS__, self::DEFAULT_ELEMENT);
|
||||
$this->_position = $position;
|
||||
$this
|
||||
->text($text);
|
||||
if ($id !== null) {
|
||||
$this->for($id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTML component.
|
||||
*
|
||||
* @param null|string $buffer The buffer
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render(?string $buffer = ''): string
|
||||
{
|
||||
/**
|
||||
* sprintf formats
|
||||
*
|
||||
* %1$s = label opening block
|
||||
* %2$s = text of label
|
||||
* %3$s = linked component
|
||||
* %4$s = label closing block
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
$formats = [
|
||||
'<%1$s>%2$s %3$s</%4$s>', // Component inside label with label text before it
|
||||
'<%1$s>%3$s %2$s</%4$s>', // Component inside label with label text after it
|
||||
'<%1$s>%2$s</%4$s> %3$s', // Component after label (for attribute will be used)
|
||||
'%3$s <%1$s>%2$s</%4$s>', // Component before label (for attribute will be used)
|
||||
];
|
||||
|
||||
if ($this->_position < 0 || $this->_position > count($formats)) {
|
||||
$this->_position = self::INSIDE_TEXT_BEFORE;
|
||||
}
|
||||
|
||||
$start = ($this->getElement() ?? self::DEFAULT_ELEMENT);
|
||||
/* @phpstan-ignore-next-line */
|
||||
if (($this->_position !== self::INSIDE_TEXT_BEFORE || $this->_position !== self::INSIDE_TEXT_AFTER) && isset($this->for)) {
|
||||
$start .= ' for="' . $this->for . '"';
|
||||
}
|
||||
$start .= $this->renderCommonAttributes();
|
||||
|
||||
$end = ($this->getElement() ?? self::DEFAULT_ELEMENT);
|
||||
|
||||
return sprintf($formats[$this->_position], $start, $this->text, $buffer ?: '', $end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the position.
|
||||
*
|
||||
* @param int $position The position
|
||||
*/
|
||||
public function setPosition(int $position = self::INSIDE_TEXT_BEFORE)
|
||||
{
|
||||
$this->_position = $position;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default element.
|
||||
*
|
||||
* @return string The default element.
|
||||
*/
|
||||
public function getDefaultElement(): string
|
||||
{
|
||||
return self::DEFAULT_ELEMENT;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formLegend
|
||||
* @brief HTML Forms legend creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formLegend extends formComponent
|
||||
{
|
||||
private const DEFAULT_ELEMENT = 'legend';
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param string $text The text
|
||||
* @param mixed $id The identifier
|
||||
* @param string $element The element
|
||||
*/
|
||||
public function __construct(string $text = '', $id = null, ?string $element = null)
|
||||
{
|
||||
parent::__construct(__CLASS__, $element ?? self::DEFAULT_ELEMENT);
|
||||
$this->text($text);
|
||||
if ($id !== null) {
|
||||
$this->setIdentifier($id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTML component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render(): string
|
||||
{
|
||||
$buffer = '<' . ($this->getElement() ?? self::DEFAULT_ELEMENT) . $this->renderCommonAttributes() . '>';
|
||||
if ($this->text) {
|
||||
$buffer .= $this->text;
|
||||
}
|
||||
$buffer .= '</' . ($this->getElement() ?? self::DEFAULT_ELEMENT) . '>' . "\n";
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default element.
|
||||
*
|
||||
* @return string The default element.
|
||||
*/
|
||||
public function getDefaultElement(): string
|
||||
{
|
||||
return self::DEFAULT_ELEMENT;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formNumber
|
||||
* @brief HTML Forms number field creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formNumber extends formInput
|
||||
{
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param int $min The minimum value
|
||||
* @param int $max The maximum value
|
||||
* @param int $value The value
|
||||
*/
|
||||
public function __construct($id = null, ?int $min = null, ?int $max = null, ?int $value = null)
|
||||
{
|
||||
parent::__construct($id, 'number');
|
||||
$this
|
||||
->min($min)
|
||||
->max($max);
|
||||
if ($value !== null) {
|
||||
$this->value($value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formOptgroup
|
||||
* @brief HTML Forms optgroup creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formOptgroup extends formComponent
|
||||
{
|
||||
private const DEFAULT_ELEMENT = 'optgroup';
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param string $name The optgroup name
|
||||
* @param null|string $element The element
|
||||
*/
|
||||
public function __construct(string $name, ?string $element = null)
|
||||
{
|
||||
parent::__construct(__CLASS__, $element ?? self::DEFAULT_ELEMENT);
|
||||
$this
|
||||
->text($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTML component.
|
||||
*
|
||||
* @param null|string $default The default value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render(?string $default = null): string
|
||||
{
|
||||
$buffer = '<' . ($this->getElement() ?? self::DEFAULT_ELEMENT) .
|
||||
(isset($this->text) ? ' label="' . $this->text . '"' : '') .
|
||||
$this->renderCommonAttributes() . '>' . "\n";
|
||||
|
||||
if (isset($this->items) && is_array($this->items)) {
|
||||
foreach ($this->items as $item => $value) {
|
||||
if ($value instanceof formOption || $value instanceof formOptgroup) {
|
||||
$buffer .= $value->render($default);
|
||||
} elseif (is_array($value)) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$buffer .= (new formOptgroup($item))->items($value)->render($this->default ?? $default ?? null);
|
||||
} else {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$buffer .= (new formOption($item, $value))->render($this->default ?? $default ?? null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$buffer .= '</' . ($this->getElement() ?? self::DEFAULT_ELEMENT) . '>' . "\n";
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default element.
|
||||
*
|
||||
* @return string The default element.
|
||||
*/
|
||||
public function getDefaultElement(): string
|
||||
{
|
||||
return self::DEFAULT_ELEMENT;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formOption
|
||||
* @brief HTML Forms option creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formOption extends formComponent
|
||||
{
|
||||
private const DEFAULT_ELEMENT = 'option';
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param string $name The option name
|
||||
* @param string $value The option value
|
||||
* @param null|string $element The element
|
||||
*/
|
||||
public function __construct(string $name, string $value, ?string $element = null)
|
||||
{
|
||||
parent::__construct(__CLASS__, $element ?? self::DEFAULT_ELEMENT);
|
||||
$this
|
||||
->text($name)
|
||||
->value($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTML component.
|
||||
*
|
||||
* @param null|string $default The default value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render(?string $default = null): string
|
||||
{
|
||||
$buffer = '<' . ($this->getElement() ?? self::DEFAULT_ELEMENT) .
|
||||
($this->value === $default ? ' selected' : '') .
|
||||
$this->renderCommonAttributes() . '>';
|
||||
|
||||
if ($this->text) {
|
||||
$buffer .= $this->text;
|
||||
}
|
||||
|
||||
$buffer .= '</' . ($this->getElement() ?? self::DEFAULT_ELEMENT) . '>' . "\n";
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default element.
|
||||
*
|
||||
* @return string The default element.
|
||||
*/
|
||||
public function getDefaultElement(): string
|
||||
{
|
||||
return self::DEFAULT_ELEMENT;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formPassword
|
||||
* @brief HTML Forms password field creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formPassword extends formInput
|
||||
{
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $value The value
|
||||
*/
|
||||
public function __construct($id = null, ?string $value = null)
|
||||
{
|
||||
parent::__construct($id, 'password');
|
||||
// Default attributes for a password, may be supercharge after
|
||||
$this->autocorrect('off');
|
||||
$this->spellcheck('off');
|
||||
$this->autocapitalize('off');
|
||||
if ($value !== null) {
|
||||
$this->value($value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formRadio
|
||||
* @brief HTML Forms radio button creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formRadio extends formInput
|
||||
{
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param bool $checked If checked
|
||||
*/
|
||||
public function __construct($id = null, ?bool $checked = null)
|
||||
{
|
||||
parent::__construct($id, 'radio');
|
||||
if ($checked !== null) {
|
||||
$this->checked($checked);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formSelect
|
||||
* @brief HTML Forms select creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formSelect extends formComponent
|
||||
{
|
||||
private const DEFAULT_ELEMENT = 'select';
|
||||
|
||||
/**
|
||||
* Should include the associated label if exist
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $renderLabel = true;
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $element The element
|
||||
* @param bool $renderLabel Render label if present
|
||||
*/
|
||||
public function __construct($id = null, ?string $element = null, bool $renderLabel = true)
|
||||
{
|
||||
parent::__construct(__CLASS__, $element ?? self::DEFAULT_ELEMENT);
|
||||
$this->renderLabel = $renderLabel;
|
||||
if ($id !== null) {
|
||||
$this->setIdentifier($id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTML component (including select options).
|
||||
*
|
||||
* @param null|string $default The default value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render(?string $default = null): string
|
||||
{
|
||||
if (!$this->checkMandatoryAttributes()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$buffer = '<' . ($this->getElement() ?? self::DEFAULT_ELEMENT) . $this->renderCommonAttributes() . '>' . "\n";
|
||||
|
||||
if (isset($this->items) && is_array($this->items)) {
|
||||
foreach ($this->items as $item => $value) {
|
||||
if ($value instanceof formOption || $value instanceof formOptgroup) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$buffer .= $value->render($this->default ?? $default ?? null);
|
||||
} elseif (is_array($value)) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$buffer .= (new formOptgroup($item))->items($value)->render($this->default ?? $default ?? null);
|
||||
} else {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$buffer .= (new formOption($item, (string) $value))->render($this->default ?? $default ?? null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$buffer .= '</' . ($this->getElement() ?? self::DEFAULT_ELEMENT) . '>' . "\n";
|
||||
|
||||
if ($this->renderLabel && isset($this->label) && isset($this->id)) {
|
||||
$this->label->for = $this->id;
|
||||
$buffer = $this->label->render($buffer);
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default element.
|
||||
*
|
||||
* @return string The default element.
|
||||
*/
|
||||
public function getDefaultElement(): string
|
||||
{
|
||||
return self::DEFAULT_ELEMENT;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formSubmit
|
||||
* @brief HTML Forms password field creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formSubmit extends formInput
|
||||
{
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $value The value
|
||||
*/
|
||||
public function __construct($id = null, ?string $value = null)
|
||||
{
|
||||
parent::__construct($id, 'submit');
|
||||
if ($value !== null) {
|
||||
$this->value($value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formTextarea
|
||||
* @brief HTML Forms textarea creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formTextarea extends formComponent
|
||||
{
|
||||
private const DEFAULT_ELEMENT = 'textarea';
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $value The value
|
||||
*/
|
||||
public function __construct($id = null, ?string $value = null)
|
||||
{
|
||||
parent::__construct(__CLASS__, self::DEFAULT_ELEMENT);
|
||||
if ($id !== null) {
|
||||
$this->setIdentifier($id);
|
||||
}
|
||||
if ($value !== null) {
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTML component (including the associated label if any).
|
||||
*
|
||||
* @param null|string $extra The extra
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render(?string $extra = null): string
|
||||
{
|
||||
if (!$this->checkMandatoryAttributes()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$buffer = '<' . ($this->getElement() ?? self::DEFAULT_ELEMENT) . ($extra ?? '') . $this->renderCommonAttributes(false) .
|
||||
(isset($this->cols) ? ' cols="' . strval((int) $this->cols) . '"' : '') .
|
||||
(isset($this->rows) ? ' rows="' . strval((int) $this->rows) . '"' : '') .
|
||||
'>' .
|
||||
($this->value ?? '') .
|
||||
'</' . ($this->getElement() ?? self::DEFAULT_ELEMENT) . '>' . "\n";
|
||||
|
||||
if (isset($this->label) && isset($this->id)) {
|
||||
$this->label->for = $this->id;
|
||||
$buffer = $this->label->render($buffer);
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default element.
|
||||
*
|
||||
* @return string The default element.
|
||||
*/
|
||||
public function getDefaultElement(): string
|
||||
{
|
||||
return self::DEFAULT_ELEMENT;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formTime
|
||||
* @brief HTML Forms time field creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formTime extends formInput
|
||||
{
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $value The value
|
||||
*/
|
||||
public function __construct($id = null, ?string $value = null)
|
||||
{
|
||||
parent::__construct($id, 'time');
|
||||
$this
|
||||
->size(5)
|
||||
->maxlength(5)
|
||||
->pattern('[0-9]{2}:[0-9]{2}')
|
||||
->placeholder('14:45');
|
||||
|
||||
if ($value !== null) {
|
||||
$this->value($value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @class formUrl
|
||||
* @brief HTML Forms url field creation helpers
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage html.form
|
||||
*
|
||||
* @since 1.2 First time this was introduced.
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class formUrl extends formInput
|
||||
{
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param mixed $id The identifier
|
||||
* @param string $value The value
|
||||
*/
|
||||
public function __construct($id = null, ?string $value = null)
|
||||
{
|
||||
parent::__construct($id, 'url');
|
||||
if ($value !== null) {
|
||||
$this->value($value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
/**
|
||||
* @class htmlValidator
|
||||
* @brief HTML Validator
|
||||
*
|
||||
* This class will perform an HTML validation upon WDG validator.
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage HTML
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class htmlValidator extends netHttp
|
||||
{
|
||||
/**
|
||||
* Validator host
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $host = 'validator.w3.org';
|
||||
|
||||
/**
|
||||
* Validator path
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $path = '/nu/';
|
||||
|
||||
/**
|
||||
* Use SSL
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $use_ssl = true;
|
||||
|
||||
/**
|
||||
* User agent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $user_agent = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.3a) Gecko/20021207';
|
||||
|
||||
/**
|
||||
* Timeout (in seconds)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $timeout = 2;
|
||||
|
||||
/**
|
||||
* Validation errors list (HTML)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $html_errors = '';
|
||||
|
||||
/**
|
||||
* Constructor, no parameters.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct($this->host, 443, $this->timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML Document
|
||||
*
|
||||
* Returns an HTML document from a <var>$fragment</var>.
|
||||
*
|
||||
* @param string $fragment HTML content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDocument(string $fragment): string
|
||||
{
|
||||
return
|
||||
'<!DOCTYPE html>' . "\n" .
|
||||
'<html>' . "\n" .
|
||||
'<head>' . "\n" .
|
||||
'<title>validation</title>' . "\n" .
|
||||
'</head>' . "\n" .
|
||||
'<body>' . "\n" .
|
||||
$fragment . "\n" .
|
||||
'</body>' . "\n" .
|
||||
'</html>';
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML validation
|
||||
*
|
||||
* Performs HTML validation of <var>$html</var>.
|
||||
*
|
||||
* @param string $html HTML document
|
||||
* @param string $charset Document charset
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function perform(string $html, string $charset = 'UTF-8'): bool
|
||||
{
|
||||
$this->setMoreHeader('Content-Type: text/html; charset=' . strtolower($charset));
|
||||
$this->post($this->path, $html);
|
||||
|
||||
if ($this->getStatus() !== 200) {
|
||||
throw new Exception('Status code line invalid.');
|
||||
}
|
||||
|
||||
$result = $this->getContent();
|
||||
|
||||
if (strpos($result, '<p class="success">The document validates according to the specified schema(s).</p>')) {
|
||||
return true;
|
||||
}
|
||||
if (preg_match('#(<ol>.*</ol>)<p class="failure">There were errors.</p>#msU', $result, $matches)) {
|
||||
$this->html_errors = strip_tags($matches[1], '<ol><li><p><code><strong>');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation Errors
|
||||
*
|
||||
* @return string HTML validation errors list
|
||||
*/
|
||||
public function getErrors(): string
|
||||
{
|
||||
return $this->html_errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static HTML validation
|
||||
*
|
||||
* Static validation method of an HTML fragment. Returns an array with the
|
||||
* following parameters:
|
||||
*
|
||||
* - valid (boolean)
|
||||
* - errors (string)
|
||||
*
|
||||
* @param string $fragment HTML content
|
||||
* @param string $charset Document charset
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function validate(string $fragment, string $charset = 'UTF-8'): array
|
||||
{
|
||||
$instance = new self();
|
||||
$fragment = $instance->getDocument($fragment);
|
||||
|
||||
if ($instance->perform($fragment, $charset)) {
|
||||
return [
|
||||
'valid' => true,
|
||||
'errors' => null,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => false,
|
||||
'errors' => $instance->getErrors(),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,409 @@
|
|||
<?php
|
||||
/**
|
||||
* @class imageMeta
|
||||
* @brief Image metadata
|
||||
*
|
||||
* This class reads EXIF, IPTC and XMP metadata from a JPEG file.
|
||||
*
|
||||
* - Contributor: Mathieu Lecarme.
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Images
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class imageMeta
|
||||
{
|
||||
/**
|
||||
* Internal XMP array
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $xmp = [];
|
||||
|
||||
/**
|
||||
* Internal IPTC array
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $iptc = [];
|
||||
|
||||
/**
|
||||
* Internal EXIF array
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $exif = [];
|
||||
|
||||
/**
|
||||
* Read metadata
|
||||
*
|
||||
* Returns all image metadata in an array as defined in {@link $properties}.
|
||||
*
|
||||
* @param string $filename Image file path
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function readMeta($filename)
|
||||
{
|
||||
$instance = new self();
|
||||
$instance->loadFile($filename);
|
||||
|
||||
return $instance->getMeta();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata
|
||||
*
|
||||
* Returns all image metadata in an array as defined in {@link $properties}.
|
||||
* Should call {@link loadFile()} before.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getMeta(): array
|
||||
{
|
||||
foreach (array_keys($this->properties) as $k) {
|
||||
if (!empty($this->xmp[$k])) {
|
||||
$this->properties[$k] = $this->xmp[$k];
|
||||
} elseif (!empty($this->iptc[$k])) {
|
||||
$this->properties[$k] = $this->iptc[$k];
|
||||
} elseif (!empty($this->exif[$k])) {
|
||||
$this->properties[$k] = $this->exif[$k];
|
||||
}
|
||||
}
|
||||
|
||||
# Fix date format
|
||||
if ($this->properties['DateTimeOriginal'] !== null) {
|
||||
$this->properties['DateTimeOriginal'] = preg_replace(
|
||||
'/^(\d{4}):(\d{2}):(\d{2})/',
|
||||
'$1-$2-$3',
|
||||
$this->properties['DateTimeOriginal']
|
||||
);
|
||||
}
|
||||
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load file
|
||||
*
|
||||
* Loads a file and read its metadata.
|
||||
*
|
||||
* @param string $filename Image file path
|
||||
*/
|
||||
public function loadFile($filename): void
|
||||
{
|
||||
if (!is_file($filename) || !is_readable($filename)) {
|
||||
throw new Exception('Unable to read file');
|
||||
}
|
||||
|
||||
$this->readXMP($filename);
|
||||
$this->readIPTC($filename);
|
||||
$this->readExif($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read XMP
|
||||
*
|
||||
* Reads XML metadata and assigns values to {@link $xmp}.
|
||||
*
|
||||
* @param string $filename Image file path
|
||||
*/
|
||||
protected function readXMP($filename)
|
||||
{
|
||||
if (($fp = @fopen($filename, 'rb')) === false) {
|
||||
throw new Exception('Unable to open image file');
|
||||
}
|
||||
|
||||
$inside = false;
|
||||
$done = false;
|
||||
$xmp = null;
|
||||
|
||||
while (!feof($fp)) {
|
||||
$buffer = fgets($fp, 4096);
|
||||
|
||||
$xmp_start = strpos($buffer, '<x:xmpmeta');
|
||||
|
||||
if ($xmp_start !== false) {
|
||||
$buffer = substr($buffer, $xmp_start);
|
||||
$inside = true;
|
||||
}
|
||||
|
||||
if ($inside) {
|
||||
$xmp_end = strpos($buffer, '</x:xmpmeta>');
|
||||
if ($xmp_end !== false) {
|
||||
$buffer = substr($buffer, $xmp_end, 12);
|
||||
$inside = false;
|
||||
$done = true;
|
||||
}
|
||||
|
||||
$xmp .= $buffer;
|
||||
}
|
||||
|
||||
if ($done) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose($fp);
|
||||
|
||||
if (!$xmp) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->xmp_reg as $code => $patterns) {
|
||||
foreach ($patterns as $p) {
|
||||
if (preg_match($p, $xmp, $matches)) {
|
||||
$this->xmp[$code] = $matches[1];
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('%<dc:subject>\s*<rdf:Bag>(.+?)</rdf:Bag%msu', $xmp, $rdf_bag)
|
||||
&& preg_match_all('%<rdf:li>(.+?)</rdf:li>%msu', $rdf_bag[1], $rdf_bag_li)) {
|
||||
$this->xmp['Keywords'] = implode(',', $rdf_bag_li[1]);
|
||||
}
|
||||
|
||||
foreach ($this->xmp as $k => $v) {
|
||||
$this->xmp[$k] = html::decodeEntities(text::toUTF8($v));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read IPTC
|
||||
*
|
||||
* Reads IPTC metadata and assigns values to {@link $iptc}.
|
||||
*
|
||||
* @param string $filename Image file path
|
||||
*/
|
||||
protected function readIPTC($filename)
|
||||
{
|
||||
if (!function_exists('iptcparse')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$imageinfo = null;
|
||||
@getimagesize($filename, $imageinfo);
|
||||
|
||||
if (!is_array($imageinfo) || !isset($imageinfo['APP13'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$iptc = @iptcparse($imageinfo['APP13']);
|
||||
|
||||
if (!is_array($iptc)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->iptc_ref as $k => $v) {
|
||||
if (isset($iptc[$k]) && isset($this->iptc_to_property[$v])) {
|
||||
$this->iptc[$this->iptc_to_property[$v]] = text::toUTF8(trim(implode(',', $iptc[$k])));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read EXIF
|
||||
*
|
||||
* Reads EXIF metadata and assigns values to {@link $exif}.
|
||||
*
|
||||
* @param string $filename Image file path
|
||||
*/
|
||||
protected function readEXIF($filename)
|
||||
{
|
||||
if (!function_exists('exif_read_data')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = @exif_read_data($filename, 'ANY_TAG');
|
||||
|
||||
if (!is_array($data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->exif_to_property as $k => $v) {
|
||||
if (isset($data[$k])) {
|
||||
if (is_array($data[$k])) {
|
||||
foreach ($data[$k] as $kk => $vv) {
|
||||
$this->exif[$v . '.' . $kk] = text::toUTF8($vv);
|
||||
}
|
||||
} else {
|
||||
$this->exif[$v] = text::toUTF8($data[$k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* array $properties Final properties array
|
||||
*/
|
||||
protected $properties = [
|
||||
'Title' => null,
|
||||
'Description' => null,
|
||||
'Creator' => null,
|
||||
'Rights' => null,
|
||||
'Make' => null,
|
||||
'Model' => null,
|
||||
'Exposure' => null,
|
||||
'FNumber' => null,
|
||||
'MaxApertureValue' => null,
|
||||
'ExposureProgram' => null,
|
||||
'ISOSpeedRatings' => null,
|
||||
'DateTimeOriginal' => null,
|
||||
'ExposureBiasValue' => null,
|
||||
'MeteringMode' => null,
|
||||
'FocalLength' => null,
|
||||
'Lens' => null,
|
||||
'CountryCode' => null,
|
||||
'Country' => null,
|
||||
'State' => null,
|
||||
'City' => null,
|
||||
'Keywords' => null,
|
||||
];
|
||||
|
||||
# XMP
|
||||
protected $xmp_reg = [
|
||||
'Title' => [
|
||||
'%<dc:title>\s*<rdf:Alt>\s*<rdf:li.*?>(.+?)</rdf:li>%msu',
|
||||
],
|
||||
'Description' => [
|
||||
'%<dc:description>\s*<rdf:Alt>\s*<rdf:li.*?>(.+?)</rdf:li>%msu',
|
||||
],
|
||||
'Creator' => [
|
||||
'%<dc:creator>\s*<rdf:Seq>\s*<rdf:li>(.+?)</rdf:li>%msu',
|
||||
],
|
||||
'Rights' => [
|
||||
'%<dc:rights>\s*<rdf:Alt>\s*<rdf:li.*?>(.+?)</rdf:li>%msu',
|
||||
],
|
||||
'Make' => [
|
||||
'%<tiff:Make>(.+?)</tiff:Make>%msu',
|
||||
'%tiff:Make="(.+?)"%msu',
|
||||
],
|
||||
'Model' => [
|
||||
'%<tiff:Model>(.+?)</tiff:Model>%msu',
|
||||
'%tiff:Model="(.+?)"%msu',
|
||||
],
|
||||
'Exposure' => [
|
||||
'%<exif:ExposureTime>(.+?)</exif:ExposureTime>%msu',
|
||||
'%exif:ExposureTime="(.+?)"%msu',
|
||||
],
|
||||
'FNumber' => [
|
||||
'%<exif:FNumber>(.+?)</exif:FNumber>%msu',
|
||||
'%exif:FNumber="(.+?)"%msu',
|
||||
],
|
||||
'MaxApertureValue' => [
|
||||
'%<exif:MaxApertureValue>(.+?)</exif:MaxApertureValue>%msu',
|
||||
'%exif:MaxApertureValue="(.+?)"%msu',
|
||||
],
|
||||
'ExposureProgram' => [
|
||||
'%<exif:ExposureProgram>(.+?)</exif:ExposureProgram>%msu',
|
||||
'%exif:ExposureProgram="(.+?)"%msu',
|
||||
],
|
||||
'ISOSpeedRatings' => [
|
||||
'%<exif:ISOSpeedRatings>\s*<rdf:Seq>\s*<rdf:li>(.+?)</rdf:li>%msu',
|
||||
],
|
||||
'DateTimeOriginal' => [
|
||||
'%<exif:DateTimeOriginal>(.+?)</exif:DateTimeOriginal>%msu',
|
||||
'%exif:DateTimeOriginal="(.+?)"%msu',
|
||||
],
|
||||
'ExposureBiasValue' => [
|
||||
'%<exif:ExposureBiasValue>(.+?)</exif:ExposureBiasValue>%msu',
|
||||
'%exif:ExposureBiasValue="(.+?)"%msu',
|
||||
],
|
||||
'MeteringMode' => [
|
||||
'%<exif:MeteringMode>(.+?)</exif:MeteringMode>%msu',
|
||||
'%exif:MeteringMode="(.+?)"%msu',
|
||||
],
|
||||
'FocalLength' => [
|
||||
'%<exif:FocalLength>(.+?)</exif:FocalLength>%msu',
|
||||
'%exif:FocalLength="(.+?)"%msu',
|
||||
],
|
||||
'Lens' => [
|
||||
'%<aux:Lens>(.+?)</aux:Lens>%msu',
|
||||
'%aux:Lens="(.+?)"%msu',
|
||||
],
|
||||
'CountryCode' => [
|
||||
'%<Iptc4xmpCore:CountryCode>(.+?)</Iptc4xmpCore:CountryCode>%msu',
|
||||
'%Iptc4xmpCore:CountryCode="(.+?)"%msu',
|
||||
],
|
||||
'Country' => [
|
||||
'%<photoshop:Country>(.+?)</photoshop:Country>%msu',
|
||||
'%photoshop:Country="(.+?)"%msu',
|
||||
],
|
||||
'State' => [
|
||||
'%<photoshop:State>(.+?)</photoshop:State>%msu',
|
||||
'%photoshop:State="(.+?)"%msu',
|
||||
],
|
||||
'City' => [
|
||||
'%<photoshop:City>(.+?)</photoshop:City>%msu',
|
||||
'%photoshop:City="(.+?)"%msu',
|
||||
],
|
||||
];
|
||||
|
||||
# IPTC
|
||||
protected $iptc_ref = [
|
||||
'1#090' => 'Iptc.Envelope.CharacterSet', // Character Set used (32 chars max)
|
||||
'2#005' => 'Iptc.ObjectName', // Title (64 chars max)
|
||||
'2#015' => 'Iptc.Category', // (3 chars max)
|
||||
'2#020' => 'Iptc.Supplementals', // Supplementals categories (32 chars max)
|
||||
'2#025' => 'Iptc.Keywords', // (64 chars max)
|
||||
'2#040' => 'Iptc.SpecialsInstructions', // (256 chars max)
|
||||
'2#055' => 'Iptc.DateCreated', // YYYYMMDD (8 num chars max)
|
||||
'2#060' => 'Iptc.TimeCreated', // HHMMSS+/-HHMM (11 chars max)
|
||||
'2#062' => 'Iptc.DigitalCreationDate', // YYYYMMDD (8 num chars max)
|
||||
'2#063' => 'Iptc.DigitalCreationTime', // HHMMSS+/-HHMM (11 chars max)
|
||||
'2#080' => 'Iptc.ByLine', // Author (32 chars max)
|
||||
'2#085' => 'Iptc.ByLineTitle', // Author position (32 chars max)
|
||||
'2#090' => 'Iptc.City', // (32 chars max)
|
||||
'2#092' => 'Iptc.Sublocation', // (32 chars max)
|
||||
'2#095' => 'Iptc.ProvinceState', // (32 chars max)
|
||||
'2#100' => 'Iptc.CountryCode', // (32 alpha chars max)
|
||||
'2#101' => 'Iptc.CountryName', // (64 chars max)
|
||||
'2#105' => 'Iptc.Headline', // (256 chars max)
|
||||
'2#110' => 'Iptc.Credits', // (32 chars max)
|
||||
'2#115' => 'Iptc.Source', // (32 chars max)
|
||||
'2#116' => 'Iptc.Copyright', // Copyright Notice (128 chars max)
|
||||
'2#118' => 'Iptc.Contact', // (128 chars max)
|
||||
'2#120' => 'Iptc.Caption', // Caption/Abstract (2000 chars max)
|
||||
'2#122' => 'Iptc.CaptionWriter', // Caption Writer/Editor (32 chars max)
|
||||
];
|
||||
|
||||
protected $iptc_to_property = [
|
||||
'Iptc.ObjectName' => 'Title',
|
||||
'Iptc.Caption' => 'Description',
|
||||
'Iptc.ByLine' => 'Creator',
|
||||
'Iptc.Copyright' => 'Rights',
|
||||
'Iptc.CountryCode' => 'CountryCode',
|
||||
'Iptc.CountryName' => 'Country',
|
||||
'Iptc.ProvinceState' => 'State',
|
||||
'Iptc.City' => 'City',
|
||||
'Iptc.Keywords' => 'Keywords',
|
||||
];
|
||||
|
||||
# EXIF
|
||||
protected $exif_to_property = [
|
||||
//'' => 'Title',
|
||||
'ImageDescription' => 'Description',
|
||||
'Artist' => 'Creator',
|
||||
'Copyright' => 'Rights',
|
||||
'Make' => 'Make',
|
||||
'Model' => 'Model',
|
||||
'ExposureTime' => 'Exposure',
|
||||
'FNumber' => 'FNumber',
|
||||
'MaxApertureValue' => 'MaxApertureValue',
|
||||
'ExposureProgram' => 'ExposureProgram',
|
||||
'ISOSpeedRatings' => 'ISOSpeedRatings',
|
||||
'DateTimeOriginal' => 'DateTimeOriginal',
|
||||
'ExposureBiasValue' => 'ExposureBiasValue',
|
||||
'MeteringMode' => 'MeteringMode',
|
||||
'FocalLength' => 'FocalLength',
|
||||
//'' => 'Lens',
|
||||
//'' => 'CountryCode',
|
||||
//'' => 'Country',
|
||||
//'' => 'State',
|
||||
//'' => 'City',
|
||||
//'' => 'Keywords'
|
||||
];
|
||||
}
|
|
@ -0,0 +1,385 @@
|
|||
<?php
|
||||
/**
|
||||
* @class imageTools
|
||||
* @brief Image manipulations
|
||||
*
|
||||
* Class to manipulate images. Some methods are based on https://dev.media-box.net/big/
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Images
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class imageTools
|
||||
{
|
||||
/**
|
||||
* Image resource
|
||||
*
|
||||
* @var mixed resource|GdImage|null|false
|
||||
*/
|
||||
public $res;
|
||||
|
||||
/**
|
||||
* Memory limit
|
||||
*
|
||||
* @var mixed float|null|false
|
||||
*/
|
||||
public $memory_limit = null;
|
||||
|
||||
/**
|
||||
* Constructor, no parameters.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
if (!function_exists('imagegd2')) {
|
||||
throw new Exception('GD is not installed');
|
||||
}
|
||||
$this->res = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close
|
||||
*
|
||||
* Destroy image resource
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
if (!empty($this->res)) {
|
||||
imagedestroy($this->res);
|
||||
}
|
||||
|
||||
if ($this->memory_limit) {
|
||||
ini_set('memory_limit', $this->memory_limit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load image
|
||||
*
|
||||
* Loads an image content in memory and set {@link $res} property.
|
||||
*
|
||||
* @param string $filename Image file path
|
||||
*/
|
||||
public function loadImage(string $filename): void
|
||||
{
|
||||
if (!file_exists($filename)) {
|
||||
throw new Exception('Image doest not exists');
|
||||
}
|
||||
|
||||
if (($info = @getimagesize($filename)) !== false) {
|
||||
$this->memoryAllocate(
|
||||
$info[0],
|
||||
$info[1],
|
||||
$info['channels'] ?? 4
|
||||
);
|
||||
|
||||
switch ($info[2]) {
|
||||
case 3: // IMAGETYPE_PNG:
|
||||
$this->res = @imagecreatefrompng($filename);
|
||||
if (!empty($this->res)) {
|
||||
@imagealphablending($this->res, false);
|
||||
@imagesavealpha($this->res, true);
|
||||
}
|
||||
|
||||
break;
|
||||
case 2: // IMAGETYPE_JPEG:
|
||||
$this->res = @imagecreatefromjpeg($filename);
|
||||
|
||||
break;
|
||||
case 1: // IMAGETYPE_GIF:
|
||||
$this->res = @imagecreatefromgif($filename);
|
||||
|
||||
break;
|
||||
case 18: // IMAGETYPE_WEBP:
|
||||
if (function_exists('imagecreatefromwebp')) {
|
||||
$this->res = @imagecreatefromwebp($filename);
|
||||
if (!empty($this->res)) {
|
||||
@imagealphablending($this->res, false);
|
||||
@imagesavealpha($this->res, true);
|
||||
}
|
||||
} else {
|
||||
throw new Exception('WebP image format not supported');
|
||||
}
|
||||
|
||||
break;
|
||||
case 19: // IMAGETYPE_AVIF:
|
||||
if (function_exists('imagecreatefromavif')) {
|
||||
// PHP 8.1+
|
||||
$this->res = @imagecreatefromavif($filename);
|
||||
if (!empty($this->res)) {
|
||||
@imagealphablending($this->res, false);
|
||||
@imagesavealpha($this->res, true);
|
||||
}
|
||||
} else {
|
||||
throw new Exception('AVIF image format not supported');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->res)) {
|
||||
throw new Exception('Unable to load image');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Image width
|
||||
*
|
||||
* @return int Image width
|
||||
*/
|
||||
public function getW(): int
|
||||
{
|
||||
return imagesx($this->res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Image height
|
||||
*
|
||||
* @return int Image height
|
||||
*/
|
||||
public function getH(): int
|
||||
{
|
||||
return imagesy($this->res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate memory
|
||||
*
|
||||
* @param int $width The width
|
||||
* @param int $height The height
|
||||
* @param int $bpp The bits per pixel
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function memoryAllocate(int $width, int $height, int $bpp = 4)
|
||||
{
|
||||
$mem_used = function_exists('memory_get_usage') ? @memory_get_usage() : 4000000;
|
||||
$mem_limit = @ini_get('memory_limit');
|
||||
if ($mem_limit && trim((string) $mem_limit) === '-1' || !files::str2bytes($mem_limit)) {
|
||||
// Cope with memory_limit set to -1 in PHP.ini
|
||||
return;
|
||||
}
|
||||
if ($mem_used && $mem_limit) {
|
||||
$mem_limit = files::str2bytes($mem_limit);
|
||||
$mem_avail = $mem_limit - $mem_used - (512 * 1024);
|
||||
|
||||
$mem_needed = $width * $height * $bpp;
|
||||
|
||||
if ($mem_needed > $mem_avail) {
|
||||
if (@ini_set('memory_limit', (string) ($mem_limit + $mem_needed + $mem_used)) === false) {
|
||||
throw new Exception(__('Not enough memory to open image.'));
|
||||
}
|
||||
|
||||
if (!$this->memory_limit) {
|
||||
$this->memory_limit = $mem_limit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Image output
|
||||
*
|
||||
* Returns image content in a file or as HTML output (with headers)
|
||||
*
|
||||
* @param string $type Image type (png, jpg, webp or avif)
|
||||
* @param string|null $file Output file. If null, output will be echoed in STDOUT
|
||||
* @param int $qual JPEG image quality
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function output(string $type = 'png', ?string $file = null, int $qual = 90)
|
||||
{
|
||||
if (!$file) {
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
|
||||
header('Pragma: no-cache');
|
||||
switch (strtolower($type)) {
|
||||
case 'png':
|
||||
header('Content-type: image/png');
|
||||
imagepng($this->res);
|
||||
|
||||
return true;
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
header('Content-type: image/jpeg');
|
||||
imagejpeg($this->res, null, $qual);
|
||||
|
||||
return true;
|
||||
case 'wepb':
|
||||
if (function_exists('imagewebp')) {
|
||||
header('Content-type: image/webp');
|
||||
imagewebp($this->res, null, $qual);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
case 'avif':
|
||||
if (function_exists('imageavif')) {
|
||||
// PHP 8.1+
|
||||
header('Content-type: image/avif');
|
||||
imageavif($this->res, null, $qual);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} elseif (is_writable(dirname($file))) {
|
||||
switch (strtolower($type)) {
|
||||
case 'png':
|
||||
return imagepng($this->res, $file);
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
return imagejpeg($this->res, $file, $qual);
|
||||
case 'webp':
|
||||
if (function_exists('imagewebp')) {
|
||||
return imagewebp($this->res, $file, $qual);
|
||||
}
|
||||
|
||||
return false;
|
||||
case 'avif':
|
||||
if (function_exists('imageavif')) {
|
||||
return imageavif($this->res, $file, $qual);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize image
|
||||
*
|
||||
* @param mixed $width Image width (px or percent)
|
||||
* @param mixed $height Image height (px or percent)
|
||||
* @param string $mode Crop mode (force, crop, ratio)
|
||||
* @param boolean $expand Allow resize of image
|
||||
*
|
||||
* @return true
|
||||
*/
|
||||
public function resize($width, $height, string $mode = 'ratio', bool $expand = false)
|
||||
{
|
||||
$computed_height = 0;
|
||||
$computed_width = 0;
|
||||
|
||||
$imgage_width = $this->getW();
|
||||
$imgage_height = $this->getH();
|
||||
|
||||
if (strpos((string) $width, '%', 0)) {
|
||||
$width = $imgage_width * $width / 100;
|
||||
}
|
||||
|
||||
if (strpos((string) $height, '%', 0)) {
|
||||
$height = $imgage_height * $height / 100;
|
||||
}
|
||||
|
||||
$ratio = $imgage_width / $imgage_height;
|
||||
|
||||
// Guess resize
|
||||
if ($mode === 'ratio') {
|
||||
$computed_width = 99999;
|
||||
if ($height > 0) {
|
||||
$computed_height = $height;
|
||||
$computed_width = $computed_height * $ratio;
|
||||
}
|
||||
if ($width > 0 && $computed_width > $width) {
|
||||
$computed_width = $width;
|
||||
$computed_height = $computed_width / $ratio;
|
||||
}
|
||||
|
||||
if (!$expand && $computed_width > $imgage_width) {
|
||||
$computed_width = $imgage_width;
|
||||
$computed_height = $imgage_height;
|
||||
}
|
||||
} else {
|
||||
// Crop source image
|
||||
$computed_width = $width;
|
||||
$computed_height = $height;
|
||||
}
|
||||
|
||||
if ($mode === 'force') {
|
||||
if ($width > 0) {
|
||||
$computed_width = $width;
|
||||
} else {
|
||||
$computed_width = $height * $ratio;
|
||||
}
|
||||
|
||||
if ($height > 0) {
|
||||
$computed_height = $height;
|
||||
} else {
|
||||
$computed_height = $width / $ratio;
|
||||
}
|
||||
|
||||
if (!$expand && $computed_width > $imgage_width) {
|
||||
$computed_width = $imgage_width;
|
||||
$computed_height = $imgage_height;
|
||||
}
|
||||
|
||||
$crop_width = $imgage_width;
|
||||
$crop_height = $imgage_height;
|
||||
$offset_width = 0;
|
||||
$offset_height = 0;
|
||||
} else {
|
||||
// Guess real viewport of image
|
||||
$innerRatio = $computed_width / $computed_height;
|
||||
if ($ratio >= $innerRatio) {
|
||||
$crop_height = $imgage_height;
|
||||
$crop_width = $imgage_height * $innerRatio;
|
||||
$offset_height = 0;
|
||||
$offset_width = ($imgage_width - $crop_width) / 2;
|
||||
} else {
|
||||
$crop_width = $imgage_width;
|
||||
$crop_height = $imgage_width / $innerRatio;
|
||||
$offset_width = 0;
|
||||
$offset_height = ($imgage_height - $crop_height) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
if ($computed_width < 1) {
|
||||
$computed_width = 1;
|
||||
}
|
||||
if ($computed_height < 1) {
|
||||
$computed_height = 1;
|
||||
}
|
||||
|
||||
// convert float to int
|
||||
settype($offset_width, 'int');
|
||||
settype($offset_height, 'int');
|
||||
settype($computed_width, 'int');
|
||||
settype($computed_height, 'int');
|
||||
settype($crop_width, 'int');
|
||||
settype($crop_height, 'int');
|
||||
|
||||
// truecolor is 24 bit RGB, ie. 3 bytes per pixel.
|
||||
$this->memoryAllocate($computed_width, $computed_height, 3);
|
||||
|
||||
$dest = imagecreatetruecolor($computed_width, $computed_height);
|
||||
|
||||
// Fill image with neutral gray (#808080)
|
||||
imagefill($dest, 0, 0, imagecolorallocate($dest, 128, 128, 128));
|
||||
|
||||
// Disable blending mode
|
||||
@imagealphablending($dest, false);
|
||||
|
||||
// Preserve alpha channel of image
|
||||
@imagesavealpha($dest, true);
|
||||
|
||||
// Copy and resize (with resampling) from source to destination
|
||||
imagecopyresampled($dest, $this->res, 0, 0, $offset_width, $offset_height, $computed_width, $computed_height, $crop_width, $crop_height);
|
||||
|
||||
imagedestroy($this->res);
|
||||
$this->res = $dest;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
/**
|
||||
* @class mail
|
||||
* @brief Email utilities
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Mail
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class mail
|
||||
{
|
||||
/**
|
||||
* Send email
|
||||
*
|
||||
* Sends email to destination. If a function called _mail() exists it will
|
||||
* be used instead of PHP mail() function. _mail() function should have the
|
||||
* same signature. Headers could be provided as a string or an array.
|
||||
*
|
||||
* @param string $to Email destination
|
||||
* @param string $subject Email subject
|
||||
* @param string $message Email message
|
||||
* @param string|array $headers Email headers
|
||||
* @param string $params UNIX mail additionnal parameters
|
||||
*
|
||||
* @return boolean true on success
|
||||
*/
|
||||
public static function sendMail(string $to, string $subject, string $message, $headers = null, ?string $params = null): bool
|
||||
{
|
||||
/**
|
||||
* User defined mail function
|
||||
*
|
||||
* @var callable $user_defined_mail
|
||||
*/
|
||||
$user_defined_mail = function_exists('_mail') ? '_mail' : null;
|
||||
|
||||
$eol = trim((string) ini_get('sendmail_path')) ? "\n" : "\r\n";
|
||||
|
||||
if (is_array($headers)) {
|
||||
$headers = implode($eol, $headers);
|
||||
}
|
||||
|
||||
if ($user_defined_mail == null) {
|
||||
if (!@mail($to, $subject, $message, $headers, $params)) {
|
||||
throw new Exception('Unable to send email');
|
||||
}
|
||||
} else {
|
||||
$user_defined_mail($to, $subject, $message, $headers, $params);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Host MX
|
||||
*
|
||||
* Returns MX records sorted by weight for a given host.
|
||||
*
|
||||
* @param string $host Hostname
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public static function getMX(string $host)
|
||||
{
|
||||
if (!getmxrr($host, $mx_hosts, $mx_weights) || count($mx_hosts)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$res = array_combine($mx_hosts, $mx_weights);
|
||||
asort($res);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* B64 header
|
||||
*
|
||||
* Encodes given string as a base64 mail header.
|
||||
*
|
||||
* @param string $str String to encode
|
||||
* @param string $charset Charset (default UTF-8)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function B64Header(string $str, string $charset = 'UTF-8'): string
|
||||
{
|
||||
if (!preg_match('/[^\x00-\x3C\x3E-\x7E]/', $str)) {
|
||||
return $str;
|
||||
}
|
||||
|
||||
return '=?' . $charset . '?B?' . base64_encode($str) . '?=';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,237 @@
|
|||
<?php
|
||||
/**
|
||||
* @class socketMail
|
||||
* @brief Send email through socket
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Mail
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class socketMail
|
||||
{
|
||||
/**
|
||||
* Socket handle
|
||||
*
|
||||
* @var resource|null|false
|
||||
*/
|
||||
public static $fp;
|
||||
|
||||
/**
|
||||
* Connection timeout (in seconds)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public static $timeout = 10;
|
||||
|
||||
/**
|
||||
* SMTP Relay to user
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $smtp_relay = null;
|
||||
|
||||
/**
|
||||
* Send email through socket
|
||||
*
|
||||
* This static method sends an email through a simple socket connection.
|
||||
* If {@link $smtp_relay} is set, it will be used as a relay to send the
|
||||
* email. Instead, email is sent directly to MX host of domain.
|
||||
*
|
||||
* @param string $to Email destination
|
||||
* @param string $subject Email subject
|
||||
* @param string $message Email message
|
||||
* @param string|array $headers Email headers
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function mail(string $to, string $subject, string $message, $headers = null): void
|
||||
{
|
||||
if (!is_null($headers) && !is_array($headers)) {
|
||||
$headers = [$headers];
|
||||
}
|
||||
$from = self::getFrom($headers);
|
||||
|
||||
$from_host = explode('@', $from);
|
||||
$from_host = $from_host[1];
|
||||
|
||||
$to_host = explode('@', $to);
|
||||
$to_host = $to_host[1];
|
||||
|
||||
if (self::$smtp_relay != null) {
|
||||
$mx = [gethostbyname(self::$smtp_relay) => 1];
|
||||
} else {
|
||||
$mx = mail::getMX($to_host);
|
||||
}
|
||||
|
||||
foreach (array_keys($mx) as $mx_host) {
|
||||
self::$fp = @fsockopen($mx_host, 25, $errno, $errstr, self::$timeout);
|
||||
|
||||
if (self::$fp !== false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_resource(self::$fp)) {
|
||||
self::$fp = null;
|
||||
|
||||
throw new Exception('Unable to open socket');
|
||||
}
|
||||
|
||||
# We need to read the first line
|
||||
fgets(self::$fp);
|
||||
|
||||
$data = '';
|
||||
# HELO cmd
|
||||
if (!self::cmd('HELO ' . $from_host, $data)) {
|
||||
self::quit();
|
||||
|
||||
throw new Exception($data);
|
||||
}
|
||||
|
||||
# MAIL FROM: <...>
|
||||
if (!self::cmd('MAIL FROM: <' . $from . '>', $data)) {
|
||||
self::quit();
|
||||
|
||||
throw new Exception($data);
|
||||
}
|
||||
|
||||
# RCPT TO: <...>
|
||||
if (!self::cmd('RCPT TO: <' . $to . '>', $data)) {
|
||||
self::quit();
|
||||
|
||||
throw new Exception($data);
|
||||
}
|
||||
|
||||
# Compose mail and send it with DATA
|
||||
$buffer = 'Return-Path: <' . $from . ">\r\n" .
|
||||
'To: <' . $to . ">\r\n" .
|
||||
'Subject: ' . $subject . "\r\n";
|
||||
|
||||
foreach ($headers as $header) {
|
||||
$buffer .= $header . "\r\n";
|
||||
}
|
||||
|
||||
$buffer .= "\r\n\r\n" . $message;
|
||||
|
||||
if (!self::sendMessage($buffer, $data)) {
|
||||
self::quit();
|
||||
|
||||
throw new Exception($data);
|
||||
}
|
||||
|
||||
self::quit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the from.
|
||||
*
|
||||
* @param array $headers The headers
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return string The from.
|
||||
*/
|
||||
private static function getFrom(?array $headers): string
|
||||
{
|
||||
if (!is_null($headers)) {
|
||||
// Try to find a from:… in header(s)
|
||||
foreach ($headers as $header) {
|
||||
$from = '';
|
||||
|
||||
if (preg_match('/^from: (.+?)$/msi', $header, $m)) {
|
||||
$from = trim((string) $m[1]);
|
||||
}
|
||||
|
||||
if (preg_match('/(?:<)(.+?)(?:$|>)/si', $from, $m)) {
|
||||
$from = trim((string) $m[1]);
|
||||
} elseif (preg_match('/^(.+?)\(/si', $from, $m)) {
|
||||
$from = trim((string) $m[1]);
|
||||
} elseif (!text::isEmail($from)) {
|
||||
$from = '';
|
||||
}
|
||||
|
||||
if ($from !== '') {
|
||||
return $from;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Is a from set in configuration options ?
|
||||
$from = trim((string) ini_get('sendmail_from'));
|
||||
if ($from !== '') {
|
||||
return $from;
|
||||
}
|
||||
|
||||
throw new Exception('No valid from e-mail address');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send SMTP command
|
||||
*
|
||||
* @param string $out The out
|
||||
* @param string $data The received data
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function cmd(string $out, string &$data = ''): bool
|
||||
{
|
||||
fwrite(self::$fp, $out . "\r\n");
|
||||
$data = self::data();
|
||||
|
||||
if (substr($data, 0, 3) != '250') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data from opened stream
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function data(): string
|
||||
{
|
||||
$buffer = '';
|
||||
stream_set_timeout(self::$fp, 2);
|
||||
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$buffer .= fgets(self::$fp, 1024);
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message body.
|
||||
*
|
||||
* @param string $msg The message
|
||||
* @param string $data The data
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function sendMessage(string $msg, string &$data): bool
|
||||
{
|
||||
$msg .= "\r\n.";
|
||||
|
||||
self::cmd('DATA', $data);
|
||||
|
||||
if (substr($data, 0, 3) != '354') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::cmd($msg, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send QUIT command and close socket handle
|
||||
*/
|
||||
private static function quit(): void
|
||||
{
|
||||
self::cmd('QUIT');
|
||||
fclose(self::$fp);
|
||||
self::$fp = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,315 @@
|
|||
<?php
|
||||
/**
|
||||
* @class feedParser
|
||||
* @brief Feed parser
|
||||
*
|
||||
* This class can read RSS 1.0, RSS 2.0, Atom 0.3 and Atom 1.0 feeds. Works with
|
||||
* {@link feedReader}
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Feeds
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class feedParser
|
||||
{
|
||||
/**
|
||||
* Feed type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $feed_type;
|
||||
|
||||
/**
|
||||
* Feed title
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $title;
|
||||
|
||||
/**
|
||||
* Feed link
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $link;
|
||||
|
||||
/**
|
||||
* Feed description
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* Feed publication date
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $pubdate;
|
||||
|
||||
/**
|
||||
* Feed generator
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $generator;
|
||||
|
||||
/**
|
||||
* Feed items
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $items = [];
|
||||
|
||||
/**
|
||||
* Feed XML content
|
||||
*
|
||||
* @var SimpleXMLElement|false
|
||||
*/
|
||||
protected $xml;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Takes some <var>$data</var> as input. Returns false if data is
|
||||
* not a valid XML stream. If everything's fine, feed is parsed and items
|
||||
* are in {@link $items} property.
|
||||
*
|
||||
* @param string $data XML stream
|
||||
*/
|
||||
public function __construct(string $data)
|
||||
{
|
||||
$this->xml = @simplexml_load_string($data);
|
||||
|
||||
if (!$this->xml) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (preg_match('/<rdf:RDF/', (string) $data)) {
|
||||
$this->parseRSSRDF();
|
||||
} elseif (preg_match('/<rss/', (string) $data)) {
|
||||
$this->parseRSS();
|
||||
} elseif (preg_match('!www.w3.org/2005/Atom!', (string) $data)) {
|
||||
$this->parseAtom10();
|
||||
} else {
|
||||
$this->parseAtom03();
|
||||
}
|
||||
|
||||
unset($data, $this->xml);
|
||||
}
|
||||
|
||||
/**
|
||||
* RSS 1.0 parser.
|
||||
*/
|
||||
protected function parseRSSRDF(): void
|
||||
{
|
||||
$this->feed_type = 'rss 1.0 (rdf)';
|
||||
|
||||
$this->title = (string) $this->xml->channel->title;
|
||||
$this->link = (string) $this->xml->channel->link;
|
||||
$this->description = (string) $this->xml->channel->description;
|
||||
$this->pubdate = (string) $this->xml->channel->children('http://purl.org/dc/elements/1.1/')->date;
|
||||
|
||||
# Feed generator agent
|
||||
$generator = $this->xml->channel->children('http://webns.net/mvcb/')->generatorAgent;
|
||||
if ($generator) {
|
||||
$generator = $generator->attributes('http://www.w3.org/1999/02/22-rdf-syntax-ns#');
|
||||
$this->generator = (string) $generator['resource'];
|
||||
}
|
||||
|
||||
if (empty($this->xml->item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->xml->item as $i) {
|
||||
$item = new stdClass();
|
||||
$item->title = (string) $i->title;
|
||||
$item->link = (string) $i->link;
|
||||
$item->creator = (string) $i->children('http://purl.org/dc/elements/1.1/')->creator;
|
||||
$item->description = (string) $i->description;
|
||||
$item->content = (string) $i->children('http://purl.org/rss/1.0/modules/content/')->encoded;
|
||||
$item->subject = $this->nodes2array($i->children('http://purl.org/dc/elements/1.1/')->subject);
|
||||
$item->pubdate = (string) $i->children('http://purl.org/dc/elements/1.1/')->date;
|
||||
$item->TS = strtotime($item->pubdate);
|
||||
|
||||
$item->guid = (string) $item->link;
|
||||
if (!empty($i->attributes('http://www.w3.org/1999/02/22-rdf-syntax-ns#')->about)) {
|
||||
$item->guid = (string) $i->attributes('http://www.w3.org/1999/02/22-rdf-syntax-ns#')->about;
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RSS 2.0 parser
|
||||
*/
|
||||
protected function parseRSS(): void
|
||||
{
|
||||
$this->feed_type = 'rss ' . $this->xml['version'];
|
||||
|
||||
$this->title = (string) $this->xml->channel->title;
|
||||
$this->link = (string) $this->xml->channel->link;
|
||||
$this->description = (string) $this->xml->channel->description;
|
||||
$this->pubdate = (string) $this->xml->channel->pubDate;
|
||||
|
||||
$this->generator = (string) $this->xml->channel->generator;
|
||||
|
||||
if (empty($this->xml->channel->item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->xml->channel->item as $i) {
|
||||
$item = new stdClass();
|
||||
$item->title = (string) $i->title;
|
||||
$item->link = (string) $i->link;
|
||||
$item->creator = (string) $i->children('http://purl.org/dc/elements/1.1/')->creator;
|
||||
$item->description = (string) $i->description;
|
||||
$item->content = (string) $i->children('http://purl.org/rss/1.0/modules/content/')->encoded;
|
||||
|
||||
$item->subject = array_merge(
|
||||
$this->nodes2array($i->children('http://purl.org/dc/elements/1.1/')->subject),
|
||||
$this->nodes2array($i->category)
|
||||
);
|
||||
|
||||
$item->pubdate = (string) $i->pubDate;
|
||||
if (!$item->pubdate && !empty($i->children('http://purl.org/dc/elements/1.1/')->date)) {
|
||||
$item->pubdate = (string) $i->children('http://purl.org/dc/elements/1.1/')->date;
|
||||
}
|
||||
|
||||
$item->TS = strtotime($item->pubdate);
|
||||
|
||||
$item->guid = (string) $item->link;
|
||||
if (!empty($i->guid)) {
|
||||
$item->guid = (string) $i->guid;
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Atom 0.3 parser
|
||||
*/
|
||||
protected function parseAtom03(): void
|
||||
{
|
||||
$this->feed_type = 'atom 0.3';
|
||||
|
||||
$this->title = (string) $this->xml->title;
|
||||
$this->description = (string) $this->xml->subtitle;
|
||||
$this->pubdate = (string) $this->xml->modified;
|
||||
|
||||
$this->generator = (string) $this->xml->generator;
|
||||
|
||||
foreach ($this->xml->link as $link) {
|
||||
if ($link['rel'] == 'alternate' && ($link['type'] == 'text/html' || $link['type'] == 'application/xhtml+xml')) {
|
||||
$this->link = (string) $link['href'];
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->xml->entry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->xml->entry as $i) {
|
||||
$item = new stdClass();
|
||||
|
||||
foreach ($i->link as $link) {
|
||||
if ($link['rel'] == 'alternate' && ($link['type'] == 'text/html' || $link['type'] == 'application/xhtml+xml')) {
|
||||
$item->link = (string) $link['href'];
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$item->link = (string) $link['href'];
|
||||
}
|
||||
|
||||
$item->title = (string) $i->title;
|
||||
$item->creator = (string) $i->author->name;
|
||||
$item->description = (string) $i->summary;
|
||||
$item->content = (string) $i->content;
|
||||
$item->subject = $this->nodes2array($i->children('http://purl.org/dc/elements/1.1/')->subject);
|
||||
$item->pubdate = (string) $i->modified;
|
||||
$item->TS = strtotime($item->pubdate);
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Atom 1.0 parser
|
||||
*/
|
||||
protected function parseAtom10(): void
|
||||
{
|
||||
$this->feed_type = 'atom 1.0';
|
||||
|
||||
$this->title = (string) $this->xml->title;
|
||||
$this->description = (string) $this->xml->subtitle;
|
||||
$this->pubdate = (string) $this->xml->updated;
|
||||
|
||||
$this->generator = (string) $this->xml->generator;
|
||||
|
||||
foreach ($this->xml->link as $link) {
|
||||
if ($link['rel'] == 'alternate' && ($link['type'] == 'text/html' || $link['type'] == 'application/xhtml+xml')) {
|
||||
$this->link = (string) $link['href'];
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->xml->entry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->xml->entry as $i) {
|
||||
$item = new stdClass();
|
||||
|
||||
foreach ($i->link as $link) {
|
||||
if ($link['rel'] == 'alternate' && ($link['type'] == 'text/html' || $link['type'] == 'application/xhtml+xml')) {
|
||||
$item->link = (string) $link['href'];
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$item->link = (string) $link['href'];
|
||||
}
|
||||
|
||||
$item->title = (string) $i->title;
|
||||
$item->creator = (string) $i->author->name;
|
||||
$item->description = (string) $i->summary;
|
||||
$item->content = (string) $i->content;
|
||||
$item->subject = $this->nodes2array($i->children('http://purl.org/dc/elements/1.1/')->subject);
|
||||
$item->pubdate = !empty($i->published) ? (string) $i->published : (string) $i->updated;
|
||||
$item->TS = strtotime($item->pubdate);
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SimpleXML to array
|
||||
*
|
||||
* Converts a SimpleXMLElement to an array.
|
||||
*
|
||||
* @param SimpleXMLElement $node SimpleXML Node
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function nodes2array(SimpleXMLElement &$node): array
|
||||
{
|
||||
if (empty($node)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$res = [];
|
||||
foreach ($node as $v) {
|
||||
$res[] = (string) $v;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,285 @@
|
|||
<?php
|
||||
/**
|
||||
* @class feedReader
|
||||
* @brief Feed Reader
|
||||
*
|
||||
* Features:
|
||||
*
|
||||
* - Reads RSS 1.0 (rdf), RSS 2.0 and Atom feeds.
|
||||
* - HTTP cache negociation support
|
||||
* - Cache TTL.
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Feeds
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class feedReader extends netHttp
|
||||
{
|
||||
/**
|
||||
* User agent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $user_agent = 'Clearbricks Feed Reader/0.2';
|
||||
|
||||
/**
|
||||
* Connection timeout (in seconds)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $timeout = 5;
|
||||
|
||||
/**
|
||||
* HTTP Cache validators
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected $validators = null;
|
||||
|
||||
/**
|
||||
* Cache directory path
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $cache_dir = null;
|
||||
|
||||
/**
|
||||
* Cache file prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $cache_file_prefix = 'cbfeed';
|
||||
|
||||
/**
|
||||
* Cache TTL (must be a negative string value as "-30 minutes")
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $cache_ttl = '-30 minutes';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Does nothing. See {@link parse()} method for URL handling.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Feed
|
||||
*
|
||||
* Returns a new feedParser instance for given URL or false if source URL is
|
||||
* not a valid feed.
|
||||
*
|
||||
* @uses feedParser
|
||||
*
|
||||
* @param string $url Feed URL
|
||||
*
|
||||
* @return feedParser|false
|
||||
*/
|
||||
public function parse(string $url)
|
||||
{
|
||||
$this->validators = [];
|
||||
if ($this->cache_dir) {
|
||||
return $this->withCache($url);
|
||||
}
|
||||
if (!$this->getFeed($url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->getStatus() != '200') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new feedParser($this->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick Parse
|
||||
*
|
||||
* This static method returns a new {@link feedParser} instance for given URL. If a
|
||||
* <var>$cache_dir</var> is specified, cache will be activated.
|
||||
*
|
||||
* @param string $url Feed URL
|
||||
* @param string $cache_dir Cache directory
|
||||
*
|
||||
* @return feedParser|false
|
||||
*/
|
||||
public static function quickParse(string $url, ?string $cache_dir = null)
|
||||
{
|
||||
$parser = new self();
|
||||
if ($cache_dir) {
|
||||
$parser->setCacheDir($cache_dir);
|
||||
}
|
||||
|
||||
return $parser->parse($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Cache Directory
|
||||
*
|
||||
* Returns true and sets {@link $cache_dir} property if <var>$dir</var> is
|
||||
* a writable directory. Otherwise, returns false.
|
||||
*
|
||||
* @param string $dir Cache directory
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function setCacheDir(string $dir): bool
|
||||
{
|
||||
$this->cache_dir = null;
|
||||
|
||||
if (!empty($dir) && is_dir($dir) && is_writeable($dir)) {
|
||||
$this->cache_dir = $dir;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Cache TTL
|
||||
*
|
||||
* Sets cache TTL. <var>$str</var> is a interval readable by strtotime
|
||||
* (-3 minutes, -2 hours, etc.)
|
||||
*
|
||||
* @param string $str TTL
|
||||
*/
|
||||
public function setCacheTTL(string $str): void
|
||||
{
|
||||
$str = trim($str);
|
||||
if (!empty($str)) {
|
||||
if (substr($str, 0, 1) != '-') {
|
||||
$str = '-' . $str;
|
||||
}
|
||||
$this->cache_ttl = $str;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed Content
|
||||
*
|
||||
* Returns feed content for given URL.
|
||||
*
|
||||
* @param string $url Feed URL
|
||||
*
|
||||
* @return string|boolean
|
||||
*/
|
||||
protected function getFeed(string $url)
|
||||
{
|
||||
$ssl = false;
|
||||
$host = '';
|
||||
$port = 0;
|
||||
$path = '';
|
||||
$user = '';
|
||||
$pass = '';
|
||||
|
||||
if (!self::readURL($url, $ssl, $host, $port, $path, $user, $pass)) {
|
||||
return false;
|
||||
}
|
||||
$this->setHost($host, $port);
|
||||
$this->useSSL($ssl);
|
||||
$this->setAuthorization($user, $pass);
|
||||
|
||||
return $this->get($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache content
|
||||
*
|
||||
* Returns feedParser object from cache if present or write it to cache and
|
||||
* returns result.
|
||||
*
|
||||
* @param string $url Feed URL
|
||||
*
|
||||
* @return feedParser|false
|
||||
*/
|
||||
protected function withCache(string $url)
|
||||
{
|
||||
$url_md5 = md5($url);
|
||||
$cached_file = sprintf(
|
||||
'%s/%s/%s/%s/%s.php',
|
||||
$this->cache_dir,
|
||||
$this->cache_file_prefix,
|
||||
substr($url_md5, 0, 2),
|
||||
substr($url_md5, 2, 2),
|
||||
$url_md5
|
||||
);
|
||||
|
||||
$may_use_cached = false;
|
||||
|
||||
if (@file_exists($cached_file)) {
|
||||
$may_use_cached = true;
|
||||
$timestamp = @filemtime($cached_file);
|
||||
if ($timestamp > strtotime($this->cache_ttl)) {
|
||||
# Direct cache
|
||||
return unserialize(file_get_contents($cached_file));
|
||||
}
|
||||
|
||||
$this->validators['IfModifiedSince'] = gmdate('D, d M Y H:i:s', $timestamp) . ' GMT';
|
||||
}
|
||||
|
||||
if (!$this->getFeed($url)) {
|
||||
if ($may_use_cached) {
|
||||
# connection failed - fetched from cache
|
||||
return unserialize(file_get_contents($cached_file));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($this->getStatus()) {
|
||||
case '304':
|
||||
@files::touch($cached_file);
|
||||
|
||||
return unserialize(file_get_contents($cached_file));
|
||||
case '200':
|
||||
$feed = new feedParser($this->getContent());
|
||||
|
||||
try {
|
||||
files::makeDir(dirname($cached_file), true);
|
||||
} catch (Exception $e) {
|
||||
return $feed;
|
||||
}
|
||||
|
||||
if ($fp = @fopen($cached_file, 'wb')) {
|
||||
fwrite($fp, serialize($feed));
|
||||
fclose($fp);
|
||||
files::inheritChmod($cached_file);
|
||||
}
|
||||
|
||||
return $feed;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build request
|
||||
*
|
||||
* Adds HTTP cache headers to common headers.
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function buildRequest(): array
|
||||
{
|
||||
$headers = parent::buildRequest();
|
||||
|
||||
# Cache validators
|
||||
if (!empty($this->validators)) {
|
||||
if (isset($this->validators['IfModifiedSince'])) {
|
||||
$headers[] = 'If-Modified-Since: ' . $this->validators['IfModifiedSince'];
|
||||
}
|
||||
if (isset($this->validators['IfNoneMatch'])) {
|
||||
$headers[] = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,359 @@
|
|||
<?php
|
||||
/**
|
||||
* @class netSocket
|
||||
* @brief Network base
|
||||
*
|
||||
* This class handles network socket through an iterator.
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Network
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class netSocket
|
||||
{
|
||||
/**
|
||||
* Server host
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_host;
|
||||
|
||||
/**
|
||||
* Server port
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $_port;
|
||||
|
||||
/**
|
||||
* Server transport
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_transport = '';
|
||||
|
||||
/**
|
||||
* Connection timeout
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $_timeout;
|
||||
|
||||
/**
|
||||
* Resource handler
|
||||
*
|
||||
* @var resource|null
|
||||
*/
|
||||
protected $_handle;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param string $host Server host
|
||||
* @param int $port Server port
|
||||
* @param int $timeout Connection timeout
|
||||
*/
|
||||
public function __construct(string $host, int $port, int $timeout = 10)
|
||||
{
|
||||
$this->_host = $host;
|
||||
$this->_port = abs($port);
|
||||
$this->_timeout = abs($timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Object destructor
|
||||
*
|
||||
* Calls {@link close()} method
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get / Set host
|
||||
*
|
||||
* If <var>$host</var> is set, set {@link $_host} and returns true.
|
||||
* Otherwise, returns {@link $_host} value.
|
||||
*
|
||||
* @param string $host Server host
|
||||
*
|
||||
* @return string|true
|
||||
*/
|
||||
public function host(?string $host = null)
|
||||
{
|
||||
if ($host) {
|
||||
$this->_host = $host;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->_host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get / Set port
|
||||
*
|
||||
* If <var>$port</var> is set, set {@link $_port} and returns true.
|
||||
* Otherwise, returns {@link $_port} value.
|
||||
*
|
||||
* @param int $port Server port
|
||||
*
|
||||
* @return int|true
|
||||
*/
|
||||
public function port(?int $port = null)
|
||||
{
|
||||
if ($port) {
|
||||
$this->_port = abs($port);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->_port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get / Set timeout
|
||||
*
|
||||
* If <var>$timeout</var> is set, set {@link $_timeout} and returns true.
|
||||
* Otherwise, returns {@link $_timeout} value.
|
||||
*
|
||||
* @param int $timeout Connection timeout
|
||||
*
|
||||
* @return int|true
|
||||
*/
|
||||
public function timeout(?int $timeout = null)
|
||||
{
|
||||
if ($timeout) {
|
||||
$this->_timeout = abs($timeout);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->_timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set blocking
|
||||
*
|
||||
* Sets blocking or non-blocking mode on the socket.
|
||||
*
|
||||
* @param bool $block
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function setBlocking(bool $block): bool
|
||||
{
|
||||
if (!$this->isOpen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return stream_set_blocking($this->_handle, $block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open connection.
|
||||
*
|
||||
* Opens socket connection and Returns an object of type {@link netSocketIterator}
|
||||
* which can be iterate with a simple foreach loop.
|
||||
*
|
||||
* @return netSocketIterator|bool
|
||||
*/
|
||||
public function open()
|
||||
{
|
||||
$handle = @fsockopen($this->_transport . $this->_host, $this->_port, $errno, $errstr, $this->_timeout);
|
||||
if (!$handle) {
|
||||
throw new Exception('Socket error: ' . $errstr . ' (' . $errno . ')');
|
||||
}
|
||||
$this->_handle = $handle;
|
||||
|
||||
return $this->iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes socket connection
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
if ($this->isOpen()) {
|
||||
fclose($this->_handle);
|
||||
$this->_handle = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data
|
||||
*
|
||||
* Sends data to current socket and returns an object of type
|
||||
* {@link netSocketIterator} which can be iterate with a simple foreach loop.
|
||||
*
|
||||
* <var>$data</var> can be a string or an array of lines.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* <code>
|
||||
* <?php
|
||||
* $s = new netSocket('www.google.com',80,2);
|
||||
* $s->open();
|
||||
* $data = [
|
||||
* 'GET / HTTP/1.0'
|
||||
* ];
|
||||
* foreach($s->write($data) as $v) {
|
||||
* echo $v."\n";
|
||||
* }
|
||||
* $s->close();
|
||||
* ?>
|
||||
* </code>
|
||||
*
|
||||
* @param string|array $data Data to send
|
||||
*
|
||||
* @return netSocketIterator|false
|
||||
*/
|
||||
public function write($data)
|
||||
{
|
||||
if (!$this->isOpen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_array($data)) {
|
||||
$data = implode("\r\n", $data) . "\r\n\r\n";
|
||||
}
|
||||
|
||||
fwrite($this->_handle, $data);
|
||||
|
||||
return $this->iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush buffer
|
||||
*
|
||||
* Flushes socket write buffer.
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
if (!$this->isOpen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fflush($this->_handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator
|
||||
*
|
||||
* @return netSocketIterator|false
|
||||
*/
|
||||
protected function iterator()
|
||||
{
|
||||
if (!$this->isOpen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new netSocketIterator($this->_handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is open
|
||||
*
|
||||
* Returns true if socket connection is open.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isOpen()
|
||||
{
|
||||
return is_resource($this->_handle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class netSocketIterator
|
||||
* @brief Network socket iterator
|
||||
*
|
||||
* This class offers an iterator for network operations made with
|
||||
* {@link netSocket}.
|
||||
*
|
||||
* @see netSocket::write()
|
||||
*/
|
||||
class netSocketIterator implements Iterator
|
||||
{
|
||||
/**
|
||||
* Socket resource handler
|
||||
*
|
||||
* @var resource
|
||||
*/
|
||||
protected $_handle;
|
||||
|
||||
/**
|
||||
* Current index position
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $_index;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param resource $handle Socket resource handler
|
||||
*/
|
||||
public function __construct(&$handle)
|
||||
{
|
||||
if (!is_resource($handle)) {
|
||||
throw new Exception('Handle is not a resource');
|
||||
}
|
||||
$this->_handle = &$handle;
|
||||
$this->_index = 0;
|
||||
}
|
||||
|
||||
/* Iterator methods
|
||||
--------------------------------------------------- */
|
||||
/**
|
||||
* Rewind
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function rewind()
|
||||
{
|
||||
# Nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid
|
||||
*
|
||||
* @return bool True if EOF of handler
|
||||
*/
|
||||
public function valid(): bool
|
||||
{
|
||||
return !feof($this->_handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move index forward
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function next()
|
||||
{
|
||||
$this->_index++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current index
|
||||
*
|
||||
* @return int Current index
|
||||
*/
|
||||
public function key(): int
|
||||
{
|
||||
return $this->_index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current value
|
||||
*
|
||||
* @return string Current socket response line
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function current()
|
||||
{
|
||||
return fgets($this->_handle, 4096);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,311 @@
|
|||
<?php
|
||||
/**
|
||||
* @class pager
|
||||
* @brief (x)HTML Pager
|
||||
*
|
||||
* This class implements a pager helper to browse any type of results.
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Pager
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class pager
|
||||
{
|
||||
/**
|
||||
* Current page index
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $env;
|
||||
|
||||
/**
|
||||
* Total number of elements
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $nb_elements;
|
||||
|
||||
/**
|
||||
* Number of elements per page
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $nb_per_page;
|
||||
|
||||
/**
|
||||
* Number of pages per group
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $nb_pages_per_group;
|
||||
|
||||
/**
|
||||
* Total number of pages
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $nb_pages;
|
||||
|
||||
/**
|
||||
* Total number of grourps
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $nb_groups;
|
||||
|
||||
/**
|
||||
* Current group index
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $env_group;
|
||||
|
||||
/**
|
||||
* First page index of current group
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $index_group_start;
|
||||
|
||||
/**
|
||||
* Last page index of current group
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $index_group_end;
|
||||
|
||||
/**
|
||||
* Page URI
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $page_url = null;
|
||||
|
||||
/**
|
||||
* First element index of current page
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $index_start;
|
||||
|
||||
/**
|
||||
* Last element index of current page
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $index_end;
|
||||
|
||||
/**
|
||||
* Base URI
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $base_url = null;
|
||||
|
||||
/**
|
||||
* GET param name for current page
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $var_page = 'page';
|
||||
|
||||
/**
|
||||
* Current page format (HTML)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $html_cur_page = '<strong>%s</strong>';
|
||||
|
||||
/**
|
||||
* Link separator
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $html_link_sep = '-';
|
||||
|
||||
/**
|
||||
* Previous HTML code
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $html_prev = '«prev.';
|
||||
|
||||
/**
|
||||
* Next HTML code
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $html_next = 'next»';
|
||||
|
||||
/**
|
||||
* Next group HTML code
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $html_prev_grp = '...';
|
||||
|
||||
/**
|
||||
* Previous group HTML code
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $html_next_grp = '...';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param int $env Current page index
|
||||
* @param int $nb_elements Total number of elements
|
||||
* @param int $nb_per_page Number of items per page
|
||||
* @param int $nb_pages_per_group Number of pages per group
|
||||
*/
|
||||
public function __construct(int $env, int $nb_elements, int $nb_per_page = 10, int $nb_pages_per_group = 10)
|
||||
{
|
||||
$this->env = abs($env);
|
||||
$this->nb_elements = abs($nb_elements);
|
||||
$this->nb_per_page = abs($nb_per_page);
|
||||
$this->nb_pages_per_group = abs($nb_pages_per_group);
|
||||
|
||||
// Pages count
|
||||
$this->nb_pages = (int) ceil($this->nb_elements / $this->nb_per_page);
|
||||
|
||||
// Fix env value
|
||||
if ($this->env > $this->nb_pages || $this->env < 1) {
|
||||
$this->env = 1;
|
||||
}
|
||||
|
||||
// Groups count
|
||||
$this->nb_groups = (int) ceil($this->nb_pages / $this->nb_pages_per_group);
|
||||
|
||||
// Page first element index
|
||||
$this->index_start = ($this->env - 1) * $this->nb_per_page;
|
||||
|
||||
// Page last element index
|
||||
$this->index_end = $this->index_start + $this->nb_per_page - 1;
|
||||
if ($this->index_end >= $this->nb_elements) {
|
||||
$this->index_end = $this->nb_elements - 1;
|
||||
}
|
||||
|
||||
// Current group
|
||||
$this->env_group = (int) ceil($this->env / $this->nb_pages_per_group);
|
||||
|
||||
// Group first page index
|
||||
$this->index_group_start = ($this->env_group - 1) * $this->nb_pages_per_group + 1;
|
||||
|
||||
// Group last page index
|
||||
$this->index_group_end = $this->index_group_start + $this->nb_pages_per_group - 1;
|
||||
if ($this->index_group_end > $this->nb_pages) {
|
||||
$this->index_group_end = $this->nb_pages;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pager Links
|
||||
*
|
||||
* Returns pager links
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLinks(): string
|
||||
{
|
||||
$htmlLinks = '';
|
||||
$htmlPrev = '';
|
||||
$htmlNext = '';
|
||||
$htmlPrevGrp = '';
|
||||
$htmlNextGrp = '';
|
||||
|
||||
$this->setURL();
|
||||
|
||||
for ($i = $this->index_group_start; $i <= $this->index_group_end; $i++) {
|
||||
if ($i === $this->env) {
|
||||
$htmlLinks .= sprintf($this->html_cur_page, $i);
|
||||
} else {
|
||||
$htmlLinks .= '<a href="' . sprintf($this->page_url, $i) . '">' . $i . '</a>';
|
||||
}
|
||||
|
||||
if ($i !== $this->index_group_end) {
|
||||
$htmlLinks .= $this->html_link_sep;
|
||||
}
|
||||
}
|
||||
|
||||
# Previous page
|
||||
if ($this->env !== 1) {
|
||||
$htmlPrev = '<a href="' . sprintf($this->page_url, $this->env - 1) . '">' . $this->html_prev . '</a> ';
|
||||
}
|
||||
|
||||
# Next page
|
||||
if ($this->env !== $this->nb_pages) {
|
||||
$htmlNext = ' <a href="' . sprintf($this->page_url, $this->env + 1) . '">' . $this->html_next . '</a>';
|
||||
}
|
||||
|
||||
# Previous group
|
||||
if ($this->env_group != 1) {
|
||||
$htmlPrevGrp = ' <a href="' . sprintf($this->page_url, $this->index_group_start - $this->nb_pages_per_group) . '">' . $this->html_prev_grp . '</a> ';
|
||||
}
|
||||
|
||||
# Next group
|
||||
if ($this->env_group != $this->nb_groups) {
|
||||
$htmlNextGrp = ' <a href="' . sprintf($this->page_url, $this->index_group_end + 1) . '">' . $this->html_next_grp . '</a> ';
|
||||
}
|
||||
|
||||
$res = $htmlPrev .
|
||||
$htmlPrevGrp .
|
||||
$htmlLinks .
|
||||
$htmlNextGrp .
|
||||
$htmlNext;
|
||||
|
||||
return $this->nb_elements > 0 ? $res : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the page URI
|
||||
*/
|
||||
protected function setURL()
|
||||
{
|
||||
if ($this->base_url !== null) {
|
||||
$this->page_url = $this->base_url;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$url = (string) $_SERVER['REQUEST_URI'];
|
||||
|
||||
# Removing session information
|
||||
if (session_id()) {
|
||||
$url = preg_replace('/' . preg_quote(session_name() . '=' . session_id(), '/') . '([&]?)/', '', $url);
|
||||
$url = preg_replace('/&$/', '', $url);
|
||||
}
|
||||
|
||||
# Escape page_url for sprintf
|
||||
$url = str_replace('%', '%%', $url);
|
||||
|
||||
# Changing page ref
|
||||
if (preg_match('/[?&]' . $this->var_page . '=\d+/', $url)) {
|
||||
$url = preg_replace('/([?&]' . $this->var_page . '=)\d+/', '$1%1$d', $url);
|
||||
} elseif (preg_match('/[?]/', $url)) {
|
||||
$url .= '&' . $this->var_page . '=%1$d';
|
||||
} else {
|
||||
$url .= '?' . $this->var_page . '=%1$d';
|
||||
}
|
||||
|
||||
$this->page_url = html::escapeHTML($url);
|
||||
}
|
||||
|
||||
public function debug()
|
||||
{
|
||||
return
|
||||
'Elements per page ........... ' . $this->nb_per_page . "\n" .
|
||||
'Pages per group.............. ' . $this->nb_pages_per_group . "\n" .
|
||||
'Elements count .............. ' . $this->nb_elements . "\n" .
|
||||
'Pages ....................... ' . $this->nb_pages . "\n" .
|
||||
'Groups ...................... ' . $this->nb_groups . "\n\n" .
|
||||
'Current page .................' . $this->env . "\n" .
|
||||
'Start index ................. ' . $this->index_start . "\n" .
|
||||
'End index ................... ' . $this->index_end . "\n" .
|
||||
'Current group ............... ' . $this->env_group . "\n" .
|
||||
'Group first page index ...... ' . $this->index_group_start . "\n" .
|
||||
'Group last page index ....... ' . $this->index_group_end;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,311 @@
|
|||
<?php
|
||||
/**
|
||||
* @class restServer
|
||||
* @brief REST Server
|
||||
*
|
||||
* A very simple REST server implementation
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Rest
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class restServer
|
||||
{
|
||||
/**
|
||||
* Server response (XML)
|
||||
*
|
||||
* @var xmlTag
|
||||
*/
|
||||
public $rsp;
|
||||
|
||||
/**
|
||||
* Server's functions
|
||||
*
|
||||
* @var array of array [callback, xml?]
|
||||
*/
|
||||
public $functions = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->rsp = new xmlTag('rsp');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Function
|
||||
*
|
||||
* This adds a new function to the server. <var>$callback</var> should be
|
||||
* a valid PHP callback. Callback function takes two arguments: GET and
|
||||
* POST values.
|
||||
*
|
||||
* @param string $name Function name
|
||||
* @param callable|array $callback Callback function
|
||||
*/
|
||||
public function addFunction(string $name, $callback): void
|
||||
{
|
||||
if (is_callable($callback)) {
|
||||
$this->functions[$name] = $callback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Function
|
||||
*
|
||||
* This method calls callback named <var>$name</var>.
|
||||
*
|
||||
* @param string $name Function name
|
||||
* @param array $get GET values
|
||||
* @param array $post POST values
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function callFunction(string $name, array $get, array $post)
|
||||
{
|
||||
if (isset($this->functions[$name])) {
|
||||
return call_user_func($this->functions[$name], $get, $post);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main server
|
||||
*
|
||||
* This method creates the main server.
|
||||
*
|
||||
* @param string $encoding Server charset
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function serve(string $encoding = 'UTF-8'): bool
|
||||
{
|
||||
$get = $_GET ?: [];
|
||||
$post = $_POST ?: [];
|
||||
|
||||
if (!isset($_REQUEST['f'])) {
|
||||
$this->rsp->status = 'failed';
|
||||
$this->rsp->message('No function given');
|
||||
$this->getXML($encoding);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($this->functions[$_REQUEST['f']])) {
|
||||
$this->rsp->status = 'failed';
|
||||
$this->rsp->message('Function does not exist');
|
||||
$this->getXML($encoding);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$res = $this->callFunction($_REQUEST['f'], $get, $post);
|
||||
} catch (Exception $e) {
|
||||
$this->rsp->status = 'failed';
|
||||
$this->rsp->message($e->getMessage());
|
||||
$this->getXML($encoding);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->rsp->status = 'ok';
|
||||
$this->rsp->insertNode($res);
|
||||
$this->getXML($encoding);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream the XML data (header and body)
|
||||
*
|
||||
* @param string $encoding The encoding
|
||||
*/
|
||||
private function getXML($encoding = 'UTF-8')
|
||||
{
|
||||
header('Content-Type: text/xml; charset=' . $encoding);
|
||||
echo $this->rsp->toXML(true, $encoding);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class xmlTag
|
||||
*
|
||||
* XML Tree
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage XML
|
||||
*/
|
||||
class xmlTag
|
||||
{
|
||||
/**
|
||||
* XML tag name
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
private $_name;
|
||||
|
||||
/**
|
||||
* XML tag attributes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $_attr = [];
|
||||
|
||||
/**
|
||||
* XML tag nodes (childs)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $_nodes = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Creates the root XML tag named <var>$name</var>. If content is given,
|
||||
* it will be appended to root tag with {@link insertNode()}
|
||||
*
|
||||
* @param string $name Tag name
|
||||
* @param mixed $content Tag content
|
||||
*/
|
||||
public function __construct(?string $name = null, $content = null)
|
||||
{
|
||||
$this->_name = $name;
|
||||
|
||||
if ($content !== null) {
|
||||
$this->insertNode($content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Attribute
|
||||
*
|
||||
* Magic __set method to add an attribute.
|
||||
*
|
||||
* @param string $name Attribute name
|
||||
* @param mixed $value Attribute value
|
||||
*
|
||||
* @see insertAttr()
|
||||
*/
|
||||
public function __set(string $name, mixed $value): void
|
||||
{
|
||||
$this->insertAttr($name, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tag
|
||||
*
|
||||
* This magic __call method appends a tag to XML tree.
|
||||
*
|
||||
* @param string $name Tag name
|
||||
* @param array $args Function arguments, the first one would be tag content
|
||||
*/
|
||||
public function __call(string $name, array $args)
|
||||
{
|
||||
if (!preg_match('#^[a-z_]#', $name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($args[0])) {
|
||||
$args[0] = null;
|
||||
}
|
||||
|
||||
$this->insertNode(new self($name, $args[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add CDATA
|
||||
*
|
||||
* Appends CDATA to current tag.
|
||||
*
|
||||
* @param string $value Tag CDATA content
|
||||
*/
|
||||
public function CDATA(string $value): void
|
||||
{
|
||||
$this->insertNode($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Attribute
|
||||
*
|
||||
* This method adds an attribute to current tag.
|
||||
*
|
||||
* @param string $name Attribute name
|
||||
* @param mixed $value Attribute value
|
||||
*
|
||||
* @see insertAttr()
|
||||
*/
|
||||
public function insertAttr(string $name, mixed $value): void
|
||||
{
|
||||
$this->_attr[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert Node
|
||||
*
|
||||
* This method adds a new XML node. Node could be a instance of xmlTag, an
|
||||
* array of valid values, a boolean or a string.
|
||||
*
|
||||
* @param xmlTag|array|bool|string $node Node value
|
||||
*/
|
||||
public function insertNode($node = null): void
|
||||
{
|
||||
if ($node instanceof self) {
|
||||
$this->_nodes[] = $node;
|
||||
} elseif (is_array($node)) {
|
||||
$child = new self(null);
|
||||
foreach ($node as $tag => $n) {
|
||||
$child->insertNode(new self($tag, $n));
|
||||
}
|
||||
$this->_nodes[] = $child;
|
||||
} elseif (is_bool($node)) {
|
||||
$this->_nodes[] = $node ? '1' : '0';
|
||||
} else {
|
||||
$this->_nodes[] = (string) $node;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* XML Result
|
||||
*
|
||||
* Returns a string with XML content.
|
||||
*
|
||||
* @param bool $prolog Append prolog to result
|
||||
* @param string $encoding Result charset
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toXML(bool $prolog = false, string $encoding = 'UTF-8'): string
|
||||
{
|
||||
if ($this->_name && count($this->_nodes) > 0) {
|
||||
$format = '<%1$s%2$s>%3$s</%1$s>';
|
||||
} elseif ($this->_name && count($this->_nodes) === 0) {
|
||||
$format = '<%1$s%2$s/>';
|
||||
} else {
|
||||
$format = '%3$s';
|
||||
}
|
||||
|
||||
$res = $attr = $content = '';
|
||||
|
||||
foreach ($this->_attr as $k => $v) {
|
||||
$attr .= ' ' . $k . '="' . htmlspecialchars((string) $v, ENT_QUOTES, $encoding) . '"';
|
||||
}
|
||||
|
||||
foreach ($this->_nodes as $node) {
|
||||
if ($node instanceof self) {
|
||||
$content .= $node->toXML();
|
||||
} else {
|
||||
$content .= htmlspecialchars((string) $node, ENT_QUOTES, $encoding);
|
||||
}
|
||||
}
|
||||
|
||||
$res = sprintf($format, $this->_name, $attr, $content);
|
||||
|
||||
if ($prolog && $this->_name) {
|
||||
$res = '<?xml version="1.0" encoding="' . $encoding . '" ?>' . "\n" . $res;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,342 @@
|
|||
<?php
|
||||
/**
|
||||
* @class sessionDB
|
||||
* @brief Database Session Handler
|
||||
*
|
||||
* This class allows you to handle session data in database.
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Session
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class sessionDB
|
||||
{
|
||||
/**
|
||||
* dbLayer handler
|
||||
*
|
||||
* @var dbLayer
|
||||
*/
|
||||
private $con;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $table;
|
||||
|
||||
/**
|
||||
* Cookie name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $cookie_name;
|
||||
|
||||
/**
|
||||
* Cookie path
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $cookie_path;
|
||||
|
||||
/**
|
||||
* Cookie domain
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $cookie_domain;
|
||||
|
||||
/**
|
||||
* Secure cookie
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $cookie_secure;
|
||||
|
||||
/**
|
||||
* TTL (must be a negative duration as '-120 minutes')
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $ttl = '-120 minutes';
|
||||
|
||||
/**
|
||||
* Transient session
|
||||
*
|
||||
* No DB optimize on session destruction if true
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $transient = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* This method creates an instance of sessionDB class.
|
||||
*
|
||||
* @param dbLayer $con dbLayer inherited database instance
|
||||
* @param string $table Table name
|
||||
* @param string $cookie_name Session cookie name
|
||||
* @param string $cookie_path Session cookie path
|
||||
* @param string $cookie_domain Session cookie domaine
|
||||
* @param bool $cookie_secure Session cookie is available only through SSL if true
|
||||
* @param string $ttl TTL (default -120 minutes)
|
||||
* @param bool $transient Transient session : no db optimize on session destruction if true
|
||||
*/
|
||||
public function __construct(
|
||||
dbLayer $con,
|
||||
string $table,
|
||||
string $cookie_name,
|
||||
?string $cookie_path = null,
|
||||
?string $cookie_domain = null,
|
||||
bool $cookie_secure = false,
|
||||
?string $ttl = null,
|
||||
bool $transient = false
|
||||
) {
|
||||
$this->con = &$con;
|
||||
$this->table = $table;
|
||||
$this->cookie_name = $cookie_name;
|
||||
$this->cookie_path = is_null($cookie_path) ? '/' : $cookie_path;
|
||||
$this->cookie_domain = $cookie_domain;
|
||||
$this->cookie_secure = $cookie_secure;
|
||||
if (!is_null($ttl)) {
|
||||
$this->ttl = $ttl;
|
||||
}
|
||||
$this->transient = $transient;
|
||||
|
||||
if (function_exists('ini_set')) {
|
||||
@ini_set('session.use_cookies', '1');
|
||||
@ini_set('session.use_only_cookies', '1');
|
||||
@ini_set('url_rewriter.tags', '');
|
||||
@ini_set('session.use_trans_sid', '0');
|
||||
@ini_set('session.cookie_path', $this->cookie_path);
|
||||
@ini_set('session.cookie_domain', $this->cookie_domain);
|
||||
@ini_set('session.cookie_secure', (string) $this->cookie_secure);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*
|
||||
* This method calls session_write_close PHP function.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (isset($_SESSION)) {
|
||||
session_write_close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Session Start
|
||||
*/
|
||||
public function start(): void
|
||||
{
|
||||
session_set_save_handler(
|
||||
[$this, '_open'],
|
||||
[$this, '_close'],
|
||||
[$this, '_read'],
|
||||
[$this, '_write'],
|
||||
[$this, '_destroy'],
|
||||
[$this, '_gc']
|
||||
);
|
||||
|
||||
if (isset($_SESSION) && session_name() !== $this->cookie_name) {
|
||||
$this->destroy();
|
||||
}
|
||||
|
||||
if (!isset($_COOKIE[$this->cookie_name])) {
|
||||
session_id(sha1(uniqid((string) rand(), true)));
|
||||
}
|
||||
|
||||
session_name($this->cookie_name);
|
||||
session_start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Session Destroy
|
||||
*
|
||||
* This method destroies all session data and removes cookie.
|
||||
*/
|
||||
public function destroy(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
session_unset();
|
||||
session_destroy();
|
||||
call_user_func_array('setcookie', $this->getCookieParameters(false, -600));
|
||||
}
|
||||
|
||||
/**
|
||||
* Session Transient
|
||||
*
|
||||
* This method set the transient flag of the session
|
||||
*
|
||||
* @param bool $transient Session transient flag
|
||||
*/
|
||||
public function setTransientSession(bool $transient = false): void
|
||||
{
|
||||
$this->transient = $transient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session Cookie
|
||||
*
|
||||
* This method returns an array of all session cookie parameters.
|
||||
*
|
||||
* @param mixed $value Cookie value
|
||||
* @param int $expire Cookie expiration timestamp
|
||||
*/
|
||||
public function getCookieParameters($value = null, int $expire = 0)
|
||||
{
|
||||
return [
|
||||
(string) session_name(),
|
||||
(string) $value,
|
||||
$expire,
|
||||
(string) $this->cookie_path,
|
||||
(string) $this->cookie_domain,
|
||||
(bool) $this->cookie_secure,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Session handler callback called on session open
|
||||
*
|
||||
* @param string $path The save path
|
||||
* @param string $name The session name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function _open(string $path, string $name): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session handler callback called on session close
|
||||
*
|
||||
* @return bool ( description_of_the_return_value )
|
||||
*/
|
||||
public function _close(): bool
|
||||
{
|
||||
$this->_gc();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session handler callback called on session read
|
||||
*
|
||||
* @param string $ses_id The session identifier
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function _read(string $ses_id): string
|
||||
{
|
||||
$strReq = 'SELECT ses_value FROM ' . $this->table . ' ' .
|
||||
'WHERE ses_id = \'' . $this->checkID($ses_id) . '\' ';
|
||||
|
||||
$rs = $this->con->select($strReq);
|
||||
|
||||
if ($rs->isEmpty()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $rs->f('ses_value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Session handler callback called on session write
|
||||
*
|
||||
* @param string $ses_id The session identifier
|
||||
* @param string $data The data
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function _write(string $ses_id, string $data): bool
|
||||
{
|
||||
$strReq = 'SELECT ses_id ' .
|
||||
'FROM ' . $this->table . ' ' .
|
||||
"WHERE ses_id = '" . $this->checkID($ses_id) . "' ";
|
||||
|
||||
$rs = $this->con->select($strReq);
|
||||
|
||||
$cur = $this->con->openCursor($this->table);
|
||||
$cur->ses_time = (string) time();
|
||||
$cur->ses_value = (string) $data;
|
||||
|
||||
if (!$rs->isEmpty()) {
|
||||
$cur->update("WHERE ses_id = '" . $this->checkID($ses_id) . "' ");
|
||||
} else {
|
||||
$cur->ses_id = $this->checkID($ses_id);
|
||||
$cur->ses_start = (string) time();
|
||||
|
||||
$cur->insert();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session handler callback called on session destroy
|
||||
*
|
||||
* @param string $ses_id The session identifier
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function _destroy(string $ses_id): bool
|
||||
{
|
||||
$strReq = 'DELETE FROM ' . $this->table . ' ' .
|
||||
'WHERE ses_id = \'' . $this->checkID($ses_id) . '\' ';
|
||||
|
||||
$this->con->execute($strReq);
|
||||
|
||||
if (!$this->transient) {
|
||||
$this->_optimize();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session handler callback called on session garbage collect
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function _gc(): bool
|
||||
{
|
||||
$ses_life = strtotime($this->ttl);
|
||||
|
||||
$strReq = 'DELETE FROM ' . $this->table . ' ' .
|
||||
'WHERE ses_time < ' . $ses_life . ' ';
|
||||
|
||||
$this->con->execute($strReq);
|
||||
|
||||
if ($this->con->changes() > 0) {
|
||||
$this->_optimize();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize the session table
|
||||
*/
|
||||
private function _optimize(): void
|
||||
{
|
||||
$this->con->vacuum($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a session id
|
||||
*
|
||||
* @param string $id The identifier
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function checkID(string $id)
|
||||
{
|
||||
return preg_match('/^([0-9a-f]{40})$/i', $id) ? $id : '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,770 @@
|
|||
<?php
|
||||
/**
|
||||
* @class template
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Template
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class template
|
||||
{
|
||||
// Constants
|
||||
|
||||
public const CACHE_FOLDER = 'cbtpl';
|
||||
|
||||
/**
|
||||
* Instance self name
|
||||
*
|
||||
* Will be use in compiled template to call instance method or use instance properties
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $self_name;
|
||||
|
||||
/**
|
||||
* Use cache for compiled template files
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $use_cache = true;
|
||||
|
||||
/**
|
||||
* Stack of node blocks callbacks
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $blocks = [];
|
||||
|
||||
/**
|
||||
* Stack of node values callbacks
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $values = [];
|
||||
|
||||
/**
|
||||
* Remove PHP from template file
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $remove_php = true;
|
||||
|
||||
/**
|
||||
* Unknown node value callback
|
||||
*
|
||||
* @var callable|array|null
|
||||
*/
|
||||
protected $unknown_value_handler = null;
|
||||
|
||||
/**
|
||||
* Unknown node block callback
|
||||
*
|
||||
* @var callable|array|null
|
||||
*/
|
||||
protected $unknown_block_handler = null;
|
||||
|
||||
/**
|
||||
* Stack of template file paths
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $tpl_path = [];
|
||||
|
||||
/**
|
||||
* Cache directory
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $cache_dir;
|
||||
|
||||
/**
|
||||
* Parent file
|
||||
*
|
||||
* May be a filename or "__parent__"
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $parent_file;
|
||||
|
||||
/**
|
||||
* Stack of compiled template files
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $compile_stack = [];
|
||||
|
||||
/**
|
||||
* Stack of parent template files
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $parent_stack = [];
|
||||
|
||||
// Inclusion variables
|
||||
|
||||
/**
|
||||
* Super globals
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $superglobals = ['GLOBALS', '_SERVER', '_GET', '_POST', '_COOKIE', '_FILES', '_ENV', '_REQUEST', '_SESSION'];
|
||||
|
||||
/**
|
||||
* Stacks of globals keys
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $_k;
|
||||
|
||||
/**
|
||||
* Working globals key name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $_n;
|
||||
|
||||
/**
|
||||
* Working output buffer
|
||||
*
|
||||
* @var string|false
|
||||
*/
|
||||
protected static $_r;
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param string $cache_dir The cache dir
|
||||
* @param string $self_name The self name
|
||||
*/
|
||||
public function __construct(string $cache_dir, string $self_name)
|
||||
{
|
||||
$this->setCacheDir($cache_dir);
|
||||
|
||||
$this->self_name = $self_name;
|
||||
$this->addValue('include', [$this, 'includeFile']);
|
||||
$this->addBlock('Block', [$this, 'blockSection']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Node value "include" callback
|
||||
*
|
||||
* Syntax: {tpl:include src="filename"}
|
||||
*
|
||||
* @param array|ArrayObject $attr The attribute
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function includeFile($attr): string
|
||||
{
|
||||
if (!isset($attr['src'])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$src = path::clean($attr['src']);
|
||||
|
||||
$tpl_file = $this->getFilePath($src);
|
||||
if (!$tpl_file) {
|
||||
return '';
|
||||
}
|
||||
if (in_array($tpl_file, $this->compile_stack)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return
|
||||
'<?php try { ' .
|
||||
'echo ' . $this->self_name . "->getData('" . str_replace("'", "\'", $src) . "'); " .
|
||||
'} catch (Exception $e) {} ?>' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Node block "Block" callback
|
||||
*
|
||||
* Syntax: <tpl:Block name="name-of-block">[content]</tpl:Block>
|
||||
*
|
||||
* @param array|ArrayObject $attr The attribute
|
||||
* @param string $content The content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function blockSection($attr, string $content)
|
||||
{
|
||||
// Ignore attributes and return block content only
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the template path(s).
|
||||
*
|
||||
* Arguments may be a string or an array of string
|
||||
*/
|
||||
public function setPath()
|
||||
{
|
||||
$path = [];
|
||||
|
||||
foreach (func_get_args() as $v) {
|
||||
if (is_array($v)) {
|
||||
$path = array_merge($path, array_values($v));
|
||||
} else {
|
||||
$path[] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($path as $k => $v) {
|
||||
if (($v = path::real($v)) === false) {
|
||||
unset($path[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->tpl_path = array_unique($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the template paths.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPath(): array
|
||||
{
|
||||
return $this->tpl_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cache dir.
|
||||
*
|
||||
* @param string $dir The dir
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setCacheDir(string $dir): void
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
throw new Exception($dir . ' is not a valid directory.');
|
||||
}
|
||||
|
||||
if (!is_writable($dir)) {
|
||||
throw new Exception($dir . ' is not writable.');
|
||||
}
|
||||
|
||||
$this->cache_dir = path::real($dir) . '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a node block callback.
|
||||
*
|
||||
* The callback signature must be: callback(array $attr, string &$content)
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param callable|array|null $callback The callback
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function addBlock(string $name, $callback): void
|
||||
{
|
||||
if (!is_callable($callback)) {
|
||||
throw new Exception('No valid callback for ' . $name);
|
||||
}
|
||||
|
||||
$this->blocks[$name] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a node value callback.
|
||||
*
|
||||
* The callback signature must be: callback(array $attr [, string $str_attr])
|
||||
*
|
||||
* @param string $name The name
|
||||
* @param callable|array|null $callback The callback
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function addValue(string $name, $callback): void
|
||||
{
|
||||
if (!is_callable($callback)) {
|
||||
throw new Exception('No valid callback for ' . $name);
|
||||
}
|
||||
|
||||
$this->values[$name] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if node block exists.
|
||||
*
|
||||
* @param string $name The name
|
||||
*
|
||||
* @return bool True if block exists, False otherwise.
|
||||
*/
|
||||
public function blockExists(string $name): bool
|
||||
{
|
||||
return isset($this->blocks[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if node value exists.
|
||||
*
|
||||
* @param string $name The name
|
||||
*
|
||||
* @return bool True if value exists, False otherwise.
|
||||
*/
|
||||
public function valueExists(string $name): bool
|
||||
{
|
||||
return isset($this->values[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if ndoe tag (value or block) exists.
|
||||
*
|
||||
* @param string $name The name
|
||||
*
|
||||
* @return bool True if tag exists, False otherwise.
|
||||
*/
|
||||
public function tagExists(string $name): bool
|
||||
{
|
||||
return $this->blockExists($name) || $this->valueExists($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the node value callback.
|
||||
*
|
||||
* @param string $name The value name
|
||||
*
|
||||
* @return callable|array|false The block callback.
|
||||
*/
|
||||
public function getValueCallback(string $name)
|
||||
{
|
||||
if ($this->valueExists($name)) {
|
||||
return $this->values[$name];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the node block callback.
|
||||
*
|
||||
* @param string $name The block name
|
||||
*
|
||||
* @return callable|array|false The block callback.
|
||||
*/
|
||||
public function getBlockCallback(string $name)
|
||||
{
|
||||
if ($this->blockExists($name)) {
|
||||
return $this->blocks[$name];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the node blocks list.
|
||||
*
|
||||
* @return array The blocks list.
|
||||
*/
|
||||
public function getBlocksList(): array
|
||||
{
|
||||
return array_keys($this->blocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the node values list.
|
||||
*
|
||||
* @return array The values list.
|
||||
*/
|
||||
public function getValuesList(): array
|
||||
{
|
||||
return array_keys($this->values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the template file fullpath, creating it if not exist or not in cache and recent enough.
|
||||
*
|
||||
* @param string $file The file
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFile(string $file): string
|
||||
{
|
||||
$tpl_file = $this->getFilePath($file);
|
||||
|
||||
if (!$tpl_file) {
|
||||
throw new Exception('No template found for ' . $file);
|
||||
}
|
||||
|
||||
$file_md5 = md5($tpl_file);
|
||||
$dest_file = sprintf(
|
||||
'%s/%s/%s/%s/%s.php',
|
||||
$this->cache_dir,
|
||||
self::CACHE_FOLDER,
|
||||
substr($file_md5, 0, 2),
|
||||
substr($file_md5, 2, 2),
|
||||
$file_md5
|
||||
);
|
||||
|
||||
clearstatcache();
|
||||
$stat_f = $stat_d = false;
|
||||
if (file_exists($dest_file)) {
|
||||
$stat_f = stat($tpl_file);
|
||||
$stat_d = stat($dest_file);
|
||||
}
|
||||
|
||||
# We create template if:
|
||||
# - dest_file doest not exists
|
||||
# - we don't want cache
|
||||
# - dest_file size == 0
|
||||
# - tpl_file is more recent thant dest_file
|
||||
if (!$stat_d || !$this->use_cache || $stat_d['size'] == 0 || $stat_f['mtime'] > $stat_d['mtime']) {
|
||||
files::makeDir(dirname($dest_file), true);
|
||||
|
||||
if (($fp = @fopen($dest_file, 'wb')) === false) {
|
||||
throw new Exception('Unable to create cache file');
|
||||
}
|
||||
|
||||
$fc = $this->compileFile($tpl_file);
|
||||
fwrite($fp, $fc);
|
||||
fclose($fp);
|
||||
files::inheritChmod($dest_file);
|
||||
}
|
||||
|
||||
return $dest_file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file path.
|
||||
*
|
||||
* @param string $file The file
|
||||
*
|
||||
* @return bool|string The file path.
|
||||
*/
|
||||
public function getFilePath(string $file)
|
||||
{
|
||||
foreach ($this->tpl_path as $p) {
|
||||
if (file_exists($p . '/' . $file)) {
|
||||
return $p . '/' . $file;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parent file path.
|
||||
*
|
||||
* @param string $previous_path The previous path
|
||||
* @param string $file The file
|
||||
*
|
||||
* @return bool|string The parent file path.
|
||||
*/
|
||||
public function getParentFilePath(string $previous_path, string $file)
|
||||
{
|
||||
$check_file = false;
|
||||
foreach ($this->tpl_path as $p) {
|
||||
if ($check_file && file_exists($p . '/' . $file)) {
|
||||
return $p . '/' . $file;
|
||||
}
|
||||
if ($p == $previous_path) {
|
||||
$check_file = true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the template file content.
|
||||
*
|
||||
* @param string $________ The template filename
|
||||
*
|
||||
* @return string The data.
|
||||
*/
|
||||
public function getData(string $________): string
|
||||
{
|
||||
self::$_k = array_keys($GLOBALS);
|
||||
|
||||
foreach (self::$_k as self::$_n) {
|
||||
if (!in_array(self::$_n, self::$superglobals)) {
|
||||
global ${self::$_n};
|
||||
}
|
||||
}
|
||||
$dest_file = $this->getFile($________);
|
||||
ob_start();
|
||||
if (ini_get('display_errors')) {
|
||||
include $dest_file;
|
||||
} else {
|
||||
@include $dest_file;
|
||||
}
|
||||
self::$_r = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
return self::$_r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the compiled tree.
|
||||
*
|
||||
* @param string $file The file
|
||||
* @param string $err The error
|
||||
*
|
||||
* @return tplNode The compiled tree.
|
||||
*/
|
||||
protected function getCompiledTree(string $file, string &$err): tplNode
|
||||
{
|
||||
$fc = file_get_contents($file);
|
||||
|
||||
$this->compile_stack[] = $file;
|
||||
|
||||
// Remove every PHP tags
|
||||
if ($this->remove_php) {
|
||||
$fc = preg_replace('/<\?(?=php|=|\s).*?\?>/ms', '', $fc);
|
||||
}
|
||||
|
||||
// Transform what could be considered as PHP short tags
|
||||
$fc = preg_replace(
|
||||
'/(<\?(?!php|=|\s))(.*?)(\?>)/ms',
|
||||
'<?php echo "$1"; ?>$2<?php echo "$3"; ?>',
|
||||
$fc
|
||||
);
|
||||
|
||||
// Remove template comments <!-- #... -->
|
||||
$fc = preg_replace('/(^\s*)?<!-- #(.*?)-->/ms', '', $fc);
|
||||
|
||||
// Lexer part : split file into small pieces
|
||||
// each array entry will be either a tag or plain text
|
||||
$blocks = preg_split(
|
||||
'#(<tpl:\w+[^>]*>)|(</tpl:\w+>)|({{tpl:\w+[^}]*}})#msu',
|
||||
$fc,
|
||||
-1,
|
||||
PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
|
||||
);
|
||||
|
||||
// Next : build semantic tree from tokens.
|
||||
$rootNode = new tplNode();
|
||||
$node = $rootNode;
|
||||
$errors = [];
|
||||
$this->parent_file = '';
|
||||
foreach ($blocks as $block) {
|
||||
$isblock = preg_match('#<tpl:(\w+)(?:(\s+.*?)>|>)|</tpl:(\w+)>|{{tpl:(\w+)(\s(.*?))?}}#ms', $block, $match);
|
||||
if ($isblock == 1) {
|
||||
if (substr($match[0], 1, 1) == '/') {
|
||||
// Closing tag, check if it matches current opened node
|
||||
$tag = $match[3];
|
||||
if (($node instanceof tplNodeBlock) && $node->getTag() == $tag) {
|
||||
$node->setClosing();
|
||||
$node = $node->getParent();
|
||||
} else {
|
||||
// Closing tag does not match opening tag
|
||||
// Search if it closes a parent tag
|
||||
$search = $node;
|
||||
while ($search->getTag() != 'ROOT' && $search->getTag() != $tag) {
|
||||
$search = $search->getParent();
|
||||
}
|
||||
if ($search->getTag() == $tag) {
|
||||
$errors[] = sprintf(
|
||||
__('Did not find closing tag for block <tpl:%s>. Content has been ignored.'),
|
||||
html::escapeHTML($node->getTag())
|
||||
);
|
||||
$search->setClosing();
|
||||
$node = $search->getParent();
|
||||
} else {
|
||||
$errors[] = sprintf(
|
||||
__('Unexpected closing tag </tpl:%s> found.'),
|
||||
$tag
|
||||
);
|
||||
}
|
||||
}
|
||||
} elseif (substr($match[0], 0, 1) == '{') {
|
||||
// Value tag
|
||||
$tag = $match[4];
|
||||
$str_attr = '';
|
||||
$attr = [];
|
||||
if (isset($match[6])) {
|
||||
$str_attr = $match[6];
|
||||
$attr = $this->getAttrs($match[6]);
|
||||
}
|
||||
if (strtolower($tag) == 'extends') {
|
||||
if (isset($attr['parent']) && $this->parent_file == '') {
|
||||
$this->parent_file = $attr['parent'];
|
||||
}
|
||||
} elseif (strtolower($tag) == 'parent') {
|
||||
$node->addChild(new tplNodeValueParent($tag, $attr, $str_attr));
|
||||
} else {
|
||||
$node->addChild(new tplNodeValue($tag, $attr, $str_attr));
|
||||
}
|
||||
} else {
|
||||
// Opening tag, create new node and dive into it
|
||||
$tag = $match[1];
|
||||
if ($tag == 'Block') {
|
||||
$newnode = new tplNodeBlockDefinition($tag, isset($match[2]) ? $this->getAttrs($match[2]) : []);
|
||||
} else {
|
||||
$newnode = new tplNodeBlock($tag, isset($match[2]) ? $this->getAttrs($match[2]) : []);
|
||||
}
|
||||
$node->addChild($newnode);
|
||||
$node = $newnode;
|
||||
}
|
||||
} else {
|
||||
// Simple text
|
||||
$node->addChild(new tplNodeText($block));
|
||||
}
|
||||
}
|
||||
|
||||
if (($node instanceof tplNodeBlock) && !$node->isClosed()) {
|
||||
$errors[] = sprintf(
|
||||
__('Did not find closing tag for block <tpl:%s>. Content has been ignored.'),
|
||||
html::escapeHTML($node->getTag())
|
||||
);
|
||||
}
|
||||
|
||||
$err = '';
|
||||
if (count($errors)) {
|
||||
$err = "\n\n<!-- \n" .
|
||||
__('WARNING: the following errors have been found while parsing template file :') .
|
||||
"\n * " .
|
||||
join("\n * ", $errors) .
|
||||
"\n -->\n";
|
||||
}
|
||||
|
||||
return $rootNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a template file
|
||||
*
|
||||
* @param string $file The file
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function compileFile(string $file): string
|
||||
{
|
||||
$tree = null;
|
||||
$err = '';
|
||||
while (true) {
|
||||
if ($file && !in_array($file, $this->parent_stack)) {
|
||||
$tree = $this->getCompiledTree($file, $err);
|
||||
|
||||
if ($this->parent_file == '__parent__') {
|
||||
$this->parent_stack[] = $file;
|
||||
$newfile = $this->getParentFilePath(dirname($file), basename($file));
|
||||
if (!$newfile) {
|
||||
throw new Exception('No template found for ' . basename($file));
|
||||
}
|
||||
$file = $newfile;
|
||||
} elseif ($this->parent_file != '') { // @phpstan-ignore-line
|
||||
$this->parent_stack[] = $file;
|
||||
$file = $this->getFilePath($this->parent_file);
|
||||
if (!$file) {
|
||||
throw new Exception('No template found for ' . $this->parent_file);
|
||||
}
|
||||
} else {
|
||||
return $tree->compile($this) . $err;
|
||||
}
|
||||
} else {
|
||||
if ($tree != null) {
|
||||
return $tree->compile($this) . $err;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile block node
|
||||
*
|
||||
* @param string $tag The tag
|
||||
* @param array|ArrayObject $attr The attribute
|
||||
* @param string $content The content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function compileBlockNode(string $tag, $attr, string $content): string
|
||||
{
|
||||
$res = '';
|
||||
if (isset($this->blocks[$tag])) {
|
||||
$res .= call_user_func($this->blocks[$tag], $attr, $content);
|
||||
} elseif (is_callable($this->unknown_block_handler)) {
|
||||
$res .= call_user_func($this->unknown_block_handler, $tag, $attr, $content);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile value node
|
||||
*
|
||||
* @param string $tag The tag
|
||||
* @param array|ArrayObject $attr The attribute
|
||||
* @param string $str_attr The string attribute
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function compileValueNode(string $tag, $attr, string $str_attr): string
|
||||
{
|
||||
$res = '';
|
||||
if (isset($this->values[$tag])) {
|
||||
$res .= call_user_func($this->values[$tag], $attr, ltrim((string) $str_attr));
|
||||
} elseif (is_callable($this->unknown_value_handler)) {
|
||||
$res .= call_user_func($this->unknown_value_handler, $tag, $attr, $str_attr);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile value
|
||||
*
|
||||
* @param array $match The match
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function compileValue(array $match): string
|
||||
{
|
||||
$v = $match[1];
|
||||
$attr = isset($match[2]) ? $this->getAttrs($match[2]) : [];
|
||||
$str_attr = $match[2] ?? null;
|
||||
|
||||
return call_user_func($this->values[$v], $attr, ltrim((string) $str_attr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the unknown value handler.
|
||||
*
|
||||
* @param callable|array|null $callback The callback
|
||||
*/
|
||||
public function setUnknownValueHandler($callback): void
|
||||
{
|
||||
$this->unknown_value_handler = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the unknown block handler.
|
||||
*
|
||||
* @param callable|array|null $callback The callback
|
||||
*/
|
||||
public function setUnknownBlockHandler($callback): void
|
||||
{
|
||||
$this->unknown_block_handler = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attributes.
|
||||
*
|
||||
* @param string $str The string
|
||||
*
|
||||
* @return array The attributes.
|
||||
*/
|
||||
protected function getAttrs(string $str): array
|
||||
{
|
||||
$res = [];
|
||||
if (preg_match_all('|([a-zA-Z0-9_:-]+)="([^"]*)"|ms', $str, $m) > 0) {
|
||||
foreach ($m[1] as $i => $v) {
|
||||
$res[$v] = $m[2][$i];
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
/**
|
||||
* @class tplNode
|
||||
* @brief Template nodes, for parsing purposes
|
||||
*
|
||||
* Generic list node, this one may only be instanciated once for root element
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Template
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class tplNode
|
||||
{
|
||||
/**
|
||||
* Basic tree structure : links to parent, children forrest
|
||||
*
|
||||
* @var null|tplNode|tplNodeBlock|tplNodeBlockDefinition|tplNodeText|tplNodeValue|tplNodeValueParent
|
||||
*/
|
||||
protected $parentNode;
|
||||
|
||||
/**
|
||||
* Node children
|
||||
*
|
||||
* @var ArrayObject
|
||||
*/
|
||||
protected $children;
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->children = new ArrayObject();
|
||||
$this->parentNode = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the node is closed.
|
||||
*/
|
||||
public function setClosing(): void
|
||||
{
|
||||
// Nothing to do at this level
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns compiled block
|
||||
*
|
||||
* @param template $tpl The current template engine instance
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function compile(template $tpl)
|
||||
{
|
||||
$res = '';
|
||||
foreach ($this->children as $child) {
|
||||
$res .= $child->compile($tpl);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a children to current node.
|
||||
*
|
||||
* @param tplNode|tplNodeBlock|tplNodeBlockDefinition|tplNodeText|tplNodeValue|tplNodeValueParent $child The child
|
||||
*/
|
||||
public function addChild($child)
|
||||
{
|
||||
$this->children[] = $child;
|
||||
$child->setParent($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current node children.
|
||||
*
|
||||
* @param ArrayObject $children The children
|
||||
*/
|
||||
public function setChildren($children)
|
||||
{
|
||||
$this->children = $children;
|
||||
foreach ($this->children as $child) {
|
||||
$child->setParent($this);
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
|
||||
/**
|
||||
* Defines parent for current node.
|
||||
*
|
||||
* @param null|tplNode|tplNodeBlock|tplNodeBlockDefinition|tplNodeValue|tplNodeValueParent $parent The parent
|
||||
*/
|
||||
protected function setParent($parent)
|
||||
{
|
||||
$this->parentNode = $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves current node parent.
|
||||
*
|
||||
* If parent is root node, null is returned
|
||||
*
|
||||
* @return null|tplNode|tplNodeBlock|tplNodeBlockDefinition|tplNodeValue|tplNodeValueParent The parent.
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return $this->parentNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tag.
|
||||
*
|
||||
* @return string The tag.
|
||||
*/
|
||||
public function getTag(): string
|
||||
{
|
||||
return 'ROOT';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
/**
|
||||
* @class tplNodeBlock
|
||||
* @brief Block node, for all <tpl:Tag>...</tpl:Tag>
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Template
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class tplNodeBlock extends tplNode
|
||||
{
|
||||
/**
|
||||
* Node block tag name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tag;
|
||||
|
||||
/**
|
||||
* Node block tag attributes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $attr;
|
||||
|
||||
/**
|
||||
* Closed node block flag
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $closed;
|
||||
|
||||
/**
|
||||
* Node block content
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param string $tag The tag
|
||||
* @param array $attr The attribute
|
||||
*/
|
||||
public function __construct(string $tag, array $attr)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->content = '';
|
||||
$this->tag = $tag;
|
||||
$this->attr = $attr;
|
||||
$this->closed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the node block is closed.
|
||||
*/
|
||||
public function setClosing(): void
|
||||
{
|
||||
$this->closed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if node block is closed.
|
||||
*
|
||||
* @return bool True if closed, False otherwise.
|
||||
*/
|
||||
public function isClosed(): bool
|
||||
{
|
||||
return $this->closed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the node block
|
||||
*
|
||||
* @param template $tpl The current template engine instance
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function compile(template $tpl): string
|
||||
{
|
||||
if ($this->closed) {
|
||||
$content = parent::compile($tpl);
|
||||
|
||||
return $tpl->compileBlockNode($this->tag, $this->attr, $content);
|
||||
}
|
||||
// if tag has not been closed, silently ignore its content...
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tag.
|
||||
*
|
||||
* @return string The tag.
|
||||
*/
|
||||
public function getTag(): string
|
||||
{
|
||||
return $this->tag;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
<?php
|
||||
/**
|
||||
* @class tplNodeBlockDefinition
|
||||
* @brief Block node, for all <tpl:Tag>...</tpl:Tag>
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Template
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class tplNodeBlockDefinition extends tplNodeBlock
|
||||
{
|
||||
/**
|
||||
* Stack of blocks
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $stack = [];
|
||||
|
||||
/**
|
||||
* Current block
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected static $current_block = null;
|
||||
|
||||
/**
|
||||
* Block name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* Renders the parent block of currently being displayed block
|
||||
*
|
||||
* @param template $tpl The current template engine instance
|
||||
*
|
||||
* @return string The compiled parent block
|
||||
*/
|
||||
public static function renderParent(template $tpl)
|
||||
{
|
||||
return self::getStackBlock(self::$current_block, $tpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* resets blocks stack
|
||||
*/
|
||||
public static function reset()
|
||||
{
|
||||
self::$stack = [];
|
||||
self::$current_block = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves block defined in call stack
|
||||
*
|
||||
* @param string $name The block name
|
||||
* @param template $tpl The current template engine instance
|
||||
*
|
||||
* @return string The block (empty string if unavailable)
|
||||
*/
|
||||
public static function getStackBlock(string $name, template $tpl)
|
||||
{
|
||||
$stack = &self::$stack[$name];
|
||||
$pos = $stack['pos'];
|
||||
|
||||
// First check if block position is correct
|
||||
if (isset($stack['blocks'][$pos])) {
|
||||
self::$current_block = $name;
|
||||
if (!is_string($stack['blocks'][$pos])) {
|
||||
// Not a string ==> need to compile the tree
|
||||
|
||||
// Go deeper 1 level in stack, to enable calls to parent
|
||||
$stack['pos']++;
|
||||
$ret = '';
|
||||
// Compile each and every children
|
||||
foreach ($stack['blocks'][$pos] as $child) {
|
||||
$ret .= $child->compile($tpl);
|
||||
}
|
||||
$stack['pos']--;
|
||||
$stack['blocks'][$pos] = $ret;
|
||||
} else {
|
||||
// Already compiled, nice ! Simply return string
|
||||
$ret = $stack['blocks'][$pos];
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
// Not found => return empty
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Block definition specific constructor : keep block name in mind
|
||||
*
|
||||
* @param string $tag Current tag (might be "Block")
|
||||
* @param array $attr Tag attributes (must contain "name" attribute)
|
||||
*/
|
||||
public function __construct(string $tag, array $attr)
|
||||
{
|
||||
parent::__construct($tag, $attr);
|
||||
$this->name = '';
|
||||
if (isset($attr['name'])) {
|
||||
$this->name = $attr['name'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override tag closing processing. Here we enrich the block stack to
|
||||
* keep block history.
|
||||
*/
|
||||
public function setClosing(): void
|
||||
{
|
||||
if (!isset(self::$stack[$this->name])) {
|
||||
self::$stack[$this->name] = [
|
||||
'pos' => 0, // pos is the pointer to the current block being rendered
|
||||
'blocks' => [], ];
|
||||
}
|
||||
parent::setClosing();
|
||||
self::$stack[$this->name]['blocks'][] = $this->children;
|
||||
$this->children = new ArrayObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the block definition : grab latest block content being defined
|
||||
*
|
||||
* @param template $tpl The current template engine instance
|
||||
*
|
||||
* @return string The compiled block
|
||||
*/
|
||||
public function compile(template $tpl): string
|
||||
{
|
||||
return $tpl->compileBlockNode(
|
||||
$this->tag,
|
||||
$this->attr,
|
||||
self::getStackBlock($this->name, $tpl)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
/**
|
||||
* @class tplNodeText
|
||||
* @brief Text node, for any non-tpl content
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Template
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class tplNodeText extends tplNode
|
||||
{
|
||||
/**
|
||||
* Simple text node, only holds its content
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
public function __construct(string $text)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->content = $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile node text
|
||||
*
|
||||
* @param template $tpl The current template engine instance
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function compile(template $tpl): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tag.
|
||||
*
|
||||
* @return string The tag.
|
||||
*/
|
||||
public function getTag(): string
|
||||
{
|
||||
return 'TEXT';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
/**
|
||||
* @class tplNodeValue
|
||||
* @brief Value node, for all {{tpl:Tag}}
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Template
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class tplNodeValue extends tplNode
|
||||
{
|
||||
/**
|
||||
* Node tag
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tag;
|
||||
|
||||
/**
|
||||
* Node attributes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $attr;
|
||||
|
||||
/**
|
||||
* Node string attributes
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $str_attr;
|
||||
|
||||
/**
|
||||
* Node content
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param string $tag The tag
|
||||
* @param array $attr The attribute
|
||||
* @param string $str_attr The string attribute
|
||||
*/
|
||||
public function __construct(string $tag, array $attr, string $str_attr)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->content = '';
|
||||
$this->tag = $tag;
|
||||
$this->attr = $attr;
|
||||
$this->str_attr = $str_attr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the value node
|
||||
*
|
||||
* @param template $tpl The current template engine instance
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function compile(template $tpl): string
|
||||
{
|
||||
return $tpl->compileValueNode($this->tag, $this->attr, $this->str_attr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tag.
|
||||
*
|
||||
* @return string The tag.
|
||||
*/
|
||||
public function getTag(): string
|
||||
{
|
||||
return $this->tag;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
/**
|
||||
* @class tplNodeValueParent
|
||||
* @brief Value node, for all {{tpl:Tag}}
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Template
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class tplNodeValueParent extends tplNodeValue
|
||||
{
|
||||
/**
|
||||
* Compile node value parent
|
||||
*
|
||||
* @param template $tpl The current template engine instance
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function compile(template $tpl): string
|
||||
{
|
||||
// simply ask currently being displayed to display itself!
|
||||
return tplNodeBlockDefinition::renderParent($tpl);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,311 @@
|
|||
<?php
|
||||
/**
|
||||
* @class urlHandler
|
||||
*
|
||||
* @package Clearbricks
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class urlHandler
|
||||
{
|
||||
/**
|
||||
* Stack of URL types (name)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $types = [];
|
||||
|
||||
/**
|
||||
* Default handler, used if requested type handler not registered
|
||||
*
|
||||
* @var callable|array
|
||||
*/
|
||||
protected $default_handler;
|
||||
|
||||
/**
|
||||
* Stack of error handlers
|
||||
*
|
||||
* @var array Array of callable
|
||||
*/
|
||||
protected $error_handlers = [];
|
||||
|
||||
/**
|
||||
* URL mode
|
||||
*
|
||||
* Should be 'path_info' or 'query_string'
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $mode;
|
||||
|
||||
/**
|
||||
* Current handler
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $type = 'default';
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param string $mode The URL mode
|
||||
*/
|
||||
public function __construct(string $mode = 'path_info')
|
||||
{
|
||||
$this->mode = $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an URL handler
|
||||
*
|
||||
* @param string $type The URI type
|
||||
* @param string $url The base URI
|
||||
* @param string $representation The URI representation (regex, string)
|
||||
* @param callable|array $handler The handler
|
||||
*/
|
||||
public function register(string $type, string $url, string $representation, $handler): void
|
||||
{
|
||||
$this->types[$type] = [
|
||||
'url' => $url,
|
||||
'representation' => $representation,
|
||||
'handler' => $handler,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the default URL handler
|
||||
*
|
||||
* @param callable|array $handler The handler
|
||||
*/
|
||||
public function registerDefault($handler): void
|
||||
{
|
||||
$this->default_handler = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an error handler (prepend at the begining of the error handler stack)
|
||||
*
|
||||
* @param callable|array $handler The handler
|
||||
*/
|
||||
public function registerError($handler): void
|
||||
{
|
||||
array_unshift($this->error_handlers, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister an URL handler
|
||||
*
|
||||
* @param string $type The type
|
||||
*/
|
||||
public function unregister(string $type): void
|
||||
{
|
||||
if (isset($this->types[$type])) {
|
||||
unset($this->types[$type]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the registered URL handlers.
|
||||
*
|
||||
* @return array The types.
|
||||
*/
|
||||
public function getTypes(): array
|
||||
{
|
||||
return $this->types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the base URI of an URL handler.
|
||||
*
|
||||
* @param string $type The type
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBase(string $type)
|
||||
{
|
||||
if (isset($this->types[$type])) {
|
||||
return $this->types[$type]['url'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the document using an URL handler.
|
||||
*/
|
||||
public function getDocument(): void
|
||||
{
|
||||
$type = $args = '';
|
||||
|
||||
if ($this->mode === 'path_info') {
|
||||
$part = substr($_SERVER['PATH_INFO'], 1);
|
||||
} else {
|
||||
$part = '';
|
||||
|
||||
$query_string = $this->parseQueryString();
|
||||
|
||||
# Recreates some _GET and _REQUEST pairs
|
||||
if (!empty($query_string)) {
|
||||
foreach ($_GET as $k => $v) {
|
||||
if (isset($_REQUEST[$k])) {
|
||||
unset($_REQUEST[$k]);
|
||||
}
|
||||
}
|
||||
$_GET = $query_string;
|
||||
$_REQUEST = array_merge($query_string, $_REQUEST);
|
||||
|
||||
foreach ($query_string as $k => $v) {
|
||||
if ($v === null) {
|
||||
$part = $k;
|
||||
unset($_GET[$k], $_REQUEST[$k]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$_SERVER['URL_REQUEST_PART'] = $part;
|
||||
|
||||
$this->getArgs($part, $type, $args);
|
||||
|
||||
if (!$type) {
|
||||
$this->type = 'default';
|
||||
$this->callDefaultHandler($args);
|
||||
} else {
|
||||
$this->type = $type;
|
||||
$this->callHandler($type, $args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the arguments from an URI
|
||||
*
|
||||
* @param string $part The part
|
||||
* @param mixed $type The type
|
||||
* @param mixed $args The arguments
|
||||
*/
|
||||
public function getArgs(string $part, &$type, &$args): void
|
||||
{
|
||||
if ($part == '') {
|
||||
$type = null;
|
||||
$args = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sortTypes();
|
||||
|
||||
foreach ($this->types as $k => $v) {
|
||||
$repr = $v['representation'];
|
||||
if ($repr == $part) {
|
||||
$type = $k;
|
||||
$args = null;
|
||||
|
||||
return;
|
||||
} elseif (preg_match('#' . $repr . '#', (string) $part, $m)) {
|
||||
$type = $k;
|
||||
$args = $m[1] ?? null;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No type, pass args to default
|
||||
$args = $part;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call an URL handler callback
|
||||
*
|
||||
* @param callable|array $handler The handler
|
||||
* @param string $args The arguments
|
||||
* @param string $type The URL handler type
|
||||
*/
|
||||
public function callHelper($handler, ?string $args, string $type = 'default'): void
|
||||
{
|
||||
if (!is_callable($handler)) {
|
||||
throw new Exception('Unable to call function');
|
||||
}
|
||||
|
||||
try {
|
||||
call_user_func($handler, $args);
|
||||
} catch (Exception $e) {
|
||||
foreach ($this->error_handlers as $err_handler) {
|
||||
if (call_user_func($err_handler, $args, $type, $e) === true) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// propagate exception, as it has not been processed by handlers
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call an registered URL handler callback
|
||||
*
|
||||
* @param string $type The type
|
||||
* @param string $args The arguments
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function callHandler(string $type, ?string $args): void
|
||||
{
|
||||
if (!isset($this->types[$type])) {
|
||||
throw new Exception('Unknown URL type');
|
||||
}
|
||||
|
||||
$this->callHelper($this->types[$type]['handler'], $args, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the default handler callback
|
||||
*
|
||||
* @param string $args The arguments
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function callDefaultHandler(?string $args): void
|
||||
{
|
||||
$this->callHelper($this->default_handler, $args, 'default');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse query string part of server URI
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function parseQueryString(): array
|
||||
{
|
||||
$res = [];
|
||||
if (!empty($_SERVER['QUERY_STRING'])) {
|
||||
$parameters = explode('&', $_SERVER['QUERY_STRING']);
|
||||
foreach ($parameters as $parameter) {
|
||||
$elements = explode('=', $parameter, 2);
|
||||
|
||||
// Decode the parameter's name
|
||||
$elements[0] = rawurldecode($elements[0]);
|
||||
if (!isset($elements[1])) {
|
||||
// No parameter value
|
||||
$res[$elements[0]] = null;
|
||||
} else {
|
||||
// Decode parameter's value
|
||||
$res[$elements[0]] = urldecode($elements[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort registered URL on their representations descending order
|
||||
*/
|
||||
protected function sortTypes()
|
||||
{
|
||||
$representations = [];
|
||||
foreach ($this->types as $k => $v) {
|
||||
$representations[$k] = $v['url'];
|
||||
}
|
||||
array_multisort($representations, SORT_DESC, $this->types);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,539 @@
|
|||
<?php
|
||||
/**
|
||||
* @class fileUnzip
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Zip
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class fileUnzip
|
||||
{
|
||||
protected $file_name;
|
||||
protected $compressed_list = [];
|
||||
protected $eo_central = [];
|
||||
|
||||
protected $zip_sig = "\x50\x4b\x03\x04"; # local file header signature
|
||||
protected $dir_sig = "\x50\x4b\x01\x02"; # central dir header signature
|
||||
protected $dir_sig_e = "\x50\x4b\x05\x06"; # end of central dir signature
|
||||
protected $fp = null;
|
||||
|
||||
protected $memory_limit = null;
|
||||
|
||||
protected $exclude_pattern = '';
|
||||
|
||||
public function __construct($file_name)
|
||||
{
|
||||
$this->file_name = $file_name;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if ($this->fp) {
|
||||
fclose($this->fp);
|
||||
$this->fp = null;
|
||||
}
|
||||
|
||||
if ($this->memory_limit) {
|
||||
ini_set('memory_limit', $this->memory_limit);
|
||||
}
|
||||
}
|
||||
|
||||
public function getList($stop_on_file = false, $exclude = false)
|
||||
{
|
||||
if (!empty($this->compressed_list)) {
|
||||
return $this->compressed_list;
|
||||
}
|
||||
|
||||
if (!$this->loadFileListByEOF($stop_on_file, $exclude) && !$this->loadFileListBySignatures($stop_on_file, $exclude)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->compressed_list;
|
||||
}
|
||||
|
||||
public function unzipAll($target)
|
||||
{
|
||||
if (empty($this->compressed_list)) {
|
||||
$this->getList();
|
||||
}
|
||||
|
||||
foreach ($this->compressed_list as $k => $v) {
|
||||
if ($v['is_dir']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->unzip($k, $target . '/' . $k);
|
||||
}
|
||||
}
|
||||
|
||||
public function unzip($file_name, $target = false)
|
||||
{
|
||||
if (empty($this->compressed_list)) {
|
||||
$this->getList($file_name);
|
||||
}
|
||||
|
||||
if (!isset($this->compressed_list[$file_name])) {
|
||||
throw new Exception(sprintf(__('File %s is not compressed in the zip.'), $file_name));
|
||||
}
|
||||
if ($this->isFileExcluded($file_name)) {
|
||||
return;
|
||||
}
|
||||
$details = &$this->compressed_list[$file_name];
|
||||
|
||||
if ($details['is_dir']) {
|
||||
throw new Exception(sprintf(__('Trying to unzip a folder name %s'), $file_name));
|
||||
}
|
||||
|
||||
if ($target) {
|
||||
$this->testTargetDir(dirname($target));
|
||||
}
|
||||
|
||||
if (!$details['uncompressed_size']) {
|
||||
return $this->putContent('', $target);
|
||||
}
|
||||
|
||||
fseek($this->fp(), $details['contents_start_offset']);
|
||||
|
||||
$this->memoryAllocate($details['compressed_size']);
|
||||
|
||||
return $this->uncompress(
|
||||
fread($this->fp(), $details['compressed_size']),
|
||||
$details['compression_method'],
|
||||
$details['uncompressed_size'],
|
||||
$target
|
||||
);
|
||||
}
|
||||
|
||||
public function getFilesList()
|
||||
{
|
||||
if (empty($this->compressed_list)) {
|
||||
$this->getList();
|
||||
}
|
||||
|
||||
$res = [];
|
||||
foreach ($this->compressed_list as $k => $v) {
|
||||
if (!$v['is_dir']) {
|
||||
$res[] = $k;
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function getDirsList()
|
||||
{
|
||||
if (empty($this->compressed_list)) {
|
||||
$this->getList();
|
||||
}
|
||||
|
||||
$res = [];
|
||||
foreach ($this->compressed_list as $k => $v) {
|
||||
if ($v['is_dir']) {
|
||||
$res[] = substr($k, 0, -1);
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function getRootDir()
|
||||
{
|
||||
if (empty($this->compressed_list)) {
|
||||
$this->getList();
|
||||
}
|
||||
|
||||
$files = $this->getFilesList();
|
||||
$dirs = $this->getDirsList();
|
||||
|
||||
$root_files = 0;
|
||||
$root_dirs = 0;
|
||||
foreach ($files as $v) {
|
||||
if (strpos($v, '/') === false) {
|
||||
$root_files++;
|
||||
}
|
||||
}
|
||||
foreach ($dirs as $v) {
|
||||
if (strpos($v, '/') === false) {
|
||||
$root_dirs++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($root_files == 0 && $root_dirs == 1) {
|
||||
return $dirs[0];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isEmpty()
|
||||
{
|
||||
if (empty($this->compressed_list)) {
|
||||
$this->getList();
|
||||
}
|
||||
|
||||
return count($this->compressed_list) == 0;
|
||||
}
|
||||
|
||||
public function hasFile($f)
|
||||
{
|
||||
if (empty($this->compressed_list)) {
|
||||
$this->getList();
|
||||
}
|
||||
|
||||
return isset($this->compressed_list[$f]);
|
||||
}
|
||||
|
||||
public function setExcludePattern($pattern)
|
||||
{
|
||||
$this->exclude_pattern = $pattern;
|
||||
}
|
||||
|
||||
protected function fp()
|
||||
{
|
||||
if ($this->fp === null) {
|
||||
$this->fp = @fopen($this->file_name, 'rb');
|
||||
}
|
||||
|
||||
if ($this->fp === false) {
|
||||
throw new Exception('Unable to open file.');
|
||||
}
|
||||
|
||||
return $this->fp;
|
||||
}
|
||||
|
||||
protected function isFileExcluded($f)
|
||||
{
|
||||
if (!$this->exclude_pattern) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return preg_match($this->exclude_pattern, (string) $f);
|
||||
}
|
||||
|
||||
protected function putContent($content, $target = false)
|
||||
{
|
||||
if ($target) {
|
||||
$r = @file_put_contents($target, $content);
|
||||
if ($r === false) {
|
||||
throw new Exception(__('Unable to write destination file.'));
|
||||
}
|
||||
files::inheritChmod($target);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function testTargetDir($dir)
|
||||
{
|
||||
if (is_dir($dir) && !is_writable($dir)) {
|
||||
throw new Exception(__('Unable to write in target directory, permission denied.'));
|
||||
}
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
files::makeDir($dir, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected function uncompress($content, $mode, $size, $target = false)
|
||||
{
|
||||
switch ($mode) {
|
||||
case 0:
|
||||
# Not compressed
|
||||
$this->memoryAllocate($size * 2);
|
||||
|
||||
return $this->putContent($content, $target);
|
||||
case 1:
|
||||
throw new Exception('Shrunk mode is not supported.');
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
throw new Exception('Compression factor ' . ($mode - 1) . ' is not supported.');
|
||||
case 6:
|
||||
throw new Exception('Implode is not supported.');
|
||||
case 7:
|
||||
throw new Exception('Tokenizing compression algorithm is not supported.');
|
||||
case 8:
|
||||
# Deflate
|
||||
if (!function_exists('gzinflate')) {
|
||||
throw new Exception('Gzip functions are not available.');
|
||||
}
|
||||
$this->memoryAllocate($size * 2);
|
||||
|
||||
return $this->putContent(gzinflate($content, $size), $target);
|
||||
case 9:
|
||||
throw new Exception('Enhanced Deflating is not supported.');
|
||||
case 10:
|
||||
throw new Exception('PKWARE Date Compression Library Impoloding is not supported.');
|
||||
case 12:
|
||||
# Bzip2
|
||||
if (!function_exists('bzdecompress')) {
|
||||
throw new Exception('Bzip2 functions are not available.');
|
||||
}
|
||||
$this->memoryAllocate($size * 2);
|
||||
|
||||
return $this->putContent(bzdecompress($content), $target);
|
||||
case 18:
|
||||
throw new Exception('IBM TERSE is not supported.');
|
||||
default:
|
||||
throw new Exception('Unknown uncompress method');
|
||||
}
|
||||
}
|
||||
|
||||
protected function loadFileListByEOF($stop_on_file = false, $exclude = false)
|
||||
{
|
||||
$fp = $this->fp();
|
||||
|
||||
for ($x = 0; $x < 1024; $x++) {
|
||||
fseek($fp, -22 - $x, SEEK_END);
|
||||
$signature = fread($fp, 4);
|
||||
|
||||
if ($signature == $this->dir_sig_e) {
|
||||
$dir_list = [];
|
||||
|
||||
$eodir = [
|
||||
'disk_number_this' => unpack('v', fread($fp, 2)),
|
||||
'disk_number' => unpack('v', fread($fp, 2)),
|
||||
'total_entries_this' => unpack('v', fread($fp, 2)),
|
||||
'total_entries' => unpack('v', fread($fp, 2)),
|
||||
'size_of_cd' => unpack('V', fread($fp, 4)),
|
||||
'offset_start_cd' => unpack('V', fread($fp, 4)),
|
||||
];
|
||||
|
||||
$zip_comment_len = unpack('v', fread($fp, 2));
|
||||
$eodir['zipfile_comment'] = $zip_comment_len[1] ? fread($fp, (int) $zip_comment_len) : '';
|
||||
|
||||
$this->eo_central = [
|
||||
'disk_number_this' => $eodir['disk_number_this'][1],
|
||||
'disk_number' => $eodir['disk_number'][1],
|
||||
'total_entries_this' => $eodir['total_entries_this'][1],
|
||||
'total_entries' => $eodir['total_entries'][1],
|
||||
'size_of_cd' => $eodir['size_of_cd'][1],
|
||||
'offset_start_cd' => $eodir['offset_start_cd'][1],
|
||||
'zipfile_comment' => $eodir['zipfile_comment'],
|
||||
];
|
||||
|
||||
fseek($fp, $this->eo_central['offset_start_cd']);
|
||||
$signature = fread($fp, 4);
|
||||
|
||||
while ($signature == $this->dir_sig) {
|
||||
$dir = [];
|
||||
$dir['version_madeby'] = unpack('v', fread($fp, 2)); # version made by
|
||||
$dir['version_needed'] = unpack('v', fread($fp, 2)); # version needed to extract
|
||||
$dir['general_bit_flag'] = unpack('v', fread($fp, 2)); # general purpose bit flag
|
||||
$dir['compression_method'] = unpack('v', fread($fp, 2)); # compression method
|
||||
$dir['lastmod_time'] = unpack('v', fread($fp, 2)); # last mod file time
|
||||
$dir['lastmod_date'] = unpack('v', fread($fp, 2)); # last mod file date
|
||||
$dir['crc-32'] = fread($fp, 4); # crc-32
|
||||
$dir['compressed_size'] = unpack('V', fread($fp, 4)); # compressed size
|
||||
$dir['uncompressed_size'] = unpack('V', fread($fp, 4)); # uncompressed size
|
||||
|
||||
$file_name_len = unpack('v', fread($fp, 2)); # filename length
|
||||
$extra_field_len = unpack('v', fread($fp, 2)); # extra field length
|
||||
$file_comment_len = unpack('v', fread($fp, 2)); # file comment length
|
||||
|
||||
$dir['disk_number_start'] = unpack('v', fread($fp, 2)); # disk number start
|
||||
$dir['internal_attributes'] = unpack('v', fread($fp, 2)); # internal file attributes-byte1
|
||||
$dir['external_attributes1'] = unpack('v', fread($fp, 2)); # external file attributes-byte2
|
||||
$dir['external_attributes2'] = unpack('v', fread($fp, 2)); # external file attributes
|
||||
$dir['relative_offset'] = unpack('V', fread($fp, 4)); # relative offset of local header
|
||||
$dir['file_name'] = $this->cleanFileName(fread($fp, $file_name_len[1])); # filename
|
||||
$dir['extra_field'] = $extra_field_len[1] ? fread($fp, $extra_field_len[1]) : ''; # extra field
|
||||
$dir['file_comment'] = $file_comment_len[1] ? fread($fp, $file_comment_len[1]) : ''; # file comment
|
||||
|
||||
$dir_list[$dir['file_name']] = [
|
||||
'version_madeby' => $dir['version_madeby'][1],
|
||||
'version_needed' => $dir['version_needed'][1],
|
||||
'general_bit_flag' => str_pad(decbin($dir['general_bit_flag'][1]), 8, '0', STR_PAD_LEFT),
|
||||
'compression_method' => $dir['compression_method'][1],
|
||||
'lastmod_datetime' => $this->getTimeStamp($dir['lastmod_date'][1], $dir['lastmod_time'][1]),
|
||||
'crc-32' => str_pad(dechex(ord($dir['crc-32'][3])), 2, '0', STR_PAD_LEFT) .
|
||||
str_pad(dechex(ord($dir['crc-32'][2])), 2, '0', STR_PAD_LEFT) .
|
||||
str_pad(dechex(ord($dir['crc-32'][1])), 2, '0', STR_PAD_LEFT) .
|
||||
str_pad(dechex(ord($dir['crc-32'][0])), 2, '0', STR_PAD_LEFT),
|
||||
'compressed_size' => $dir['compressed_size'][1],
|
||||
'uncompressed_size' => $dir['uncompressed_size'][1],
|
||||
'disk_number_start' => $dir['disk_number_start'][1],
|
||||
'internal_attributes' => $dir['internal_attributes'][1],
|
||||
'external_attributes1' => $dir['external_attributes1'][1],
|
||||
'external_attributes2' => $dir['external_attributes2'][1],
|
||||
'relative_offset' => $dir['relative_offset'][1],
|
||||
'file_name' => $dir['file_name'],
|
||||
'extra_field' => $dir['extra_field'],
|
||||
'file_comment' => $dir['file_comment'],
|
||||
];
|
||||
$signature = fread($fp, 4);
|
||||
}
|
||||
|
||||
foreach ($dir_list as $k => $v) {
|
||||
if ($exclude && preg_match($exclude, (string) $k)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$i = $this->getFileHeaderInformation($v['relative_offset']);
|
||||
|
||||
$this->compressed_list[$k]['file_name'] = $k;
|
||||
$this->compressed_list[$k]['is_dir'] = $v['external_attributes1'] == 16 || substr($k, -1, 1) == '/';
|
||||
$this->compressed_list[$k]['compression_method'] = $v['compression_method'];
|
||||
$this->compressed_list[$k]['version_needed'] = $v['version_needed'];
|
||||
$this->compressed_list[$k]['lastmod_datetime'] = $v['lastmod_datetime'];
|
||||
$this->compressed_list[$k]['crc-32'] = $v['crc-32'];
|
||||
$this->compressed_list[$k]['compressed_size'] = $v['compressed_size'];
|
||||
$this->compressed_list[$k]['uncompressed_size'] = $v['uncompressed_size'];
|
||||
$this->compressed_list[$k]['lastmod_datetime'] = $v['lastmod_datetime'];
|
||||
$this->compressed_list[$k]['extra_field'] = $i['extra_field'];
|
||||
$this->compressed_list[$k]['contents_start_offset'] = $i['contents_start_offset'];
|
||||
|
||||
if (strtolower($stop_on_file) == strtolower($k)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function loadFileListBySignatures($stop_on_file = false, $exclude = false)
|
||||
{
|
||||
$fp = $this->fp();
|
||||
fseek($fp, 0);
|
||||
|
||||
$return = false;
|
||||
while (true) {
|
||||
$details = $this->getFileHeaderInformation();
|
||||
if (!$details) {
|
||||
fseek($fp, 12 - 4, SEEK_CUR); # 12: Data descriptor - 4: Signature (that will be read again)
|
||||
$details = $this->getFileHeaderInformation();
|
||||
}
|
||||
if (!$details) {
|
||||
break;
|
||||
}
|
||||
$filename = $details['file_name'];
|
||||
|
||||
if ($exclude && preg_match($exclude, (string) $filename)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->compressed_list[$filename] = $details;
|
||||
$return = true;
|
||||
|
||||
if (strtolower($stop_on_file) == strtolower($filename)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
protected function getFileHeaderInformation($start_offset = false)
|
||||
{
|
||||
$fp = $this->fp();
|
||||
|
||||
if ($start_offset !== false) {
|
||||
fseek($fp, $start_offset);
|
||||
}
|
||||
|
||||
$signature = fread($fp, 4);
|
||||
if ($signature == $this->zip_sig) {
|
||||
# Get information about the zipped file
|
||||
$file = [];
|
||||
$file['version_needed'] = unpack('v', fread($fp, 2)); # version needed to extract
|
||||
$file['general_bit_flag'] = unpack('v', fread($fp, 2)); # general purpose bit flag
|
||||
$file['compression_method'] = unpack('v', fread($fp, 2)); # compression method
|
||||
$file['lastmod_time'] = unpack('v', fread($fp, 2)); # last mod file time
|
||||
$file['lastmod_date'] = unpack('v', fread($fp, 2)); # last mod file date
|
||||
$file['crc-32'] = fread($fp, 4); # crc-32
|
||||
$file['compressed_size'] = unpack('V', fread($fp, 4)); # compressed size
|
||||
$file['uncompressed_size'] = unpack('V', fread($fp, 4)); # uncompressed size
|
||||
|
||||
$file_name_len = unpack('v', fread($fp, 2)); # filename length
|
||||
$extra_field_len = unpack('v', fread($fp, 2)); # extra field length
|
||||
|
||||
$file['file_name'] = $this->cleanFileName(fread($fp, $file_name_len[1])); # filename
|
||||
$file['extra_field'] = $extra_field_len[1] ? fread($fp, $extra_field_len[1]) : ''; # extra field
|
||||
$file['contents_start_offset'] = ftell($fp);
|
||||
|
||||
# Look for the next file
|
||||
fseek($fp, $file['compressed_size'][1], SEEK_CUR);
|
||||
|
||||
# Mount file table
|
||||
return [
|
||||
'file_name' => $file['file_name'],
|
||||
'is_dir' => substr($file['file_name'], -1, 1) == '/',
|
||||
'compression_method' => $file['compression_method'][1],
|
||||
'version_needed' => $file['version_needed'][1],
|
||||
'lastmod_datetime' => $this->getTimeStamp($file['lastmod_date'][1], $file['lastmod_time'][1]),
|
||||
'crc-32' => str_pad(dechex(ord($file['crc-32'][3])), 2, '0', STR_PAD_LEFT) .
|
||||
str_pad(dechex(ord($file['crc-32'][2])), 2, '0', STR_PAD_LEFT) .
|
||||
str_pad(dechex(ord($file['crc-32'][1])), 2, '0', STR_PAD_LEFT) .
|
||||
str_pad(dechex(ord($file['crc-32'][0])), 2, '0', STR_PAD_LEFT),
|
||||
'compressed_size' => $file['compressed_size'][1],
|
||||
'uncompressed_size' => $file['uncompressed_size'][1],
|
||||
'extra_field' => $file['extra_field'],
|
||||
'general_bit_flag' => str_pad(decbin($file['general_bit_flag'][1]), 8, '0', STR_PAD_LEFT),
|
||||
'contents_start_offset' => $file['contents_start_offset'],
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getTimeStamp($date, $time)
|
||||
{
|
||||
$BINlastmod_date = str_pad(decbin($date), 16, '0', STR_PAD_LEFT);
|
||||
$BINlastmod_time = str_pad(decbin($time), 16, '0', STR_PAD_LEFT);
|
||||
$lastmod_dateY = bindec(substr($BINlastmod_date, 0, 7)) + 1980;
|
||||
$lastmod_dateM = bindec(substr($BINlastmod_date, 7, 4));
|
||||
$lastmod_dateD = bindec(substr($BINlastmod_date, 11, 5));
|
||||
$lastmod_timeH = bindec(substr($BINlastmod_time, 0, 5));
|
||||
$lastmod_timeM = bindec(substr($BINlastmod_time, 5, 6));
|
||||
$lastmod_timeS = bindec(substr($BINlastmod_time, 11, 5)) * 2;
|
||||
|
||||
return mktime($lastmod_timeH, $lastmod_timeM, $lastmod_timeS, $lastmod_dateM, $lastmod_dateD, $lastmod_dateY);
|
||||
}
|
||||
|
||||
protected function cleanFileName($n)
|
||||
{
|
||||
$n = str_replace('../', '', (string) $n);
|
||||
$n = preg_replace('#^/+#', '', (string) $n);
|
||||
|
||||
return $n;
|
||||
}
|
||||
|
||||
protected function memoryAllocate($size)
|
||||
{
|
||||
$mem_used = function_exists('memory_get_usage') ? @memory_get_usage() : 4000000;
|
||||
$mem_limit = @ini_get('memory_limit');
|
||||
if ($mem_limit && trim((string) $mem_limit) === '-1' || !files::str2bytes($mem_limit)) {
|
||||
// Cope with memory_limit set to -1 in PHP.ini
|
||||
return;
|
||||
}
|
||||
if ($mem_used && $mem_limit) {
|
||||
$mem_limit = files::str2bytes($mem_limit);
|
||||
$mem_avail = $mem_limit - $mem_used - (512 * 1024);
|
||||
$mem_needed = $size;
|
||||
|
||||
if ($mem_needed > $mem_avail) {
|
||||
if (@ini_set('memory_limit', (string) ($mem_limit + $mem_needed + $mem_used)) === false) {
|
||||
throw new Exception(__('Not enough memory to open file.'));
|
||||
}
|
||||
|
||||
if (!$this->memory_limit) {
|
||||
$this->memory_limit = $mem_limit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,352 @@
|
|||
<?php
|
||||
/**
|
||||
* @class fileZip
|
||||
*
|
||||
* @package Clearbricks
|
||||
* @subpackage Zip
|
||||
*
|
||||
* @copyright Olivier Meunier & Association Dotclear
|
||||
* @copyright GPL-2.0-only
|
||||
*/
|
||||
class fileZip
|
||||
{
|
||||
protected $entries = [];
|
||||
protected $root_dir = null;
|
||||
|
||||
protected $ctrl_dir = [];
|
||||
protected $eof_ctrl_dir = "\x50\x4b\x05\x06\x00\x00\x00\x00";
|
||||
protected $old_offset = 0;
|
||||
|
||||
protected $fp;
|
||||
protected $memory_limit = null;
|
||||
|
||||
protected $exclusions = [];
|
||||
|
||||
public function __construct($out_fp)
|
||||
{
|
||||
if (!is_resource($out_fp)) {
|
||||
throw new Exception('Output file descriptor is not a resource');
|
||||
}
|
||||
|
||||
if (!in_array(get_resource_type($out_fp), ['stream', 'file'])) {
|
||||
throw new Exception('Output file descriptor is not a valid resource');
|
||||
}
|
||||
|
||||
$this->fp = $out_fp;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if ($this->memory_limit) {
|
||||
ini_set('memory_limit', $this->memory_limit);
|
||||
}
|
||||
}
|
||||
|
||||
public function addExclusion($reg)
|
||||
{
|
||||
$this->exclusions[] = $reg;
|
||||
}
|
||||
|
||||
public function addFile($file, $name = null)
|
||||
{
|
||||
$file = preg_replace('#[\\\/]+#', '/', (string) $file);
|
||||
|
||||
if (!$name) {
|
||||
$name = $file;
|
||||
}
|
||||
$name = $this->formatName($name);
|
||||
|
||||
if ($this->isExcluded($name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file_exists($file) || !is_file($file)) {
|
||||
throw new Exception(__('File does not exist'));
|
||||
}
|
||||
if (!is_readable($file)) {
|
||||
throw new Exception(__('Cannot read file'));
|
||||
}
|
||||
|
||||
$info = stat($file);
|
||||
|
||||
$this->entries[$name] = [
|
||||
'file' => $file,
|
||||
'is_dir' => false,
|
||||
'mtime' => $info['mtime'],
|
||||
'size' => $info['size'],
|
||||
];
|
||||
}
|
||||
|
||||
public function addDirectory($dir, $name = null, $recursive = false)
|
||||
{
|
||||
$dir = preg_replace('#[\\\/]+#', '/', (string) $dir);
|
||||
if (substr($dir, -1 - 1) != '/') {
|
||||
$dir .= '/';
|
||||
}
|
||||
|
||||
if (!$name && $name !== '') {
|
||||
$name = $dir;
|
||||
}
|
||||
|
||||
if ($this->isExcluded($name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($name !== '') {
|
||||
if (substr($name, -1, 1) != '/') {
|
||||
$name .= '/';
|
||||
}
|
||||
|
||||
$name = $this->formatName($name);
|
||||
|
||||
if ($name !== '') {
|
||||
$this->entries[$name] = [
|
||||
'file' => null,
|
||||
'is_dir' => true,
|
||||
'mtime' => time(),
|
||||
'size' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($recursive) {
|
||||
if (!is_dir($dir)) {
|
||||
throw new Exception(__('Directory does not exist'));
|
||||
}
|
||||
if (!is_readable($dir)) {
|
||||
throw new Exception(__('Cannot read directory'));
|
||||
}
|
||||
|
||||
$D = dir($dir);
|
||||
while (($e = $D->read()) !== false) {
|
||||
if ($e == '.' || $e == '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_dir($dir . '/' . $e)) {
|
||||
$this->addDirectory($dir . $e, $name . $e, true);
|
||||
} elseif (is_file($dir . '/' . $e)) {
|
||||
$this->addFile($dir . $e, $name . $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function write()
|
||||
{
|
||||
foreach ($this->entries as $name => $v) {
|
||||
if ($v['is_dir']) {
|
||||
$this->writeDirectory($name);
|
||||
} else {
|
||||
$this->writeFile($name, $v['file'], $v['size'], $v['mtime']);
|
||||
}
|
||||
}
|
||||
|
||||
$ctrldir = implode('', $this->ctrl_dir);
|
||||
|
||||
fwrite(
|
||||
$this->fp,
|
||||
$ctrldir .
|
||||
$this->eof_ctrl_dir .
|
||||
pack('v', sizeof($this->ctrl_dir)) . # total # of entries "on this disk"
|
||||
pack('v', sizeof($this->ctrl_dir)) . # total # of entries overall
|
||||
pack('V', strlen($ctrldir)) . # size of central dir
|
||||
pack('V', $this->old_offset) . # offset to start of central dir
|
||||
"\x00\x00" # .zip file comment length
|
||||
);
|
||||
}
|
||||
|
||||
protected function writeDirectory($name)
|
||||
{
|
||||
if (!isset($this->entries[$name])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mdate = $this->makeDate(time());
|
||||
$mtime = $this->makeTime(time());
|
||||
|
||||
# Data descriptor
|
||||
$data_desc = "\x50\x4b\x03\x04" .
|
||||
"\x0a\x00" . # ver needed to extract
|
||||
"\x00\x00" . # gen purpose bit flag
|
||||
"\x00\x00" . # compression method
|
||||
pack('v', $mtime) . # last mod time
|
||||
pack('v', $mdate) . # last mod date
|
||||
pack('V', 0) . # crc32
|
||||
pack('V', 0) . # compressed filesize
|
||||
pack('V', 0) . # uncompressed filesize
|
||||
pack('v', strlen($name)) . # length of pathname
|
||||
pack('v', 0) . # extra field length
|
||||
$name . # end of "local file header" segment
|
||||
pack('V', 0) . # crc32
|
||||
pack('V', 0) . # compressed filesize
|
||||
pack('V', 0); # uncompressed filesize
|
||||
|
||||
$new_offset = $this->old_offset + strlen($data_desc);
|
||||
fwrite($this->fp, $data_desc);
|
||||
|
||||
# Add to central record
|
||||
$cdrec = "\x50\x4b\x01\x02" .
|
||||
"\x00\x00" . # version made by
|
||||
"\x0a\x00" . # version needed to extract
|
||||
"\x00\x00" . # gen purpose bit flag
|
||||
"\x00\x00" . # compression method
|
||||
pack('v', $mtime) . # last mod time
|
||||
pack('v', $mdate) . # last mod date
|
||||
pack('V', 0) . # crc32
|
||||
pack('V', 0) . # compressed filesize
|
||||
pack('V', 0) . # uncompressed filesize
|
||||
pack('v', strlen($name)) . # length of filename
|
||||
pack('v', 0) . # extra field length
|
||||
pack('v', 0) . # file comment length
|
||||
pack('v', 0) . # disk number start
|
||||
pack('v', 0) . # internal file attributes
|
||||
pack('V', 16) . # external file attributes - 'directory' bit set
|
||||
pack('V', $this->old_offset) . # relative offset of local header
|
||||
$name;
|
||||
|
||||
$this->old_offset = $new_offset;
|
||||
$this->ctrl_dir[] = $cdrec;
|
||||
}
|
||||
|
||||
protected function writeFile($name, $file, $size, $mtime)
|
||||
{
|
||||
if (!isset($this->entries[$name])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filesize = filesize($file);
|
||||
$this->memoryAllocate($filesize * 3);
|
||||
|
||||
$content = file_get_contents($file);
|
||||
|
||||
$unc_len = strlen($content);
|
||||
$crc = crc32($content);
|
||||
$zdata = gzdeflate($content);
|
||||
$c_len = strlen($zdata);
|
||||
|
||||
unset($content);
|
||||
|
||||
$mdate = $this->makeDate($mtime);
|
||||
$mtime = $this->makeTime($mtime);
|
||||
|
||||
# Data descriptor
|
||||
$data_desc = "\x50\x4b\x03\x04" .
|
||||
"\x14\x00" . # ver needed to extract
|
||||
"\x00\x00" . # gen purpose bit flag
|
||||
"\x08\x00" . # compression method
|
||||
pack('v', $mtime) . # last mod time
|
||||
pack('v', $mdate) . # last mod date
|
||||
pack('V', $crc) . # crc32
|
||||
pack('V', $c_len) . # compressed filesize
|
||||
pack('V', $unc_len) . # uncompressed filesize
|
||||
pack('v', strlen($name)) . # length of filename
|
||||
pack('v', 0) . # extra field length
|
||||
$name . # end of "local file header" segment
|
||||
$zdata . # "file data" segment
|
||||
pack('V', $crc) . # crc32
|
||||
pack('V', $c_len) . # compressed filesize
|
||||
pack('V', $unc_len); # uncompressed filesize
|
||||
|
||||
fwrite($this->fp, $data_desc);
|
||||
unset($zdata);
|
||||
|
||||
$new_offset = $this->old_offset + strlen($data_desc);
|
||||
|
||||
# Add to central directory record
|
||||
$cdrec = "\x50\x4b\x01\x02" .
|
||||
"\x00\x00" . # version made by
|
||||
"\x14\x00" . # version needed to extract
|
||||
"\x00\x00" . # gen purpose bit flag
|
||||
"\x08\x00" . # compression method
|
||||
pack('v', $mtime) . # last mod time
|
||||
pack('v', $mdate) . # last mod date
|
||||
pack('V', $crc) . # crc32
|
||||
pack('V', $c_len) . # compressed filesize
|
||||
pack('V', $unc_len) . # uncompressed filesize
|
||||
pack('v', strlen($name)) . # length of filename
|
||||
pack('v', 0) . # extra field length
|
||||
pack('v', 0) . # file comment length
|
||||
pack('v', 0) . # disk number start
|
||||
pack('v', 0) . # internal file attributes
|
||||
pack('V', 32) . # external file attributes - 'archive' bit set
|
||||
pack('V', $this->old_offset) . # relative offset of local header
|
||||
$name;
|
||||
|
||||
$this->old_offset = $new_offset;
|
||||
$this->ctrl_dir[] = $cdrec;
|
||||
}
|
||||
|
||||
protected function formatName($name)
|
||||
{
|
||||
if (substr($name, 0, 1) == '/') {
|
||||
$name = substr($name, 1);
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
protected function isExcluded($name)
|
||||
{
|
||||
foreach ($this->exclusions as $reg) {
|
||||
if (preg_match((string) $reg, (string) $name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function makeDate($ts)
|
||||
{
|
||||
$year = date('Y', $ts) - 1980;
|
||||
if ($year < 0) {
|
||||
$year = 0;
|
||||
}
|
||||
|
||||
$year = sprintf('%07b', $year);
|
||||
$month = sprintf('%04b', date('n', $ts));
|
||||
$day = sprintf('%05b', date('j', $ts));
|
||||
|
||||
return bindec($year . $month . $day);
|
||||
}
|
||||
|
||||
protected function makeTime($ts)
|
||||
{
|
||||
$hour = sprintf('%05b', date('G', $ts));
|
||||
$minute = sprintf('%06b', date('i', $ts));
|
||||
$second = sprintf('%05b', ceil(date('s', $ts) / 2));
|
||||
|
||||
return bindec($hour . $minute . $second);
|
||||
}
|
||||
|
||||
protected function memoryAllocate($size)
|
||||
{
|
||||
$mem_used = function_exists('memory_get_usage') ? @memory_get_usage() : 4000000;
|
||||
$mem_limit = @ini_get('memory_limit');
|
||||
if ($mem_limit && trim((string) $mem_limit) === '-1' || !files::str2bytes($mem_limit)) {
|
||||
// Cope with memory_limit set to -1 in PHP.ini
|
||||
return;
|
||||
}
|
||||
if ($mem_used && $mem_limit) {
|
||||
$mem_limit = files::str2bytes($mem_limit);
|
||||
$mem_avail = $mem_limit - $mem_used - (512 * 1024);
|
||||
$mem_needed = $size;
|
||||
|
||||
if ($mem_needed > $mem_avail) {
|
||||
if (@ini_set('memory_limit', (string) ($mem_limit + $mem_needed + $mem_used)) === false) {
|
||||
throw new Exception(__('Not enough memory to open file.'));
|
||||
}
|
||||
|
||||
if (!$this->memory_limit) {
|
||||
$this->memory_limit = $mem_limit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
Subproject commit e877570a574598b3b0812ac9fe1f778f11d8c4fc
|
|
@ -14,8 +14,8 @@ define('DC_START_TIME', microtime(true));
|
|||
# ClearBricks, DotClear classes auto-loader
|
||||
if (@is_dir(implode(DIRECTORY_SEPARATOR, ['usr', 'lib', 'clearbricks']))) {
|
||||
define('CLEARBRICKS_PATH', implode(DIRECTORY_SEPARATOR, ['usr', 'lib', 'clearbricks']));
|
||||
} elseif (is_dir(implode(DIRECTORY_SEPARATOR, [__DIR__, 'libs', 'clearbricks']))) {
|
||||
define('CLEARBRICKS_PATH', implode(DIRECTORY_SEPARATOR, [__DIR__, 'libs', 'clearbricks']));
|
||||
} elseif (is_dir(implode(DIRECTORY_SEPARATOR, [__DIR__, 'helper']))) {
|
||||
define('CLEARBRICKS_PATH', implode(DIRECTORY_SEPARATOR, [__DIR__, 'helper']));
|
||||
} elseif (isset($_SERVER['CLEARBRICKS_PATH']) && is_dir($_SERVER['CLEARBRICKS_PATH'])) {
|
||||
define('CLEARBRICKS_PATH', $_SERVER['CLEARBRICKS_PATH']);
|
||||
}
|
||||
|
|
|
@ -33,7 +33,8 @@ parameters:
|
|||
|
||||
excludePaths:
|
||||
- inc/config.php
|
||||
- inc/libs/clearbricks/tests/*/*
|
||||
- inc/libs/clearbricks/*/*
|
||||
# - inc/libs/clearbricks/tests/*/*
|
||||
|
||||
dynamicConstantNames:
|
||||
- DC_ADBLOCKER_CHECK
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
# ***** BEGIN LICENSE BLOCK *****
|
||||
# This file is part of Clearbricks.
|
||||
# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
|
||||
# All rights reserved.
|
||||
#
|
||||
# Clearbricks is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Clearbricks is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Clearbricks; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# ***** END LICENSE BLOCK *****
|
||||
|
||||
define('CLEARBRICKS_PATH', __DIR__ . '/../../inc/helper');
|
||||
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
$__autoload = [];
|
||||
|
||||
$__autoload['dbStruct'] = CLEARBRICKS_PATH . '/dbschema/class.dbstruct.php';
|
||||
$__autoload['dbSchema'] = CLEARBRICKS_PATH . '/dbschema/class.dbschema.php';
|
||||
$__autoload['mysqliSchema'] = CLEARBRICKS_PATH . '/dbschema/class.mysqli.dbschema.php';
|
||||
$__autoload['mysqlimb4Schema'] = CLEARBRICKS_PATH . '/dbschema/class.mysqlimb4.dbschema.php';
|
||||
$__autoload['pgsqlSchema'] = CLEARBRICKS_PATH . '/dbschema/class.pgsql.dbschema.php';
|
||||
$__autoload['sqliteSchema'] = CLEARBRICKS_PATH . '/dbschema/class.sqlite.dbschema.php';
|
||||
|
||||
$__autoload['dbLayer'] = CLEARBRICKS_PATH . '/dblayer/dblayer.php';
|
||||
$__autoload['mysqliConnection'] = CLEARBRICKS_PATH . '/dblayer/class.mysqli.php';
|
||||
$__autoload['mysqlimb4Connection'] = CLEARBRICKS_PATH . '/dblayer/class.mysqlimb4.php';
|
||||
$__autoload['pgsqlConnection'] = CLEARBRICKS_PATH . '/dblayer/class.pgsql.php';
|
||||
$__autoload['sqliteConnection'] = CLEARBRICKS_PATH . '/dblayer/class.sqlite.php';
|
||||
|
||||
function cb_autoload($name)
|
||||
{
|
||||
global $__autoload;
|
||||
|
||||
if (isset($__autoload[$name])) {
|
||||
require_once $__autoload[$name];
|
||||
}
|
||||
}
|
||||
spl_autoload_register('cb_autoload');
|
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
|
||||
# -- BEGIN LICENSE BLOCK ---------------------------------------
|
||||
#
|
||||
# This file is part of Dotclear 2.
|
||||
#
|
||||
# Copyright (c) Olivier Meunier & Association Dotclear
|
||||
# Licensed under the GPL version 2.0 license.
|
||||
# See LICENSE file or
|
||||
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
#
|
||||
# -- END LICENSE BLOCK -----------------------------------------
|
||||
|
||||
namespace tests\unit;
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
require_once CLEARBRICKS_PATH . '/common/lib.crypt.php';
|
||||
|
||||
use atoum;
|
||||
use Faker;
|
||||
|
||||
/**
|
||||
* Crypt test.
|
||||
*/
|
||||
class crypt extends atoum
|
||||
{
|
||||
public const BIG_KEY_SIZE = 200;
|
||||
public const DATA_SIZE = 50;
|
||||
|
||||
private $big_key;
|
||||
private $data;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$faker = Faker\Factory::create();
|
||||
$this->big_key = $faker->text(self::BIG_KEY_SIZE);
|
||||
$this->data = $faker->text(self::DATA_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test big key. crypt don't allow key > than 64 cars
|
||||
*/
|
||||
public function testHMacBigKeyMD5()
|
||||
{
|
||||
$this
|
||||
->string(\crypt::hmac($this->big_key, $this->data, 'md5'))
|
||||
->isIdenticalTo(hash_hmac('md5', $this->data, $this->big_key));
|
||||
}
|
||||
|
||||
/**
|
||||
* hmac implicit SHA1 encryption (default argument)
|
||||
*/
|
||||
public function testHMacSHA1Implicit()
|
||||
{
|
||||
$this
|
||||
->string(\crypt::hmac($this->big_key, $this->data))
|
||||
->isIdenticalTo(hash_hmac('sha1', $this->data, $this->big_key));
|
||||
}
|
||||
|
||||
/**
|
||||
* hmac explicit SHA1 encryption
|
||||
*/
|
||||
public function testHMacSHA1Explicit()
|
||||
{
|
||||
$this
|
||||
->string(\crypt::hmac($this->big_key, $this->data, 'sha1'))
|
||||
->isIdenticalTo(hash_hmac('sha1', $this->data, $this->big_key));
|
||||
}
|
||||
|
||||
/**
|
||||
* hmac explicit MD5 encryption
|
||||
*/
|
||||
public function testHMacMD5()
|
||||
{
|
||||
$this
|
||||
->string(\crypt::hmac($this->big_key, $this->data, 'md5'))
|
||||
->isIdenticalTo(hash_hmac('md5', $this->data, $this->big_key));
|
||||
}
|
||||
|
||||
/**
|
||||
* If the encoder is not known, fallback into sha1 encoder (if PHP hash_hmac() exists)
|
||||
*/
|
||||
public function testHMacFallback()
|
||||
{
|
||||
$this
|
||||
->string(\crypt::hmac($this->big_key, $this->data, 'dummyencoder'))
|
||||
->isIdenticalTo(hash_hmac('sha1', $this->data, $this->big_key));
|
||||
}
|
||||
|
||||
/**
|
||||
* hmac_legacy implicit
|
||||
*/
|
||||
public function testHMacLegacy()
|
||||
{
|
||||
$this
|
||||
->string(\crypt::hmac_legacy($this->big_key, $this->data))
|
||||
->isIdenticalTo(hash_hmac('sha1', $this->data, $this->big_key));
|
||||
}
|
||||
|
||||
/**
|
||||
* hmac_legacy explicit MD5 encryption
|
||||
*/
|
||||
public function testHMacLegacyMD5()
|
||||
{
|
||||
$this
|
||||
->string(\crypt::hmac_legacy($this->big_key, $this->data, 'md5'))
|
||||
->isIdenticalTo(hash_hmac('md5', $this->data, $this->big_key));
|
||||
}
|
||||
|
||||
/**
|
||||
* hmac_legacy explicit Sha1 encryption
|
||||
*/
|
||||
public function testHMacLegacySha1()
|
||||
{
|
||||
$this
|
||||
->string(\crypt::hmac_legacy($this->big_key, $this->data, 'sha1'))
|
||||
->isIdenticalTo(hash_hmac('sha1', $this->data, $this->big_key));
|
||||
}
|
||||
|
||||
/**
|
||||
* If the encoder is not known, fallback into sha1 encoder (if PHP hash_hmac() exists)
|
||||
*/
|
||||
public function testHMacLegacyFallback()
|
||||
{
|
||||
$this
|
||||
->string(\crypt::hmac_legacy($this->big_key, $this->data, 'dummyencoder'))
|
||||
->isIdenticalTo(hash_hmac('md5', $this->data, $this->big_key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Password must be 8 char size and only contains alpha numerical
|
||||
* values
|
||||
*/
|
||||
public function testCreatePassword()
|
||||
{
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$this
|
||||
->string(\crypt::createPassword())
|
||||
->hasLength(8)
|
||||
->match('/[a-zA-Z0-9@\!\$]/');
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$this
|
||||
->string(\crypt::createPassword(10))
|
||||
->hasLength(10)
|
||||
->match('/[a-zA-Z0-9@\!\$]/');
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$this
|
||||
->string(\crypt::createPassword(13))
|
||||
->hasLength(13)
|
||||
->match('/[a-zA-Z0-9@\!\$]/');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
<?php
|
||||
|
||||
# -- BEGIN LICENSE BLOCK ---------------------------------------
|
||||
#
|
||||
# This file is part of Dotclear 2.
|
||||
#
|
||||
# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
|
||||
# Licensed under the GPL version 2.0 license.
|
||||
# See LICENSE file or
|
||||
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
#
|
||||
# -- END LICENSE BLOCK -----------------------------------------
|
||||
|
||||
namespace tests\unit;
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
require_once CLEARBRICKS_PATH . '/common/lib.l10n.php';
|
||||
require_once CLEARBRICKS_PATH . '/common/lib.date.php';
|
||||
|
||||
use atoum;
|
||||
|
||||
/**
|
||||
* Test clearbrick dt (date) class.
|
||||
*/
|
||||
class dt extends atoum
|
||||
{
|
||||
/**
|
||||
* Normal way. The result must be as the PHP function.
|
||||
*/
|
||||
public function testStrNormal()
|
||||
{
|
||||
\dt::setTZ('UTC');
|
||||
$this
|
||||
->string(\dt::str('%d%m%Y'))
|
||||
// Avoid deprecated notice until PHP 9 should be supported or a correct strftime() replacement
|
||||
->isEqualTo(@strftime('%d%m%Y'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp is set to 1 which is 1 second after Janurary, 1th 1970
|
||||
*/
|
||||
public function testStrTimestamp()
|
||||
{
|
||||
\dt::setTZ('UTC');
|
||||
$this
|
||||
->string(\dt::str('%d%m%Y', 1))
|
||||
->isEqualTo('01011970');
|
||||
}
|
||||
|
||||
/**
|
||||
* Difference between two time zones. Europe/Paris is GMT+1 and Indian/Reunion is
|
||||
* GMT+4. The difference might be 3.
|
||||
* The timestamp is forced due to the summer or winter time.
|
||||
*/
|
||||
public function testStrWithTimestampAndTimezone()
|
||||
{
|
||||
\dt::setTZ('UTC');
|
||||
$this
|
||||
->integer((int) \dt::str('%H', 1, 'Indian/Reunion') - (int) \dt::str('%H', 1, 'Europe/Paris'))
|
||||
->isEqualTo(3);
|
||||
}
|
||||
|
||||
/**
|
||||
* dt2str is a wrapper for dt::str but convert the human readable time
|
||||
* into a computer understandable time
|
||||
*/
|
||||
public function testDt2Str()
|
||||
{
|
||||
\dt::setTZ('UTC');
|
||||
$this
|
||||
->string(\dt::dt2str('%Y', '1970-01-01'))
|
||||
->isEqualTo(\dt::str('%Y', 1));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function testSetGetTZ()
|
||||
{
|
||||
\dt::setTZ('Indian/Reunion');
|
||||
$this->string(\dt::getTZ())->isEqualTo('Indian/Reunion');
|
||||
}
|
||||
|
||||
/**
|
||||
* dtstr with anything but the time. We don't test strtodate,
|
||||
* we test dt1str will always have the same behaviour.
|
||||
*/
|
||||
public function testDt2DummyStr()
|
||||
{
|
||||
\dt::setTZ('UTC');
|
||||
$this
|
||||
->string(\dt::dt2str('%Y', 'Everything but a time'))
|
||||
->isEqualTo(\dt::str('%Y'));
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert timestamp to ISO8601 date
|
||||
*/
|
||||
public function testISO8601()
|
||||
{
|
||||
\dt::setTZ('UTC');
|
||||
$this
|
||||
->string(\dt::iso8601(1, 'UTC'))
|
||||
->isEqualTo('1970-01-01T00:00:01+00:00');
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert timestamp to ISO8601 date but not UTC.
|
||||
*/
|
||||
public function testISO8601WithAnotherTimezone()
|
||||
{
|
||||
\dt::setTZ('UTC');
|
||||
$this
|
||||
->string(\dt::iso8601(1, 'Indian/Reunion'))
|
||||
->isEqualTo('1970-01-01T00:00:01+04:00');
|
||||
}
|
||||
|
||||
public function testRfc822()
|
||||
{
|
||||
\dt::setTZ('UTC');
|
||||
$this
|
||||
->string(\dt::rfc822(1, 'Indian/Reunion'))
|
||||
->isEqualTo('Thu, 01 Jan 1970 00:00:01 +0400');
|
||||
}
|
||||
|
||||
public function testGetTimeOffset()
|
||||
{
|
||||
\dt::setTZ('UTC');
|
||||
$this
|
||||
->integer(\dt::getTimeOffset('Indian/Reunion'))
|
||||
->isEqualTo(4 * 3600);
|
||||
}
|
||||
|
||||
public function testToUTC()
|
||||
{
|
||||
\dt::setTZ('Indian/Reunion'); // UTC + 4
|
||||
$this->integer(\dt::toUTC(4 * 3600))
|
||||
->isEqualTo(0);
|
||||
}
|
||||
|
||||
/*
|
||||
* AddTimezone implies getZones but I prefer testing both of them separatly
|
||||
*/
|
||||
public function testAddTimezone()
|
||||
{
|
||||
\dt::setTZ('UTC');
|
||||
$this
|
||||
->integer(\dt::addTimeZone('Indian/Reunion', 0))
|
||||
->isEqualTo(4 * 3600);
|
||||
|
||||
\dt::setTZ('UTC');
|
||||
$this
|
||||
->integer(\dt::addTimeZone('Indian/Reunion') - time() - \dt::getTimeOffset('Indian/Reunion'))
|
||||
->isEqualTo(0);
|
||||
}
|
||||
|
||||
/*
|
||||
* There's many different time zone. Basicly, dt::getZone call a PHP function.
|
||||
* Ensure that the key is the value array('time/zone' => 'time/zone')
|
||||
*/
|
||||
public function testGetZones()
|
||||
{
|
||||
$tzs = \dt::getZones();
|
||||
|
||||
$this
|
||||
->array($tzs)
|
||||
->isNotNull();
|
||||
|
||||
$this
|
||||
->string($tzs['Europe/Paris'])
|
||||
->isEqualTo('Europe/Paris');
|
||||
|
||||
// Test another call
|
||||
$tzs = \dt::getZones();
|
||||
|
||||
$this
|
||||
->array($tzs)
|
||||
->isNotNull();
|
||||
|
||||
$this
|
||||
->string($tzs['Indian/Reunion'])
|
||||
->isEqualTo('Indian/Reunion');
|
||||
}
|
||||
|
||||
public function testGetZonesFlip()
|
||||
{
|
||||
$tzs = \dt::getZones(true, false);
|
||||
|
||||
$this
|
||||
->array($tzs)
|
||||
->isNotNull();
|
||||
|
||||
$this
|
||||
->string($tzs['Europe/Paris'])
|
||||
->isEqualTo('Europe/Paris');
|
||||
}
|
||||
|
||||
public function testGetZonesGroup()
|
||||
{
|
||||
$tzs = \dt::getZones(true, true);
|
||||
|
||||
$this
|
||||
->array($tzs)
|
||||
->isNotNull();
|
||||
|
||||
$this
|
||||
->array($tzs['Europe'])
|
||||
->isNotNull()
|
||||
->string($tzs['Europe']['Europe/Paris'])
|
||||
->isEqualTo('Europe/Paris');
|
||||
}
|
||||
|
||||
public function testStr()
|
||||
{
|
||||
\dt::setTZ('UTC');
|
||||
$this
|
||||
->string(\dt::str('%a %A %b %B', 1))
|
||||
->isEqualTo('_Thu Thursday _Jan January');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,593 @@
|
|||
<?php
|
||||
# -- BEGIN LICENSE BLOCK ---------------------------------------
|
||||
#
|
||||
# This file is part of Dotclear 2.
|
||||
#
|
||||
# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
|
||||
# Licensed under the GPL version 2.0 license.
|
||||
# See LICENSE file or
|
||||
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
#
|
||||
# -- END LICENSE BLOCK -----------------------------------------
|
||||
|
||||
namespace tests\unit;
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
require_once CLEARBRICKS_PATH . '/common/lib.l10n.php';
|
||||
require_once CLEARBRICKS_PATH . '/common/lib.files.php';
|
||||
require_once CLEARBRICKS_PATH . '/common/lib.text.php';
|
||||
|
||||
define('TEST_DIRECTORY', realpath(
|
||||
__DIR__ . '/../fixtures/files'
|
||||
));
|
||||
|
||||
use atoum;
|
||||
|
||||
/*
|
||||
* Test common/lib.files.php
|
||||
*/
|
||||
class files extends atoum
|
||||
{
|
||||
protected function cleanTemp()
|
||||
{
|
||||
// Look for test*, temp*, void* directories and files in TEST_DIRECTORY and destroys them
|
||||
$items = \files::scandir(TEST_DIRECTORY);
|
||||
if (is_array($items)) {
|
||||
foreach ($items as $value) {
|
||||
if (in_array(substr($value, 0, 4), ['test', 'temp', 'void'])) {
|
||||
$name = TEST_DIRECTORY . DIRECTORY_SEPARATOR . $value;
|
||||
if (is_dir($name)) {
|
||||
\files::deltree($name);
|
||||
} else {
|
||||
@unlink($name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public function setUp()
|
||||
{
|
||||
$counter = 0;
|
||||
$this->cleanTemp();
|
||||
// Check if everything is clean (as OS may have a filesystem cache for dir list)
|
||||
while (($items = \files::scandir(TEST_DIRECTORY, true)) !== ['.', '..', '02-two.txt', '1-one.txt', '30-three.txt']) {
|
||||
$counter++;
|
||||
if ($counter < 10) {
|
||||
// Wait 1 second, then clean again
|
||||
var_dump($items);
|
||||
sleep(1);
|
||||
$this->cleanTemp();
|
||||
} else {
|
||||
// Can't do more then let's go
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
public function tearDown()
|
||||
{
|
||||
$this->cleanTemp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a directory. For that we use the /../fixtures/files which contains
|
||||
* know files
|
||||
*/
|
||||
public function testScanDir()
|
||||
{
|
||||
// Normal (sorted)
|
||||
$this
|
||||
->array(\files::scandir(TEST_DIRECTORY))
|
||||
->isIdenticalTo(['.', '..', '02-two.txt', '1-one.txt', '30-three.txt']);
|
||||
// Not sorted
|
||||
$this
|
||||
->array(\files::scandir(TEST_DIRECTORY, false))
|
||||
->containsValues(['.', '..', '1-one.txt', '02-two.txt', '30-three.txt']);
|
||||
|
||||
// DOn't exists
|
||||
$this
|
||||
->exception(function () {
|
||||
\files::scandir('thisdirectorydontexists');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the extension
|
||||
*/
|
||||
public function testExtension()
|
||||
{
|
||||
$this
|
||||
->string(\files::getExtension('fichier.txt'))
|
||||
->isEqualTo('txt');
|
||||
|
||||
$this
|
||||
->string(\files::getExtension('fichier'))
|
||||
->isEqualTo('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the mime type with two well know mimetype
|
||||
* Normally if a file type is unknow it must have a application/octet-stream mimetype
|
||||
* javascript files might have an application/x-javascript mimetype regarding
|
||||
* W3C spec.
|
||||
* See http://en.wikipedia.org/wiki/Internet_media_type for all mimetypes
|
||||
*/
|
||||
public function testGetMimeType()
|
||||
{
|
||||
$this
|
||||
->string(\files::getMimeType('fichier.txt'))
|
||||
->isEqualTo('text/plain');
|
||||
|
||||
$this
|
||||
->string(\files::getMimeType('fichier.css'))
|
||||
->isEqualTo('text/css');
|
||||
|
||||
$this
|
||||
->string(\files::getMimeType('fichier.js'))
|
||||
->isEqualTo('application/javascript');
|
||||
|
||||
// FIXME: SHould be application/octet-stream (default for unknow)
|
||||
// See http://www.rfc-editor.org/rfc/rfc2046.txt section 4.
|
||||
// This test don't pass
|
||||
$this
|
||||
->string(\files::getMimeType('fichier.dummy'))
|
||||
->isEqualTo('application/octet-stream');
|
||||
}
|
||||
|
||||
/**
|
||||
* There's a lot of mimetypes. Only test if mimetypes array is not empty
|
||||
*/
|
||||
public function testMimeTypes()
|
||||
{
|
||||
$this
|
||||
->array(\files::mimeTypes())
|
||||
->isNotEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to register a new mimetype: test/test which don't exists
|
||||
*/
|
||||
public function testRegisterMimeType()
|
||||
{
|
||||
\files::registerMimeTypes(['text/test']);
|
||||
$this
|
||||
->array(\files::mimeTypes())
|
||||
->contains('text/test');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a file is deletable. Under windows every file is deletable
|
||||
* TODO: Do it under an Unix/Unix-like system
|
||||
*/
|
||||
public function testFileIsDeletable()
|
||||
{
|
||||
$tmpname = tempnam(TEST_DIRECTORY, 'testfile_1.txt');
|
||||
$file = fopen($tmpname, 'w+');
|
||||
$this
|
||||
->boolean(\files::isDeletable($tmpname))
|
||||
->isTrue();
|
||||
fclose($file);
|
||||
unlink($tmpname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a directory is deletable
|
||||
* TODO: Do it under Unix/Unix-like system
|
||||
*/
|
||||
public function testDirIsDeletable()
|
||||
{
|
||||
$dirname = TEST_DIRECTORY . DIRECTORY_SEPARATOR . 'testdirectory_2';
|
||||
mkdir($dirname);
|
||||
$this
|
||||
->boolean(\files::isDeletable($dirname))
|
||||
->isTrue();
|
||||
rmdir($dirname);
|
||||
|
||||
// Test with a non existing dir
|
||||
$this
|
||||
->boolean(\files::isDeletable($dirname))
|
||||
->isFalse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directories structure and delete it
|
||||
*/
|
||||
public function testDeltree()
|
||||
{
|
||||
$dirstructure = join(DIRECTORY_SEPARATOR, [TEST_DIRECTORY, 'temp_3', 'tests', 'are', 'good', 'for', 'you']);
|
||||
mkdir($dirstructure, 0700, true);
|
||||
touch($dirstructure . DIRECTORY_SEPARATOR . 'file.txt');
|
||||
$this
|
||||
->boolean(\files::deltree(join(DIRECTORY_SEPARATOR, [TEST_DIRECTORY, 'temp_3'])))
|
||||
->isTrue();
|
||||
|
||||
$this
|
||||
->boolean(is_dir(TEST_DIRECTORY . DIRECTORY_SEPARATOR . 'temp_3'))
|
||||
->isFalse();
|
||||
}
|
||||
|
||||
/**
|
||||
* There's a know bug on windows system with filemtime,
|
||||
* so this test might fail within this system
|
||||
*/
|
||||
public function testTouch()
|
||||
{
|
||||
$file_name = tempnam(TEST_DIRECTORY, 'testfile_4.txt');
|
||||
$fts = filemtime($file_name);
|
||||
// Must keep at least one second of difference
|
||||
sleep(1);
|
||||
\files::touch($file_name);
|
||||
clearstatcache(); // stats are cached, clear them!
|
||||
$sts = filemtime($file_name);
|
||||
$this
|
||||
->integer($sts)
|
||||
->isGreaterThan($fts);
|
||||
unlink($file_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a single directory
|
||||
*/
|
||||
public function testMakeDir()
|
||||
{
|
||||
// Test no parent
|
||||
$dirPath = TEST_DIRECTORY . DIRECTORY_SEPARATOR . 'testdirectory_5';
|
||||
\files::makeDir($dirPath);
|
||||
$this
|
||||
->boolean(is_dir($dirPath))
|
||||
->isTrue();
|
||||
\files::deltree($dirPath);
|
||||
|
||||
// Test with void name
|
||||
$this
|
||||
->variable(\files::makeDir(''))
|
||||
->isNull();
|
||||
|
||||
// test with already existing dir
|
||||
$this
|
||||
->variable(\files::makeDir(TEST_DIRECTORY))
|
||||
->isNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a directory structure
|
||||
*/
|
||||
public function testMakeDirWithParent()
|
||||
{
|
||||
// Test multitple parent
|
||||
$dirPath = TEST_DIRECTORY . DIRECTORY_SEPARATOR . 'temp_6/is/a/test/directory/';
|
||||
\files::makeDir($dirPath, true);
|
||||
$path = '';
|
||||
foreach ([TEST_DIRECTORY . DIRECTORY_SEPARATOR . 'temp_6', 'is', 'a', 'test', 'directory'] as $p) {
|
||||
$path .= $p . DIRECTORY_SEPARATOR;
|
||||
$this->boolean(is_dir($path));
|
||||
}
|
||||
\files::deltree(TEST_DIRECTORY . DIRECTORY_SEPARATOR . 'temp_6');
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to create an forbidden directory
|
||||
* Under windows try to create a reserved directory
|
||||
* Under Unix/Unix-like sytem try to create a directory at root dir
|
||||
*/
|
||||
public function testMakeDirImpossible()
|
||||
{
|
||||
if (DIRECTORY_SEPARATOR == '\\') {
|
||||
$dir = 'COM1'; // Windows system forbid that name
|
||||
} else {
|
||||
$dir = '/dummy'; // On Unix system can't create a directory at root
|
||||
}
|
||||
|
||||
$this->exception(function () use ($dir) {
|
||||
\files::makeDir($dir);
|
||||
});
|
||||
}
|
||||
|
||||
public function testInheritChmod()
|
||||
{
|
||||
$dirName = TEST_DIRECTORY . DIRECTORY_SEPARATOR . 'testdir_7';
|
||||
$sonDirName = $dirName . DIRECTORY_SEPARATOR . 'anotherDir';
|
||||
mkdir($dirName, 0777);
|
||||
mkdir($sonDirName);
|
||||
$parentPerms = fileperms($dirName);
|
||||
\files::inheritChmod($sonDirName);
|
||||
$sonPerms = fileperms($sonDirName);
|
||||
$this
|
||||
->boolean($sonPerms === $parentPerms)
|
||||
->isTrue();
|
||||
\files::deltree($dirName);
|
||||
|
||||
// Test again witha dir mode set
|
||||
\files::$dir_mode = 0770;
|
||||
mkdir($dirName, 0777);
|
||||
mkdir($sonDirName);
|
||||
$parentPerms = fileperms($dirName);
|
||||
\files::inheritChmod($sonDirName);
|
||||
$sonPerms = fileperms($sonDirName);
|
||||
$this
|
||||
->boolean($sonPerms === $parentPerms)
|
||||
->isFalse();
|
||||
$this
|
||||
->integer($sonPerms)
|
||||
->isEqualTo(16888); // Aka 0770
|
||||
\files::deltree($dirName);
|
||||
}
|
||||
|
||||
public function testPutContent()
|
||||
{
|
||||
$content = 'A Content';
|
||||
$filename = TEST_DIRECTORY . DIRECTORY_SEPARATOR . 'testfile_8.txt';
|
||||
@unlink($filename);
|
||||
\files::putContent($filename, $content);
|
||||
$this
|
||||
->string(file_get_contents($filename))
|
||||
->isEqualTo($content);
|
||||
unlink($filename);
|
||||
}
|
||||
|
||||
public function testPutContentException()
|
||||
{
|
||||
// Test exceptions
|
||||
$content = 'A Content';
|
||||
$filename = TEST_DIRECTORY . DIRECTORY_SEPARATOR . 'testfile_9.txt';
|
||||
@unlink($filename);
|
||||
\files::putContent($filename, $content);
|
||||
$this
|
||||
->exception(function () use ($filename) {
|
||||
chmod($filename, 0400); // Read only
|
||||
\files::putContent($filename, 'unwritable');
|
||||
})
|
||||
->hasMessage('File is not writable.');
|
||||
chmod($filename, 0700);
|
||||
unlink($filename);
|
||||
}
|
||||
|
||||
public function testSize()
|
||||
{
|
||||
$this
|
||||
->string(\files::size(512))
|
||||
->isEqualTo('512 B');
|
||||
|
||||
$this
|
||||
->string(\files::size(1024))
|
||||
->isEqualTo('1 KB');
|
||||
|
||||
$this
|
||||
->string(\files::size(1024 + 1024 + 1))
|
||||
->isEqualTo('2 KB');
|
||||
|
||||
$this
|
||||
->string(\files::size(1024 * 1024))
|
||||
->isEqualTo('1 MB');
|
||||
|
||||
$this
|
||||
->string(\files::size(1024 * 1024 * 1024))
|
||||
->isEqualTo('1 GB');
|
||||
|
||||
$this
|
||||
->string(\files::size(1024 * 1024 * 1024 * 3))
|
||||
->isEqualTo('3 GB');
|
||||
|
||||
$this
|
||||
->string(\files::size(1024 * 1024 * 1024 * 1024))
|
||||
->isEqualTo('1 TB');
|
||||
}
|
||||
|
||||
public function testStr2Bytes()
|
||||
{
|
||||
$this
|
||||
->float(\files::str2bytes('512B'))
|
||||
->isEqualTo((float) 512);
|
||||
|
||||
$this
|
||||
->float(\files::str2bytes('512 B'))
|
||||
->isEqualTo((float) 512);
|
||||
|
||||
$this
|
||||
->float(\files::str2bytes('1k'))
|
||||
->isEqualTo((float) 1024);
|
||||
|
||||
$this
|
||||
->float(\files::str2bytes('1M'))
|
||||
->isEqualTo((float) 1024 * 1024);
|
||||
// Max int limit reached, we have a float here
|
||||
$this
|
||||
->float(\files::str2bytes('2G'))
|
||||
->isEqualTo((float) 2 * 1024 * 1024 * 1024);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test uploadStatus
|
||||
*
|
||||
* This must fail until files::uploadStatus don't handle UPLOAD_ERR_EXTENSION
|
||||
*/
|
||||
public function testUploadStatus()
|
||||
{
|
||||
// Create a false $_FILES global without error
|
||||
$file = [
|
||||
'name' => 'test.jpg',
|
||||
'size' => ini_get('post_max_size'),
|
||||
'tmp_name' => 'temptestname.jpg',
|
||||
'error' => UPLOAD_ERR_OK,
|
||||
'type' => 'image/jpeg'
|
||||
];
|
||||
|
||||
$this
|
||||
->boolean(\files::uploadStatus($file))
|
||||
->isTrue();
|
||||
|
||||
// Simulate error
|
||||
$file['error'] = UPLOAD_ERR_INI_SIZE;
|
||||
$this->exception(function () use ($file) {\files::uploadStatus($file);});
|
||||
|
||||
$file['error'] = UPLOAD_ERR_FORM_SIZE;
|
||||
$this->exception(function () use ($file) {\files::uploadStatus($file);});
|
||||
|
||||
$file['error'] = UPLOAD_ERR_PARTIAL;
|
||||
$this->exception(function () use ($file) {\files::uploadStatus($file);});
|
||||
|
||||
$file['error'] = UPLOAD_ERR_NO_TMP_DIR; // Since PHP 5.0.3
|
||||
$this->exception(function () use ($file) {\files::uploadStatus($file);});
|
||||
|
||||
$file['error'] = UPLOAD_ERR_NO_FILE;
|
||||
$this->exception(function () use ($file) {\files::uploadStatus($file);});
|
||||
|
||||
$file['error'] = UPLOAD_ERR_CANT_WRITE;
|
||||
$this->exception(function () use ($file) {\files::uploadStatus($file);});
|
||||
|
||||
// This part might fail
|
||||
if (version_compare(phpversion(), '5.2.0', '>')) {
|
||||
$file['error'] = UPLOAD_ERR_EXTENSION; // Since PHP 5.2
|
||||
$this->exception(function () use ($file) {\files::uploadStatus($file);});
|
||||
}
|
||||
}
|
||||
|
||||
public function testGetDirList()
|
||||
{
|
||||
\files::getDirList(TEST_DIRECTORY, $arr);
|
||||
$this
|
||||
->array($arr)
|
||||
->isNotEmpty()
|
||||
->hasKeys(['files', 'dirs']);
|
||||
|
||||
$this
|
||||
->array($arr['files'])
|
||||
->isNotEmpty();
|
||||
|
||||
$this
|
||||
->array($arr['dirs'])
|
||||
->isNotEmpty();
|
||||
|
||||
$this
|
||||
->exception(function () {
|
||||
\files::getDirList(TEST_DIRECTORY . DIRECTORY_SEPARATOR . 'void', $arr);
|
||||
})
|
||||
->hasMessage(sprintf('%s is not a directory.', TEST_DIRECTORY . DIRECTORY_SEPARATOR . 'void'));
|
||||
|
||||
// Deep structure read
|
||||
$dirstructure = join(DIRECTORY_SEPARATOR, [TEST_DIRECTORY, 'temp_10', 'tests', 'are', 'good', 'for', 'you']);
|
||||
mkdir($dirstructure, 0700, true);
|
||||
\files::getDirList(join(DIRECTORY_SEPARATOR, [TEST_DIRECTORY, 'temp_10']), $arr);
|
||||
$this
|
||||
->array($arr['dirs'])
|
||||
->isNotEmpty();
|
||||
\files::deltree(join(DIRECTORY_SEPARATOR, [TEST_DIRECTORY, 'temp_10']));
|
||||
|
||||
// Unreadable dir
|
||||
$dirname = TEST_DIRECTORY . DIRECTORY_SEPARATOR . 'void_11';
|
||||
mkdir($dirname);
|
||||
$this
|
||||
->exception(function () use ($dirname) {
|
||||
chmod($dirname, 0200);
|
||||
\files::getDirList($dirname, $arr);
|
||||
})
|
||||
->hasMessage('Unable to open directory.');
|
||||
chmod($dirname, 0700);
|
||||
\files::deltree($dirname);
|
||||
}
|
||||
|
||||
public function testTidyFilename()
|
||||
{
|
||||
$this
|
||||
->string(\files::tidyFileName('a test file.txt'))
|
||||
->isEqualTo('a_test_file.txt');
|
||||
}
|
||||
}
|
||||
|
||||
class path extends atoum
|
||||
{
|
||||
public function testRealUnstrict()
|
||||
{
|
||||
if (DIRECTORY_SEPARATOR == '\\') {
|
||||
// Hack to make it works under Windows
|
||||
$this
|
||||
->string(str_replace('/', '\\', \path::real(__DIR__ . '/../fixtures/files', false)))
|
||||
->isEqualTo(TEST_DIRECTORY);
|
||||
$this
|
||||
->string(str_replace('/', '\\', \path::real('tests/unit/fixtures/files', false)))
|
||||
->isEqualTo('/tests/unit/fixtures/files');
|
||||
$this
|
||||
->string(str_replace('/', '\\', \path::real('tests/./unit/fixtures/files', false)))
|
||||
->isEqualTo('/tests/unit/fixtures/files');
|
||||
} else {
|
||||
$this
|
||||
->string(\path::real(__DIR__ . '/../fixtures/files', false))
|
||||
->isEqualTo(TEST_DIRECTORY);
|
||||
$this
|
||||
->string(\path::real('tests/unit/fixtures/files', false))
|
||||
->isEqualTo('/tests/unit/fixtures/files');
|
||||
$this
|
||||
->string(\path::real('tests/./unit/fixtures/files', false))
|
||||
->isEqualTo('/tests/unit/fixtures/files');
|
||||
}
|
||||
}
|
||||
|
||||
public function testRealStrict()
|
||||
{
|
||||
if (DIRECTORY_SEPARATOR == '\\') {
|
||||
// Hack to make it works under Windows
|
||||
$this
|
||||
->string(str_replace('/', '\\', \path::real(__DIR__ . '/../fixtures/files', true)))
|
||||
->isEqualTo(TEST_DIRECTORY);
|
||||
} else {
|
||||
$this
|
||||
->string(\path::real(__DIR__ . '/../fixtures/files', true))
|
||||
->isEqualTo(TEST_DIRECTORY);
|
||||
}
|
||||
}
|
||||
|
||||
public function testClean()
|
||||
{
|
||||
$this
|
||||
->string(\path::clean('..' . DIRECTORY_SEPARATOR . 'testDirectory'))
|
||||
->isEqualTo(DIRECTORY_SEPARATOR . 'testDirectory');
|
||||
|
||||
$this
|
||||
->string(\path::clean(DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'testDirectory' . DIRECTORY_SEPARATOR))
|
||||
->isEqualTo(DIRECTORY_SEPARATOR . 'testDirectory');
|
||||
|
||||
$this
|
||||
->string(\path::clean(DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR . 'testDirectory' . DIRECTORY_SEPARATOR))
|
||||
->isEqualTo(DIRECTORY_SEPARATOR . 'testDirectory');
|
||||
|
||||
$this
|
||||
->string(\path::clean(DIRECTORY_SEPARATOR . 'testDirectory' . DIRECTORY_SEPARATOR . '..'))
|
||||
->isEqualTo(DIRECTORY_SEPARATOR . 'testDirectory');
|
||||
}
|
||||
|
||||
public function testInfo()
|
||||
{
|
||||
$info = \path::info(TEST_DIRECTORY . DIRECTORY_SEPARATOR . '1-one.txt');
|
||||
$this
|
||||
->array($info)
|
||||
->isNotEmpty()
|
||||
->hasKeys(['dirname', 'basename', 'extension', 'base']);
|
||||
|
||||
$this
|
||||
->string($info['dirname'])
|
||||
->isEqualTo(TEST_DIRECTORY);
|
||||
|
||||
$this
|
||||
->string($info['basename'])
|
||||
->isEqualTo('1-one.txt');
|
||||
|
||||
$this
|
||||
->string($info['extension'])
|
||||
->isEqualTo('txt');
|
||||
|
||||
$this
|
||||
->string($info['base'])
|
||||
->string('1-one');
|
||||
}
|
||||
|
||||
public function testFullFromRoot()
|
||||
{
|
||||
$this
|
||||
->string(\path::fullFromRoot('/test', '/'))
|
||||
->isEqualTo('/test');
|
||||
|
||||
$this
|
||||
->string(\path::fullFromRoot('test/string', '/home/sweethome'))
|
||||
->isEqualTo('/home/sweethome/test/string');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,597 @@
|
|||
<?php
|
||||
|
||||
# -- BEGIN LICENSE BLOCK ---------------------------------------
|
||||
#
|
||||
# This file is part of Dotclear 2.
|
||||
#
|
||||
# Copyright (c) Olivier Meunier & Association Dotclear
|
||||
# Licensed under the GPL version 2.0 license.
|
||||
# See LICENSE file or
|
||||
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
#
|
||||
# -- END LICENSE BLOCK -----------------------------------------
|
||||
|
||||
namespace tests\unit;
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
require_once CLEARBRICKS_PATH . '/common/lib.form.php';
|
||||
|
||||
use atoum;
|
||||
|
||||
class formSelectOption extends atoum
|
||||
{
|
||||
public function testOption()
|
||||
{
|
||||
$option = new \formSelectOption('un', 1, 'classme', 'data-test="This Is A Test"');
|
||||
|
||||
$this
|
||||
->string($option->render(0))
|
||||
->match('/<option.*?<\/option>/')
|
||||
->match('/<option\svalue="1".*?>un<\/option>/');
|
||||
|
||||
$this
|
||||
->string($option->render(1))
|
||||
->match('/<option.*?<\/option>/')
|
||||
->match('/<option.*?value="1".*?>un<\/option>/')
|
||||
->match('/<option.*?selected.*?>un<\/option>/');
|
||||
}
|
||||
|
||||
public function testOptionOpt()
|
||||
{
|
||||
$option = new \formSelectOption('deux', 2);
|
||||
|
||||
$this
|
||||
->string($option->render(0))
|
||||
->match('/<option.*?<\/option>/')
|
||||
->match('/<option\svalue="2".*?>deux<\/option>/');
|
||||
|
||||
$this
|
||||
->string($option->render(2))
|
||||
->match('/<option.*?<\/option>/')
|
||||
->match('/<option.*?value="2".*?>deux<\/option>/')
|
||||
->match('/<option.*?selected.*?>deux<\/option>/');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the form class.
|
||||
* formSelectOptions is implicitly tested with testCombo
|
||||
*/
|
||||
class form extends atoum
|
||||
{
|
||||
/**
|
||||
* Create a combo (select)
|
||||
*/
|
||||
public function testCombo()
|
||||
{
|
||||
$this
|
||||
->string(\form::combo('testID', [], '', 'classme', 'atabindex', true, 'data-test="This Is A Test"'))
|
||||
->contains('<select')
|
||||
->contains('</select>')
|
||||
->contains('class="classme"')
|
||||
->contains('id="testID"')
|
||||
->contains('name="testID"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled')
|
||||
->contains('data-test="This Is A Test"');
|
||||
|
||||
$this
|
||||
->string(\form::combo('testID', [], '', 'classme', 'atabindex', false, 'data-test="This Is A Test"'))
|
||||
->notContains('disabled');
|
||||
|
||||
$this
|
||||
->string(\form::combo('testID', ['one', 'two', 'three'], 'one'))
|
||||
->match('/<option.*?<\/option>/')
|
||||
->match('/<option\svalue="one"\sselected.*?<\/option>/');
|
||||
|
||||
$this
|
||||
->string(\form::combo('testID', [
|
||||
new \formSelectOption('Un', 1),
|
||||
new \formSelectOption('Deux', 2), ]))
|
||||
->match('/<option.*?<\/option>/')
|
||||
->match('/<option\svalue="2">Deux<\/option>/');
|
||||
|
||||
$this
|
||||
->string(\form::combo(['aName', 'anID'], []))
|
||||
->contains('name="aName"')
|
||||
->contains('id="anID"');
|
||||
|
||||
$this
|
||||
->string(\form::combo('testID', ['onetwo' => ['one' => 'one', 'two' => 'two']]))
|
||||
->match('#<optgroup\slabel="onetwo">#')
|
||||
->match('#<option\svalue="one">one<\/option>#')
|
||||
->contains('</optgroup');
|
||||
|
||||
$this
|
||||
->string(\form::combo('testID', [], [
|
||||
'tabindex' => 'atabindex',
|
||||
'disabled' => true,
|
||||
]))
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled');
|
||||
}
|
||||
|
||||
/** Test for <input type="radio"
|
||||
*/
|
||||
public function testRadio()
|
||||
{
|
||||
$this
|
||||
->string(\form::radio('testID', 'testvalue', true, 'aclassname', 'atabindex', true, 'data-test="A test"'))
|
||||
->contains('type="radio"')
|
||||
->contains('name="testID"')
|
||||
->contains('id="testID"')
|
||||
->contains('checked')
|
||||
->contains('class="aclassname"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled')
|
||||
->contains('data-test="A test"');
|
||||
|
||||
$this
|
||||
->string(\form::radio(['aName', 'testID'], 'testvalue', true, 'aclassname', 'atabindex', false, 'data-test="A test"'))
|
||||
->contains('name="aName"')
|
||||
->contains('id="testID"');
|
||||
|
||||
$this
|
||||
->string(\form::radio('testID', 'testvalue', true, 'aclassname', 'atabindex', false, 'data-test="A test"'))
|
||||
->notContains('disabled');
|
||||
|
||||
$this
|
||||
->string(\form::radio('testID', 'testvalue', [
|
||||
'tabindex' => 'atabindex',
|
||||
'disabled' => true,
|
||||
]))
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled');
|
||||
}
|
||||
|
||||
/** Test for <input type="checkbox"
|
||||
*/
|
||||
public function testCheckbox()
|
||||
{
|
||||
$this
|
||||
->string(\form::checkbox('testID', 'testvalue', true, 'aclassname', 'atabindex', true, 'data-test="A test"'))
|
||||
->contains('type="checkbox"')
|
||||
->contains('name="testID"')
|
||||
->contains('id="testID"')
|
||||
->contains('checked')
|
||||
->contains('class="aclassname"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled')
|
||||
->contains('data-test="A test"');
|
||||
|
||||
$this
|
||||
->string(\form::checkbox(['aName', 'testID'], 'testvalue', true, 'aclassname', 'atabindex', false, 'data-test="A test"'))
|
||||
->contains('name="aName"')
|
||||
->contains('id="testID"');
|
||||
|
||||
$this
|
||||
->string(\form::checkbox('testID', 'testvalue', true, 'aclassname', 'atabindex', false, 'data-test="A test"'))
|
||||
->notContains('disabled');
|
||||
|
||||
$this
|
||||
->string(\form::checkbox('testID', 'testvalue', [
|
||||
'tabindex' => 'atabindex',
|
||||
'disabled' => true,
|
||||
]))
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled');
|
||||
}
|
||||
|
||||
public function testField()
|
||||
{
|
||||
$this
|
||||
->string(\form::field('testID', 10, 20, 'testvalue', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('type="text"')
|
||||
->contains('size="10"')
|
||||
->contains('maxlength="20"')
|
||||
->contains('name="testID"')
|
||||
->contains('id="testID"')
|
||||
->contains('class="aclassname"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled')
|
||||
->contains('data-test="A test"')
|
||||
->contains('value="testvalue"')
|
||||
->contains('required');
|
||||
|
||||
$this
|
||||
->string(\form::field(['aName', 'testID'], 10, 20, 'testvalue', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('name="aName"')
|
||||
->contains('id="testID"');
|
||||
|
||||
$this
|
||||
->string(\form::field('testID', 10, 20, 'testvalue', 'aclassname', 'atabindex', false, 'data-test="A test"', true))
|
||||
->notContains('disabled');
|
||||
|
||||
$this
|
||||
->string(\form::field('testID', 10, 20, [
|
||||
'tabindex' => 'atabindex',
|
||||
'disabled' => true,
|
||||
]))
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled');
|
||||
}
|
||||
|
||||
public function testPassword()
|
||||
{
|
||||
$this
|
||||
->string(\form::password('testID', 10, 20, 'testvalue', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('type="password"')
|
||||
->contains('size="10"')
|
||||
->contains('maxlength="20"')
|
||||
->contains('name="testID"')
|
||||
->contains('id="testID"')
|
||||
->contains('class="aclassname"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled')
|
||||
->contains('data-test="A test"')
|
||||
->contains('value="testvalue"')
|
||||
->contains('required');
|
||||
|
||||
$this
|
||||
->string(\form::password(['aName', 'testID'], 10, 20, 'testvalue', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('name="aName"')
|
||||
->contains('id="testID"');
|
||||
|
||||
$this
|
||||
->string(\form::password('testID', 10, 20, 'testvalue', 'aclassname', 'atabindex', false, 'data-test="A test"', true))
|
||||
->notContains('disabled');
|
||||
|
||||
$this
|
||||
->string(\form::password('testID', 10, 20, [
|
||||
'tabindex' => 'atabindex',
|
||||
'disabled' => true,
|
||||
]))
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a color input field
|
||||
*/
|
||||
public function testColor()
|
||||
{
|
||||
$this
|
||||
->string(\form::color('testID', 10, 20, '#f369a3', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('type="color"')
|
||||
->contains('size="10"')
|
||||
->contains('maxlength="20"')
|
||||
->contains('name="testID"')
|
||||
->contains('id="testID"')
|
||||
->contains('class="aclassname"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled')
|
||||
->contains('data-test="A test"')
|
||||
->contains('value="#f369a3"')
|
||||
->contains('required');
|
||||
|
||||
$this
|
||||
->string(\form::color(['aName', 'testID'], 10, 20, '#f369a3', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('name="aName"')
|
||||
->contains('id="testID"');
|
||||
|
||||
$this
|
||||
->string(\form::color('testID', 10, 20, '#f369a3', 'aclassname', 'atabindex', false, 'data-test="A test"', true))
|
||||
->notContains('disabled');
|
||||
|
||||
$this
|
||||
->string(\form::color('testID', [
|
||||
'tabindex' => 'atabindex',
|
||||
'disabled' => true,
|
||||
]))
|
||||
->contains('size="7"')
|
||||
->contains('maxlength="7"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an email input field
|
||||
*/
|
||||
public function testEmail()
|
||||
{
|
||||
$this
|
||||
->string(\form::email('testID', 10, 20, 'me@example.com', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('type="email"')
|
||||
->contains('size="10"')
|
||||
->contains('maxlength="20"')
|
||||
->contains('name="testID"')
|
||||
->contains('id="testID"')
|
||||
->contains('class="aclassname"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled')
|
||||
->contains('data-test="A test"')
|
||||
->contains('value="me@example.com"')
|
||||
->contains('required');
|
||||
|
||||
$this
|
||||
->string(\form::email(['aName', 'testID'], 10, 20, 'me@example.com', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('name="aName"')
|
||||
->contains('id="testID"');
|
||||
|
||||
$this
|
||||
->string(\form::email('testID', 10, 20, 'me@example.com', 'aclassname', 'atabindex', false, 'data-test="A test"', true))
|
||||
->notContains('disabled');
|
||||
|
||||
$this
|
||||
->string(\form::email('testID', [
|
||||
'tabindex' => 'atabindex',
|
||||
'disabled' => true,
|
||||
]))
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an URL input field
|
||||
*/
|
||||
public function testUrl()
|
||||
{
|
||||
$this
|
||||
->string(\form::url('testID', 10, 20, 'https://example.com/', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('type="url"')
|
||||
->contains('size="10"')
|
||||
->contains('maxlength="20"')
|
||||
->contains('name="testID"')
|
||||
->contains('id="testID"')
|
||||
->contains('class="aclassname"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled')
|
||||
->contains('data-test="A test"')
|
||||
->contains('value="https://example.com/"')
|
||||
->contains('required');
|
||||
|
||||
$this
|
||||
->string(\form::url(['aName', 'testID'], 10, 20, 'https://example.com/', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('name="aName"')
|
||||
->contains('id="testID"');
|
||||
|
||||
$this
|
||||
->string(\form::url('testID', 10, 20, 'https://example.com/', 'aclassname', 'atabindex', false, 'data-test="A test"', true))
|
||||
->notContains('disabled');
|
||||
|
||||
$this
|
||||
->string(\form::url('testID', [
|
||||
'tabindex' => 'atabindex',
|
||||
'disabled' => true,
|
||||
]))
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a datetime (local) input field
|
||||
*/
|
||||
public function testDatetime()
|
||||
{
|
||||
$this
|
||||
->string(\form::datetime('testID', 10, 20, '1962-05-13T02:15', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('type="datetime-local"')
|
||||
->contains('size="10"')
|
||||
->contains('maxlength="20"')
|
||||
->contains('name="testID"')
|
||||
->contains('id="testID"')
|
||||
->contains('class="aclassname"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled')
|
||||
->contains('data-test="A test"')
|
||||
->contains('value="1962-05-13T02:15"')
|
||||
->contains('required')
|
||||
->contains('pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}')
|
||||
->contains('placeholder="1962-05-13T14:45"');
|
||||
|
||||
$this
|
||||
->string(\form::datetime(['aName', 'testID'], 10, 20, '1962-05-13T02:15', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('name="aName"')
|
||||
->contains('id="testID"');
|
||||
|
||||
$this
|
||||
->string(\form::datetime('testID', 10, 20, '1962-05-13T02:15', 'aclassname', 'atabindex', false, 'data-test="A test"', true))
|
||||
->notContains('disabled');
|
||||
|
||||
$this
|
||||
->string(\form::datetime('testID', [
|
||||
'tabindex' => 'atabindex',
|
||||
'disabled' => true,
|
||||
]))
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a date input field
|
||||
*/
|
||||
public function testDate()
|
||||
{
|
||||
$this
|
||||
->string(\form::date('testID', 10, 20, '1962-05-13', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('type="date"')
|
||||
->contains('size="10"')
|
||||
->contains('maxlength="20"')
|
||||
->contains('name="testID"')
|
||||
->contains('id="testID"')
|
||||
->contains('class="aclassname"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled')
|
||||
->contains('data-test="A test"')
|
||||
->contains('value="1962-05-13"')
|
||||
->contains('required')
|
||||
->contains('pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}')
|
||||
->contains('placeholder="1962-05-13"');
|
||||
|
||||
$this
|
||||
->string(\form::date(['aName', 'testID'], 10, 20, '1962-05-13', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('name="aName"')
|
||||
->contains('id="testID"');
|
||||
|
||||
$this
|
||||
->string(\form::date('testID', 10, 20, '1962-05-13', 'aclassname', 'atabindex', false, 'data-test="A test"', true))
|
||||
->notContains('disabled');
|
||||
|
||||
$this
|
||||
->string(\form::date('testID', [
|
||||
'tabindex' => 'atabindex',
|
||||
'disabled' => true,
|
||||
]))
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a datetime (local) input field
|
||||
*/
|
||||
public function testTime()
|
||||
{
|
||||
$this
|
||||
->string(\form::time('testID', 10, 20, '02:15', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('type="time"')
|
||||
->contains('size="10"')
|
||||
->contains('maxlength="20"')
|
||||
->contains('name="testID"')
|
||||
->contains('id="testID"')
|
||||
->contains('class="aclassname"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled')
|
||||
->contains('data-test="A test"')
|
||||
->contains('value="02:15"')
|
||||
->contains('required')
|
||||
->contains('pattern="[0-9]{2}:[0-9]{2}')
|
||||
->contains('placeholder="14:45"');
|
||||
|
||||
$this
|
||||
->string(\form::time(['aName', 'testID'], 10, 20, '02:15', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('name="aName"')
|
||||
->contains('id="testID"');
|
||||
|
||||
$this
|
||||
->string(\form::time('testID', 10, 20, '02:15', 'aclassname', 'atabindex', false, 'data-test="A test"', true))
|
||||
->notContains('disabled');
|
||||
|
||||
$this
|
||||
->string(\form::time('testID', [
|
||||
'tabindex' => 'atabindex',
|
||||
'disabled' => true,
|
||||
]))
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file input field
|
||||
*/
|
||||
public function testFile()
|
||||
{
|
||||
$this
|
||||
->string(\form::file('testID', 'filename.ext', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('type="file"')
|
||||
->contains('name="testID"')
|
||||
->contains('id="testID"')
|
||||
->contains('class="aclassname"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled')
|
||||
->contains('data-test="A test"')
|
||||
->contains('value="filename.ext"')
|
||||
->contains('required');
|
||||
|
||||
$this
|
||||
->string(\form::file(['aName', 'testID'], 'filename.ext', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('name="aName"')
|
||||
->contains('id="testID"');
|
||||
|
||||
$this
|
||||
->string(\form::file('testID', 'filename.ext', 'aclassname', 'atabindex', false, 'data-test="A test"', true))
|
||||
->notContains('disabled');
|
||||
|
||||
$this
|
||||
->string(\form::file('testID', [
|
||||
'tabindex' => 'atabindex',
|
||||
'disabled' => true,
|
||||
]))
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled');
|
||||
}
|
||||
|
||||
public function testNumber()
|
||||
{
|
||||
$this
|
||||
->string(\form::number('testID', 0, 99, 13, 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('type="number"')
|
||||
->contains('min="0"')
|
||||
->contains('max="99"')
|
||||
->contains('name="testID"')
|
||||
->contains('id="testID"')
|
||||
->contains('class="aclassname"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled')
|
||||
->contains('data-test="A test"')
|
||||
->contains('value="13"')
|
||||
->contains('required');
|
||||
|
||||
$this
|
||||
->string(\form::number(['aName', 'testID'], 0, 99, 13, 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('name="aName"')
|
||||
->contains('id="testID"');
|
||||
|
||||
$this
|
||||
->string(\form::number('testID', 0, 99, 13, 'aclassname', 'atabindex', false, 'data-test="A test"', true))
|
||||
->notContains('disabled');
|
||||
|
||||
$this
|
||||
->string(\form::number('testID', [
|
||||
'tabindex' => 'atabindex',
|
||||
'disabled' => true,
|
||||
]))
|
||||
->notContains('min=')
|
||||
->notContains('max=')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled');
|
||||
}
|
||||
|
||||
public function testTextArea()
|
||||
{
|
||||
$this
|
||||
->string(\form::textArea('testID', 10, 20, 'testvalue', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->match('#<textarea.*?testvalue.*?<\/textarea>#s')
|
||||
->contains('cols="10"')
|
||||
->contains('rows="20"')
|
||||
->contains('name="testID"')
|
||||
->contains('id="testID"')
|
||||
->contains('class="aclassname"')
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled')
|
||||
->contains('data-test="A test"')
|
||||
->contains('required');
|
||||
|
||||
$this
|
||||
->string(\form::textArea(['aName', 'testID'], 10, 20, 'testvalue', 'aclassname', 'atabindex', true, 'data-test="A test"', true))
|
||||
->contains('name="aName"')
|
||||
->contains('id="testID"');
|
||||
|
||||
$this
|
||||
->string(\form::textArea('testID', 10, 20, 'testvalue', 'aclassname', 'atabindex', false, 'data-test="A test"', true))
|
||||
->notContains('disabled');
|
||||
|
||||
$this
|
||||
->string(\form::textArea('testID', 10, 20, [
|
||||
'tabindex' => 'atabindex',
|
||||
'disabled' => true,
|
||||
]))
|
||||
->contains('tabindex="0"')
|
||||
->contains('disabled');
|
||||
}
|
||||
|
||||
public function testHidden()
|
||||
{
|
||||
$this
|
||||
->string(\form::hidden('testID', 'testvalue'))
|
||||
->contains('type="hidden"')
|
||||
->contains('name="testID"')
|
||||
->contains('id="testID"')
|
||||
->contains('value="testvalue"');
|
||||
|
||||
$this
|
||||
->string(\form::hidden(['aName', 'testID'], 'testvalue'))
|
||||
->contains('name="aName"')
|
||||
->contains('id="testID"');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
# -- BEGIN LICENSE BLOCK ---------------------------------------
|
||||
#
|
||||
# This file is part of Dotclear 2.
|
||||
#
|
||||
# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
|
||||
# Licensed under the GPL version 2.0 license.
|
||||
# See LICENSE file or
|
||||
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
#
|
||||
# -- END LICENSE BLOCK -----------------------------------------
|
||||
|
||||
namespace tests\unit;
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
require_once CLEARBRICKS_PATH . '/common/lib.html.php';
|
||||
|
||||
use atoum;
|
||||
|
||||
/**
|
||||
* Test the form class
|
||||
*/
|
||||
class html extends atoum
|
||||
{
|
||||
/** Simple test. Don't need to test PHP functions
|
||||
*/
|
||||
public function testEscapeHTML()
|
||||
{
|
||||
$str = '"<>&';
|
||||
$this
|
||||
->string(\html::escapeHTML($str))
|
||||
->isEqualTo('"<>&');
|
||||
$this
|
||||
->string(\html::escapeHTML(null))
|
||||
->isEqualTo('');
|
||||
}
|
||||
|
||||
public function testDecodeEntities()
|
||||
{
|
||||
$this
|
||||
->string(\html::decodeEntities('<body>', true))
|
||||
->isEqualTo('<body>');
|
||||
$this
|
||||
->string(\html::decodeEntities('<body>'))
|
||||
->isEqualTo('<body>');
|
||||
}
|
||||
|
||||
/**
|
||||
* html::clean is a wrapper of a PHP native function
|
||||
* Simple test
|
||||
*/
|
||||
public function testClean()
|
||||
{
|
||||
$this
|
||||
->string(\html::clean('<b>test</b>'))
|
||||
->isEqualTo('test');
|
||||
}
|
||||
|
||||
public function testEscapeJS()
|
||||
{
|
||||
$this
|
||||
->string(\html::escapeJS('<script>alert("Hello world");</script>'))
|
||||
->isEqualTo('<script>alert(\"Hello world\");</script>');
|
||||
}
|
||||
|
||||
/**
|
||||
* html::escapeURL is a wrapper of a PHP native function
|
||||
* Simple test
|
||||
*/
|
||||
public function testEscapeURL()
|
||||
{
|
||||
$this
|
||||
->string(\html::escapeURL('https://www.dotclear.org/?q=test&test=1'))
|
||||
->isEqualTo('https://www.dotclear.org/?q=test&test=1');
|
||||
}
|
||||
|
||||
/**
|
||||
* html::sanitizeURL is a wrapper of a PHP native function
|
||||
* Simple test
|
||||
*/
|
||||
public function testSanitizeURL()
|
||||
{
|
||||
$this
|
||||
->string(\html::sanitizeURL('https://www.dotclear.org/'))
|
||||
->isEqualTo('https%3A//www.dotclear.org/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test removing host prefix
|
||||
*/
|
||||
public function testStripHostURL()
|
||||
{
|
||||
$this
|
||||
->string(\html::stripHostURL('https://www.dotclear.org/best-blog-engine/'))
|
||||
->isEqualTo('/best-blog-engine/');
|
||||
|
||||
$this
|
||||
->string(\html::stripHostURL('dummy:/not-well-formed-url.d'))
|
||||
->isEqualTo('dummy:/not-well-formed-url.d');
|
||||
}
|
||||
|
||||
public function testAbsoluteURLs()
|
||||
{
|
||||
\html::$absolute_regs[] = '/(<param\s+name="movie"\s+value=")(.*?)(")/msu';
|
||||
|
||||
$this
|
||||
->string(\html::absoluteURLs('<a href="/best-blog-engine-ever/">Clickme</a>', 'https://dotclear.org/'))
|
||||
->isEqualTo('<a href="https://dotclear.org/best-blog-engine-ever/">Clickme</a>');
|
||||
|
||||
$this
|
||||
->string(\html::absoluteURLs('<a href="best-blog-engine-ever/">Clickme</a>', 'https://dotclear.org/'))
|
||||
->isEqualTo('<a href="https://dotclear.org/best-blog-engine-ever/">Clickme</a>');
|
||||
|
||||
$this
|
||||
->string(\html::absoluteURLs('<a href="#anchor">Clickme</a>', 'https://dotclear.org/'))
|
||||
->isEqualTo('<a href="https://dotclear.org/#anchor">Clickme</a>');
|
||||
|
||||
$this
|
||||
->string(\html::absoluteURLs('<a href="index.php">Clickme</a>', '/'))
|
||||
->isEqualTo('<a href="/index.php">Clickme</a>');
|
||||
|
||||
$this
|
||||
->string(\html::absoluteURLs('<a href="lib">Clickme</a>', '/var/tmp'))
|
||||
->isEqualTo('<a href="/var/lib">Clickme</a>');
|
||||
|
||||
$this
|
||||
->string(\html::absoluteURLs('<param name="movie" value="my-movie.flv" />', 'https://dotclear.org/'))
|
||||
->isEqualTo('<param name="movie" value="https://dotclear.org/my-movie.flv" />');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,322 @@
|
|||
<?php
|
||||
|
||||
# -- BEGIN LICENSE BLOCK ---------------------------------------
|
||||
#
|
||||
# This file is part of Dotclear 2.
|
||||
#
|
||||
# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
|
||||
# Licensed under the GPL version 2.0 license.
|
||||
# See LICENSE file or
|
||||
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
#
|
||||
# -- END LICENSE BLOCK -----------------------------------------
|
||||
|
||||
namespace tests\unit;
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
require_once CLEARBRICKS_PATH . '/common/lib.http.php';
|
||||
require_once CLEARBRICKS_PATH . '/common/lib.crypt.php';
|
||||
require_once CLEARBRICKS_PATH . '/common/lib.files.php';
|
||||
|
||||
if (!defined('TEST_DIRECTORY')) {
|
||||
define('TEST_DIRECTORY', realpath(
|
||||
__DIR__ . '/../fixtures/files'
|
||||
));
|
||||
}
|
||||
|
||||
use atoum;
|
||||
|
||||
/**
|
||||
* Test the form class
|
||||
*/
|
||||
class http extends atoum
|
||||
{
|
||||
/** Test getHost
|
||||
* In CLI mode superglobal variable $_SERVER is not set correctly
|
||||
*/
|
||||
public function testGetHost()
|
||||
{
|
||||
// Normal
|
||||
$_SERVER['HTTP_HOST'] = 'localhost';
|
||||
$_SERVER['SERVER_PORT'] = 80;
|
||||
$this
|
||||
->string(\http::getHost())
|
||||
->isEqualTo('http://localhost');
|
||||
|
||||
// On a different port
|
||||
$_SERVER['SERVER_PORT'] = 8080;
|
||||
$this
|
||||
->string(\http::getHost())
|
||||
->isEqualTo('http://localhost:8080');
|
||||
|
||||
// On secure port without enforcing TLS
|
||||
$_SERVER['SERVER_PORT'] = 443;
|
||||
$this
|
||||
->string(\http::getHost())
|
||||
->isEqualTo('http://localhost:443');
|
||||
|
||||
// On secure via $_SERVER
|
||||
$_SERVER['HTTPS'] = 'on';
|
||||
$this
|
||||
->string(\http::getHost())
|
||||
->isEqualTo('https://localhost');
|
||||
|
||||
// On sercure port with enforcing TLS
|
||||
$_SERVER['SERVER_PORT'] = 443;
|
||||
\http::$https_scheme_on_443 = true;
|
||||
$this
|
||||
->string(\http::getHost())
|
||||
->isEqualTo('https://localhost');
|
||||
}
|
||||
|
||||
public function testGetHostFromURL()
|
||||
{
|
||||
$this
|
||||
->string(\http::getHostFromURL('https://www.dotclear.org/is-good-for-you/'))
|
||||
->isEqualTo('https://www.dotclear.org');
|
||||
|
||||
// Note: An empty string might be confuse
|
||||
$this
|
||||
->string(\http::getHostFromURL('http:/www.dotclear.org/is-good-for-you/'))
|
||||
->isEqualTo('');
|
||||
}
|
||||
|
||||
public function testGetSelfURI()
|
||||
{
|
||||
$_SERVER['HTTP_HOST'] = 'localhost';
|
||||
$_SERVER['SERVER_PORT'] = 80;
|
||||
$_SERVER['REQUEST_URI'] = '/test.html';
|
||||
$this
|
||||
->string(\http::getSelfURI())
|
||||
->isEqualTo('http://localhost/test.html');
|
||||
|
||||
// It's usually unlikly, but unlikly is not impossible.
|
||||
$_SERVER['REQUEST_URI'] = 'test.html';
|
||||
$this
|
||||
->string(\http::getSelfURI())
|
||||
->isEqualTo('http://localhost/test.html');
|
||||
}
|
||||
|
||||
public function testPrepareRedirect()
|
||||
{
|
||||
$_SERVER['HTTP_HOST'] = 'localhost';
|
||||
$_SERVER['SERVER_PORT'] = 80;
|
||||
$_SERVER['REQUEST_URI'] = '/test.html';
|
||||
|
||||
$prepareRedirect = new \ReflectionMethod('\http', 'prepareRedirect');
|
||||
$prepareRedirect->setAccessible(true);
|
||||
$this
|
||||
->string($prepareRedirect->invokeArgs(null, ['http://www.dotclear.org/auth.html']))
|
||||
->isEqualTo('http://www.dotclear.org/auth.html');
|
||||
|
||||
$this
|
||||
->string($prepareRedirect->invokeArgs(null, ['https://www.dotclear.org/auth.html']))
|
||||
->isEqualTo('https://www.dotclear.org/auth.html');
|
||||
|
||||
$this
|
||||
->string($prepareRedirect->invokeArgs(null, ['auth.html']))
|
||||
->isEqualTo('http://localhost/auth.html');
|
||||
|
||||
$this
|
||||
->string($prepareRedirect->invokeArgs(null, ['/admin/auth.html']))
|
||||
->isEqualTo('http://localhost/admin/auth.html');
|
||||
|
||||
$_SERVER['PHP_SELF'] = '/test.php';
|
||||
$this
|
||||
->string($prepareRedirect->invokeArgs(null, ['auth.html']))
|
||||
->isEqualTo('http://localhost/auth.html');
|
||||
}
|
||||
|
||||
public function testConcatURL()
|
||||
{
|
||||
$this
|
||||
->string(\http::concatURL('http://localhost', 'index.html'))
|
||||
->isEqualTo('http://localhost/index.html');
|
||||
|
||||
$this
|
||||
->string(\http::concatURL('http://localhost', 'page/index.html'))
|
||||
->isEqualTo('http://localhost/page/index.html');
|
||||
|
||||
$this
|
||||
->string(\http::concatURL('http://localhost', '/page/index.html'))
|
||||
->isEqualTo('http://localhost/page/index.html');
|
||||
|
||||
$this
|
||||
->string(\http::concatURL('http://localhost/', 'index.html'))
|
||||
->isEqualTo('http://localhost/index.html');
|
||||
|
||||
$this
|
||||
->string(\http::concatURL('http://localhost/', 'page/index.html'))
|
||||
->isEqualTo('http://localhost/page/index.html');
|
||||
|
||||
$this
|
||||
->string(\http::concatURL('http://localhost/', '/page/index.html'))
|
||||
->isEqualTo('http://localhost/page/index.html');
|
||||
|
||||
$this
|
||||
->string(\http::concatURL('http://localhost/admin', 'index.html'))
|
||||
->isEqualTo('http://localhost/admin/index.html');
|
||||
|
||||
$this
|
||||
->string(\http::concatURL('http://localhost/admin', 'page/index.html'))
|
||||
->isEqualTo('http://localhost/admin/page/index.html');
|
||||
|
||||
$this
|
||||
->string(\http::concatURL('http://localhost/admin', '/page/index.html'))
|
||||
->isEqualTo('http://localhost/page/index.html');
|
||||
|
||||
$this
|
||||
->string(\http::concatURL('http://localhost/admin/', 'index.html'))
|
||||
->isEqualTo('http://localhost/admin/index.html');
|
||||
|
||||
$this
|
||||
->string(\http::concatURL('http://localhost/admin/', 'page/index.html'))
|
||||
->isEqualTo('http://localhost/admin/page/index.html');
|
||||
|
||||
$this
|
||||
->string(\http::concatURL('http://localhost/admin/', '/page/index.html'))
|
||||
->isEqualTo('http://localhost/page/index.html');
|
||||
}
|
||||
|
||||
public function testRealIP()
|
||||
{
|
||||
$this
|
||||
->variable(\http::realIP())
|
||||
->isNull();
|
||||
|
||||
$_SERVER['REMOTE_ADDR'] = '192.168.0.42';
|
||||
$this
|
||||
->string(\http::realIP())
|
||||
->isEqualTo('192.168.0.42');
|
||||
}
|
||||
|
||||
public function testBrowserUID()
|
||||
{
|
||||
unset($_SERVER['HTTP_USER_AGENT'], $_SERVER['HTTP_ACCEPT_CHARSET']);
|
||||
|
||||
$this
|
||||
->string(\http::browserUID('dotclear'))
|
||||
->isEqualTo('d82ae3c43cf5af4d0a8a8bc1f691ee5cc89332fd');
|
||||
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'Dotclear';
|
||||
$this
|
||||
->string(\http::browserUID('dotclear'))
|
||||
->isEqualTo('ef1c4702c3b684637a95d482e39536a943fef7a1');
|
||||
|
||||
$_SERVER['HTTP_ACCEPT_CHARSET'] = 'ISO-8859-1,utf-8;q=0.7,*;q=0.3';
|
||||
$this
|
||||
->string(\http::browserUID('dotclear'))
|
||||
->isEqualTo('ce3880093944405b1c217b4e2fba05e93ccc07e4');
|
||||
|
||||
unset($_SERVER['HTTP_USER_AGENT']);
|
||||
$this
|
||||
->string(\http::browserUID('dotclear'))
|
||||
->isEqualTo('c1bb85ca96d62726648053f97922eee5ceda78e9');
|
||||
}
|
||||
|
||||
public function testGetAcceptLanguage()
|
||||
{
|
||||
unset($_SERVER['HTTP_ACCEPT_LANGUAGE']);
|
||||
$this
|
||||
->string(\http::getAcceptLanguage())
|
||||
->isEqualTo('');
|
||||
|
||||
$_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7';
|
||||
$this
|
||||
->string(\http::getAcceptLanguage())
|
||||
->isEqualTo('fr');
|
||||
}
|
||||
|
||||
public function testGetAcceptLanguages()
|
||||
{
|
||||
unset($_SERVER['HTTP_ACCEPT_LANGUAGE']);
|
||||
$this
|
||||
->array(\http::getAcceptLanguages())
|
||||
->isEmpty();
|
||||
|
||||
$_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7';
|
||||
$this
|
||||
->array(\http::getAcceptLanguages())
|
||||
->string[0]->isEqualTo('fr-fr')
|
||||
->string[1]->isEqualTo('fr')
|
||||
->string[2]->isEqualTo('en-us')
|
||||
->string[3]->isEqualTo('en');
|
||||
}
|
||||
|
||||
public function testCache()
|
||||
{
|
||||
$this
|
||||
->variable(\http::cache([]))
|
||||
->isNull();
|
||||
|
||||
\files::getDirList(TEST_DIRECTORY, $arr);
|
||||
$fl = [];
|
||||
foreach ($arr['files'] as $file) {
|
||||
if ($file != '.' && $file != '..') {
|
||||
$fl[] = $file;
|
||||
}
|
||||
}
|
||||
$_SERVER['HTTP_IF_MODIFIED_SINCE'] = 'Tue, 27 Feb 2004 10:17:09 GMT';
|
||||
$this
|
||||
->variable(\http::cache($fl))
|
||||
->isNull();
|
||||
}
|
||||
|
||||
public function testEtag()
|
||||
{
|
||||
$_SERVER['HTTP_IF_NONE_MATCH'] = 'W/"67ab43", "54ed21", "7892dd"';
|
||||
$this
|
||||
->variable(\http::etag())
|
||||
->isNull();
|
||||
|
||||
$this
|
||||
->variable(\http::etag('bfc13a64729c4290ef5b2c2730249c88ca92d82d'))
|
||||
->isNull();
|
||||
}
|
||||
|
||||
public function testHead()
|
||||
{
|
||||
$this
|
||||
->variable(\http::head(200))
|
||||
->isNull();
|
||||
|
||||
$this
|
||||
->variable(\http::head(200, '\\o/'))
|
||||
->isNull();
|
||||
}
|
||||
|
||||
public function testTrimRequest()
|
||||
{
|
||||
$_GET['single'] = 'single';
|
||||
$_GET['trim_single'] = ' trim_single ';
|
||||
$_GET['multiple'] = ['one ', 'two', ' three', ' four ', [' five ']];
|
||||
|
||||
$_POST['post'] = ' test ';
|
||||
$_REQUEST['request'] = ' test\\\'n\\\'test ';
|
||||
$_COOKIE['cookie'] = ' test ';
|
||||
|
||||
\http::trimRequest();
|
||||
$this
|
||||
->array($_GET)
|
||||
->string['single']->isEqualTo('single')
|
||||
->string['trim_single']->isEqualTo('trim_single')
|
||||
->array['multiple']
|
||||
->string[0]->isEqualTo('one')
|
||||
->array['multiple']
|
||||
->string[1]->isEqualTo('two')
|
||||
->array['multiple']
|
||||
->string[2]->isEqualTo('three')
|
||||
->array['multiple']
|
||||
->string[3]->isEqualTo('four')
|
||||
->array['multiple']
|
||||
->array[4]
|
||||
->string[0]->isEqualTo('five')
|
||||
->array($_POST)
|
||||
->string['post']->isEqualTo('test')
|
||||
->array($_REQUEST)
|
||||
->string['request']->isEqualTo('test\\\'n\\\'test')
|
||||
->array($_COOKIE)
|
||||
->string['cookie']->isEqualTo('test');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,432 @@
|
|||
<?php
|
||||
|
||||
# ***** BEGIN LICENSE BLOCK *****
|
||||
# This file is part of Clearbricks.
|
||||
# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
|
||||
# All rights reserved.
|
||||
#
|
||||
# Clearbricks is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Clearbricks is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Clearbricks; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# ***** END LICENSE BLOCK *****
|
||||
|
||||
namespace tests\unit;
|
||||
|
||||
use atoum;
|
||||
use Faker;
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
require_once CLEARBRICKS_PATH . '/common/lib.l10n.php';
|
||||
|
||||
class l10n extends atoum
|
||||
{
|
||||
private $l10n_dir = '/../fixtures/l10n';
|
||||
|
||||
public function testWithEmpty()
|
||||
{
|
||||
$this
|
||||
->string(__(''))
|
||||
->isEqualTo('');
|
||||
}
|
||||
|
||||
public function testWithoutTranslation()
|
||||
{
|
||||
$faker = Faker\Factory::create();
|
||||
$text = $faker->text(50);
|
||||
|
||||
$this
|
||||
->string(__($text))
|
||||
->isEqualTo($text);
|
||||
}
|
||||
|
||||
public function testSimpleSingular()
|
||||
{
|
||||
\l10n::init();
|
||||
\l10n::set(__DIR__ . '/../fixtures/l10n/fr/core');
|
||||
|
||||
$this
|
||||
->string(__('Dotclear has been upgraded.'))
|
||||
->isEqualTo('Dotclear a été mis à jour.');
|
||||
}
|
||||
|
||||
public function testZeroForCountEn()
|
||||
{
|
||||
\l10n::init();
|
||||
|
||||
$this
|
||||
->string(__('singular', 'plural', 0))
|
||||
->isEqualTo('plural');
|
||||
}
|
||||
|
||||
public function testZeroForCountFr()
|
||||
{
|
||||
\l10n::init();
|
||||
\l10n::set(__DIR__ . '/../fixtures/l10n/fr/main');
|
||||
|
||||
$this
|
||||
->string(__('The category has been successfully removed.', 'The categories have been successfully removed.', 0))
|
||||
->isEqualTo('Catégories supprimées avec succès.');
|
||||
|
||||
$this
|
||||
->string(__('Time: %1 second', 'Time: %1 seconds and Next', 2))
|
||||
->isEqualTo('Temps: %1 secondes');
|
||||
}
|
||||
|
||||
public function testZeroForCountFrUsingLang()
|
||||
{
|
||||
\l10n::init();
|
||||
\l10n::set(__DIR__ . '/../fixtures/l10n/fr/main');
|
||||
\l10n::lang('fr');
|
||||
|
||||
$this
|
||||
->string(__('The category has been successfully removed.', 'The categories have been successfully removed.', 0))
|
||||
->isEqualTo('Catégorie supprimée avec succès.');
|
||||
}
|
||||
|
||||
public function testPluralWithSingularOnly()
|
||||
{
|
||||
\l10n::init();
|
||||
\l10n::set(__DIR__ . '/../fixtures/l10n/fr/main');
|
||||
|
||||
$this
|
||||
->string(__('Dotclear has been upgraded.', 'Dotclear has been upgraded (plural).', 0))
|
||||
->isEqualTo('Dotclear a été mis à jour (pluriel).');
|
||||
}
|
||||
|
||||
public function testCodeLang()
|
||||
{
|
||||
\l10n::init();
|
||||
|
||||
$this
|
||||
->boolean(\l10n::isCode('xx'))
|
||||
->isEqualTo(false);
|
||||
|
||||
$this
|
||||
->boolean(\l10n::isCode('fr'))
|
||||
->isEqualTo(true);
|
||||
}
|
||||
|
||||
public function testChangeNonExistingLangShouldUseDefaultOne()
|
||||
{
|
||||
\l10n::init('en');
|
||||
|
||||
$this
|
||||
->string(\l10n::lang('xx'))
|
||||
->isEqualTo('en');
|
||||
}
|
||||
|
||||
public function testgetLanguageName()
|
||||
{
|
||||
\l10n::init();
|
||||
|
||||
$this
|
||||
->string(\l10n::getLanguageName('fr'))
|
||||
->isEqualTo('Français');
|
||||
}
|
||||
|
||||
public function testgetCode()
|
||||
{
|
||||
\l10n::init();
|
||||
|
||||
$this
|
||||
->string(\l10n::getCode('Français'))
|
||||
->isEqualTo('fr');
|
||||
|
||||
$this
|
||||
->string(\l10n::getCode(\l10n::getLanguageName('es')))
|
||||
->isEqualTo('es');
|
||||
}
|
||||
|
||||
public function testPhpFormatSingular()
|
||||
{
|
||||
$faker = Faker\Factory::create();
|
||||
$text = $faker->text(20);
|
||||
|
||||
\l10n::init();
|
||||
\l10n::set(__DIR__ . '/../fixtures/l10n/fr/php-format');
|
||||
|
||||
$this
|
||||
->string(sprintf(__('The e-mail was sent successfully to %s.'), $text))
|
||||
->isEqualTo(sprintf('Message envoyé avec succès à %s.', $text));
|
||||
}
|
||||
|
||||
public function testPluralWithoutTranslation()
|
||||
{
|
||||
\l10n::init();
|
||||
\l10n::set(__DIR__ . '/../fixtures/l10n/dummy');
|
||||
|
||||
$this
|
||||
->string(__('The category has been successfully removed.', 'The categories have been successfully removed.', 1))
|
||||
->isEqualTo('The category has been successfully removed.');
|
||||
|
||||
$this
|
||||
->string(__('The category has been successfully removed.', 'The categories have been successfully removed.', 2))
|
||||
->isEqualTo('The categories have been successfully removed.');
|
||||
}
|
||||
|
||||
public function testPluralWithEmptyTranslation()
|
||||
{
|
||||
\l10n::init();
|
||||
\l10n::set(__DIR__ . '/../fixtures/l10n/empty');
|
||||
|
||||
$this
|
||||
->string(__('The category has been successfully removed.', 'The categories have been successfully removed.', 1))
|
||||
->isEqualTo('The category has been successfully removed.');
|
||||
|
||||
$this
|
||||
->string(__('The category has been successfully removed.', 'The categories have been successfully removed.', 2))
|
||||
->isEqualTo('The categories have been successfully removed.');
|
||||
}
|
||||
|
||||
public function testPluralForLanguageWithoutPluralForms()
|
||||
{
|
||||
\l10n::init();
|
||||
|
||||
$this
|
||||
->integer(\l10n::getLanguagePluralsNumber('aa'))
|
||||
->isEqualTo(\l10n::getLanguagePluralsNumber('en'));
|
||||
|
||||
$this
|
||||
->string(\l10n::getLanguagePluralExpression('aa'))
|
||||
->isEqualTo(\l10n::getLanguagePluralExpression('en'));
|
||||
}
|
||||
|
||||
public function testSimplePlural()
|
||||
{
|
||||
\l10n::init();
|
||||
\l10n::set(__DIR__ . '/../fixtures/l10n/fr/main');
|
||||
|
||||
/*
|
||||
msgid "The category has been successfully removed."
|
||||
msgid_plural "The categories have been successfully removed."
|
||||
msgstr[0] "Catégorie supprimée avec succès."
|
||||
msgstr[1] "Catégories supprimées avec succès."
|
||||
*/
|
||||
|
||||
$this
|
||||
->string(__('The category has been successfully removed.', 'The categories have been successfully removed.', 1))
|
||||
->isEqualTo('Catégorie supprimée avec succès.');
|
||||
|
||||
$this
|
||||
->string(__('The category has been successfully removed.', 'The categories have been successfully removed.', 2))
|
||||
->isEqualTo('Catégories supprimées avec succès.');
|
||||
}
|
||||
|
||||
public function testNotExistingPhpAndPoFiles()
|
||||
{
|
||||
\l10n::init();
|
||||
\l10n::set(__DIR__ . '/../fixtures/l10n/dummy');
|
||||
|
||||
$this
|
||||
->string(__('Dotclear has been upgraded.'))
|
||||
->isEqualTo('Dotclear has been upgraded.');
|
||||
}
|
||||
|
||||
public function testNotExistingPoFile()
|
||||
{
|
||||
\l10n::init();
|
||||
\l10n::set(__DIR__ . '/../fixtures/l10n/fr/nopo');
|
||||
|
||||
$this
|
||||
->string(__('Dotclear has been upgraded.'))
|
||||
->isEqualTo('Dotclear a été mis à jour.');
|
||||
}
|
||||
|
||||
public function testGetFilePath()
|
||||
{
|
||||
\l10n::init();
|
||||
|
||||
$this
|
||||
->string(\l10n::getFilePath(__DIR__ . $this->l10n_dir, 'main.po', 'fr'))
|
||||
->isEqualTo(__DIR__ . $this->l10n_dir . '/fr/main.po');
|
||||
|
||||
$this
|
||||
->boolean(\l10n::getFilePath(__DIR__ . $this->l10n_dir, 'dummy.po', 'fr'))
|
||||
->isEqualTo(false);
|
||||
}
|
||||
|
||||
public function testMultiLineIdString()
|
||||
{
|
||||
\l10n::init();
|
||||
|
||||
$en_str = 'Not a real long sentence';
|
||||
$content = 'msgid ""' . "\n" . '"';
|
||||
$content .= implode('"' . "\n" . '" ', explode(' ', $en_str));
|
||||
$content .= '"' . "\n";
|
||||
$content .= 'msgstr "Pas vraiment une très longue phrase"' . "\n";
|
||||
|
||||
$tmp_file = $this->tempPoFile($content);
|
||||
\l10n::set(str_replace('.po', '', $tmp_file));
|
||||
|
||||
$this
|
||||
->string(__($en_str))
|
||||
->isEqualTo('Pas vraiment une très longue phrase');
|
||||
|
||||
if (file_exists($tmp_file)) {
|
||||
unlink($tmp_file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testMultiLineValueString()
|
||||
{
|
||||
\l10n::init();
|
||||
|
||||
$en_str = 'Not a real long sentence';
|
||||
$fr_str = 'Pas vraiment une très longue phrase';
|
||||
$content = 'msgid "' . $en_str . '"' . "\n";
|
||||
$content .= 'msgstr ""' . "\n" . '"';
|
||||
$content .= implode('"' . "\n" . '" ', explode(' ', $fr_str));
|
||||
$content .= '"' . "\n";
|
||||
|
||||
$tmp_file = $this->tempPoFile($content);
|
||||
\l10n::set(str_replace('.po', '', $tmp_file));
|
||||
|
||||
$this
|
||||
->string(__($en_str))
|
||||
->isEqualTo($fr_str);
|
||||
|
||||
if (file_exists($tmp_file)) {
|
||||
unlink($tmp_file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testSimpleStringInPhpFile()
|
||||
{
|
||||
\l10n::init();
|
||||
|
||||
$file = __DIR__ . '/../fixtures/l10n/fr/simple';
|
||||
if (file_exists("$file.lang.php")) {
|
||||
unlink("$file.lang.php");
|
||||
}
|
||||
\l10n::generatePhpFileFromPo($file);
|
||||
\l10n::set($file);
|
||||
|
||||
$this
|
||||
->array($GLOBALS['__l10n'])
|
||||
->isIdenticalTo(['Dotclear has been upgraded.' => 'Dotclear a été mis à jour.']);
|
||||
}
|
||||
|
||||
public function testPluralStringsInPhpFile()
|
||||
{
|
||||
\l10n::init();
|
||||
|
||||
$file = __DIR__ . '/../fixtures/l10n/fr/plurals';
|
||||
if (file_exists("$file.lang.php")) {
|
||||
unlink("$file.lang.php");
|
||||
}
|
||||
\l10n::generatePhpFileFromPo($file);
|
||||
\l10n::set($file);
|
||||
|
||||
$this
|
||||
->array($GLOBALS['__l10n'])
|
||||
->isIdenticalTo(['The category has been successfully removed.' => ['Catégorie supprimée avec succès.', 'Catégories supprimées avec succès.']]);
|
||||
}
|
||||
|
||||
public function testParsePluralExpression()
|
||||
{
|
||||
$this
|
||||
->array(\l10n::parsePluralExpression('nplurals=2; plural=(n > 1)'))
|
||||
->hasSize(2)
|
||||
->containsValues([2, '(n > 1)']);
|
||||
|
||||
$this
|
||||
->array(\l10n::parsePluralExpression('nplurals=6; plural=(n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5)'))
|
||||
->hasSize(2)
|
||||
->containsValues([6, '(n == 0 ? ( 0 ) : ( n == 1 ? ( 1 ) : ( n == 2 ? ( 2 ) : ( n % 100 >= 3 && n % 100 <= 10 ? ( 3 ) : ( n % 100 >= 11 ? ( 4 ) : ( 5))))))']);
|
||||
}
|
||||
|
||||
public function testGetISOcodes()
|
||||
{
|
||||
$this
|
||||
->array(\l10n::getISOcodes())
|
||||
->string['fr']->isEqualTo('Français');
|
||||
|
||||
$this
|
||||
->array(\l10n::getISOcodes(true))
|
||||
->string['Français']->isEqualTo('fr');
|
||||
|
||||
$this
|
||||
->array(\l10n::getISOcodes(false, true))
|
||||
->string['fr']->isEqualTo('fr - Français');
|
||||
|
||||
$this
|
||||
->array(\l10n::getISOcodes(true, true))
|
||||
->string['fr - Français']->isEqualTo('fr');
|
||||
}
|
||||
|
||||
public function testGetTextDirection()
|
||||
{
|
||||
$this
|
||||
->string(\l10n::getLanguageTextDirection('fr'))
|
||||
->isEqualTo('ltr');
|
||||
|
||||
$this
|
||||
->string(\l10n::getLanguageTextDirection('ar'))
|
||||
->isEqualTo('rtl');
|
||||
}
|
||||
|
||||
public function testGetLanguagesDefinitions()
|
||||
{
|
||||
$getLangDefs = new \ReflectionMethod('\l10n', 'getLanguagesDefinitions');
|
||||
$getLangDefs->setAccessible(true);
|
||||
|
||||
$this
|
||||
->array($getLangDefs->invokeArgs(null, [0]))
|
||||
->isNotEmpty();
|
||||
|
||||
$this
|
||||
->array($getLangDefs->invokeArgs(null, [13]))
|
||||
->isEmpty();
|
||||
|
||||
$this
|
||||
->array($getLangDefs->invokeArgs(null, [0]))
|
||||
->string['fr']->isEqualTo('fr');
|
||||
|
||||
$this
|
||||
->array($getLangDefs->invokeArgs(null, [1]))
|
||||
->string['fr']->isEqualTo('fre');
|
||||
|
||||
$this
|
||||
->array($getLangDefs->invokeArgs(null, [2]))
|
||||
->string['fr']->isEqualTo('French');
|
||||
|
||||
$this
|
||||
->array($getLangDefs->invokeArgs(null, [3]))
|
||||
->string['fr']->isEqualTo('Français');
|
||||
|
||||
$this
|
||||
->array($getLangDefs->invokeArgs(null, [4]))
|
||||
->string['fr']->isEqualTo('ltr');
|
||||
|
||||
$this
|
||||
->array($getLangDefs->invokeArgs(null, [5]))
|
||||
->integer['fr']->isEqualTo(2);
|
||||
|
||||
$this
|
||||
->array($getLangDefs->invokeArgs(null, [6]))
|
||||
->string['fr']->isEqualTo('n > 1');
|
||||
}
|
||||
|
||||
/*
|
||||
**/
|
||||
protected function tempPoFile($content)
|
||||
{
|
||||
$filename = sys_get_temp_dir() . '/temp.po';
|
||||
|
||||
file_put_contents($filename, $content);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
<?php
|
||||
# -- BEGIN LICENSE BLOCK ---------------------------------------
|
||||
#
|
||||
# This file is part of Dotclear 2.
|
||||
#
|
||||
# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
|
||||
# Licensed under the GPL version 2.0 license.
|
||||
# See LICENSE file or
|
||||
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
#
|
||||
# -- END LICENSE BLOCK -----------------------------------------
|
||||
|
||||
namespace tests\unit;
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
require_once CLEARBRICKS_PATH . '/common/lib.text.php';
|
||||
require_once CLEARBRICKS_PATH . '/common/lib.html.php';
|
||||
|
||||
use atoum;
|
||||
use Faker;
|
||||
|
||||
/**
|
||||
* Test the form class
|
||||
*/
|
||||
class text extends atoum
|
||||
{
|
||||
public function testIsEmail()
|
||||
{
|
||||
$faker = Faker\Factory::create();
|
||||
$text = $faker->email();
|
||||
|
||||
$this
|
||||
->boolean(\text::isEmail($text))
|
||||
->isTrue();
|
||||
|
||||
$this
|
||||
->boolean(\text::isEmail('@dotclear.org'))
|
||||
->isFalse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider testIsEmailDataProvider
|
||||
*/
|
||||
protected function testIsEmailAllDataProvider()
|
||||
{
|
||||
require_once __DIR__ . '/../fixtures/data/lib.text.php';
|
||||
|
||||
return array_values($emailTest);
|
||||
}
|
||||
|
||||
public function testIsEmailAll($payload, $expected)
|
||||
{
|
||||
$this
|
||||
->boolean(\text::isEmail($payload))
|
||||
->isEqualTo($expected);
|
||||
}
|
||||
|
||||
public function testDeaccent()
|
||||
{
|
||||
$this
|
||||
->string(\text::deaccent('ÀÅÆÇÐÈËÌÏÑÒÖØŒŠÙÜÝŽàåæçðèëìïñòöøœšùüýÿžß éè'))
|
||||
->isEqualTo('AAAECDEEIINOOOOESUUYZaaaecdeeiinooooesuuyyzss ee');
|
||||
}
|
||||
|
||||
public function teststr2URL()
|
||||
{
|
||||
$this
|
||||
->string(\text::str2URL('https://domain.com/ÀÅÆÇÐÈËÌÏÑÒÖØŒŠÙÜÝŽàåæçðèëìïñòöøœšùüýÿžß/éè.html'))
|
||||
->isEqualTo('https://domaincom/AAAECDEEIINOOOOESUUYZaaaecdeeiinooooesuuyyzss/eehtml');
|
||||
|
||||
$this
|
||||
->string(\text::str2URL('https://domain.com/ÀÅÆÇÐÈËÌÏÑÒÖØŒŠÙÜÝŽàåæçðèëìïñòöøœšùüýÿžß/éè.html', false))
|
||||
->isEqualTo('https:-domaincom-AAAECDEEIINOOOOESUUYZaaaecdeeiinooooesuuyyzss-eehtml');
|
||||
}
|
||||
|
||||
public function testTidyURL()
|
||||
{
|
||||
// Keep /, no spaces
|
||||
$this
|
||||
->string(\text::tidyURL('Étrange et curieux/=À vous !'))
|
||||
->isEqualTo('Étrange-et-curieux/À-vous-!');
|
||||
|
||||
// Keep /, keep spaces
|
||||
$this
|
||||
->string(\text::tidyURL('Étrange et curieux/=À vous !', true, true))
|
||||
->isEqualTo('Étrange et curieux/À vous !');
|
||||
|
||||
// No /, keep spaces
|
||||
$this
|
||||
->string(\text::tidyURL('Étrange et curieux/=À vous !', false, true))
|
||||
->isEqualTo('Étrange et curieux-À vous !');
|
||||
|
||||
// No /, no spaces
|
||||
$this
|
||||
->string(\text::tidyURL('Étrange et curieux/=À vous !', false, false))
|
||||
->isEqualTo('Étrange-et-curieux-À-vous-!');
|
||||
}
|
||||
|
||||
public function testcutString()
|
||||
{
|
||||
$faker = Faker\Factory::create();
|
||||
$text = $faker->realText(400);
|
||||
$this
|
||||
->string(\text::cutString($text, 200))
|
||||
->hasLengthLessThan(201);
|
||||
|
||||
$this
|
||||
->string(\text::cutString('https:-domaincom-AAAECDEEIINOOOOESUUYZaaaecdeeiinooooesuuyyzss-eehtml', 20))
|
||||
->isIdenticalTo('https:-domaincom-AAA');
|
||||
|
||||
$this
|
||||
->string(\text::cutString('https domaincom AAAECDEEIINOOOOESUUYZaaaecdeeiinooooesuuyyzss eehtml', 20))
|
||||
->isIdenticalTo('https domaincom');
|
||||
}
|
||||
|
||||
public function testSplitWords()
|
||||
{
|
||||
$this
|
||||
->array(\text::splitWords('Étrange et curieux/=À vous !'))
|
||||
->hasSize(3)
|
||||
->string[0]->isEqualTo('étrange')
|
||||
->string[1]->isEqualTo('curieux')
|
||||
->string[2]->isEqualTo('vous');
|
||||
|
||||
$this
|
||||
->array(\text::splitWords(' '))
|
||||
->hasSize(0);
|
||||
}
|
||||
|
||||
public function testDetectEncoding()
|
||||
{
|
||||
$this
|
||||
->string(\text::detectEncoding('Étrange et curieux/=À vous !'))
|
||||
->isEqualTo('utf-8');
|
||||
|
||||
$test = mb_convert_encoding('Étrange et curieux/=À vous !', 'ISO-8859-1');
|
||||
$this
|
||||
->string(\text::detectEncoding($test))
|
||||
->isEqualTo('iso-8859-1');
|
||||
}
|
||||
|
||||
public function testToUTF8()
|
||||
{
|
||||
$this
|
||||
->string(\text::toUTF8('Étrange et curieux/=À vous !'))
|
||||
->isEqualTo('Étrange et curieux/=À vous !');
|
||||
|
||||
$test = mb_convert_encoding('Étrange et curieux/=À vous !', 'ISO-8859-1');
|
||||
$this
|
||||
->string(\text::toUTF8($test))
|
||||
->isEqualTo('Étrange et curieux/=À vous !');
|
||||
}
|
||||
|
||||
public function testUtf8badFind()
|
||||
{
|
||||
$this
|
||||
->variable(\text::utf8badFind('Étrange et curieux/=À vous !'))
|
||||
->isEqualTo(false);
|
||||
|
||||
$this
|
||||
->variable(\text::utf8badFind('Étrange et ' . chr(0xE0A0BF) . ' curieux/=À vous' . chr(0xC280) . ' !'))
|
||||
->isEqualTo(12);
|
||||
}
|
||||
|
||||
public function testCleanUTF8()
|
||||
{
|
||||
$this
|
||||
->string(\text::cleanUTF8('Étrange et curieux/=À vous !'))
|
||||
->isEqualTo('Étrange et curieux/=À vous !');
|
||||
|
||||
$this
|
||||
->string(\text::cleanUTF8('Étrange et ' . chr(0xE0A0BF) . ' curieux/=À vous' . chr(0xC280) . ' !'))
|
||||
->isEqualTo('Étrange et ? curieux/=À vous? !');
|
||||
}
|
||||
|
||||
public function testRemoveBOM()
|
||||
{
|
||||
$this
|
||||
->string(\text::removeBOM('Étrange et curieux/=À vous !'))
|
||||
->isEqualTo('Étrange et curieux/=À vous !');
|
||||
|
||||
$this
|
||||
->string(\text::removeBOM('' . 'Étrange et curieux/=À vous !'))
|
||||
->isEqualTo('Étrange et curieux/=À vous !');
|
||||
}
|
||||
|
||||
public function testQPEncode()
|
||||
{
|
||||
$this
|
||||
->string(\text::QPEncode('Étrange et curieux/=À vous !'))
|
||||
->isEqualTo('=C3=89trange et curieux/=3D=C3=80 vous !' . "\r\n");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
# ***** BEGIN LICENSE BLOCK *****
|
||||
# This file is part of Clearbricks.
|
||||
# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
|
||||
# All rights reserved.
|
||||
#
|
||||
# Clearbricks is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Clearbricks is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Clearbricks; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# ***** END LICENSE BLOCK *****
|
||||
|
||||
namespace tests\unit;
|
||||
|
||||
use atoum;
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
require_once CLEARBRICKS_PATH . '/dbschema/class.dbschema.php';
|
||||
|
||||
class dbSchema extends atoum
|
||||
{
|
||||
private $prefix = 'dc_';
|
||||
private $index = 0;
|
||||
|
||||
private function getConnection($driver)
|
||||
{
|
||||
$controller = new \atoum\atoum\mock\controller();
|
||||
$controller->__construct = function () {};
|
||||
|
||||
$class_name = sprintf('\mock\%sConnection', $driver);
|
||||
$con = new $class_name($driver, $controller);
|
||||
$this->calling($con)->driver = $driver;
|
||||
|
||||
return $con;
|
||||
}
|
||||
|
||||
public function testQueryForCreateTable($driver, $query)
|
||||
{
|
||||
$con = $this->getConnection($driver);
|
||||
|
||||
$table_name = $this->prefix . 'blog';
|
||||
$fields = ['status' => ['type' => 'smallint', 'len' => 0, 'null' => false, 'default' => -2]];
|
||||
|
||||
$this
|
||||
->if($schema = \dbSchema::init($con))
|
||||
->and($schema->createTable($table_name, $fields))
|
||||
->then()
|
||||
->mock($con)->call('execute')
|
||||
->withIdenticalArguments($query)
|
||||
->once();
|
||||
}
|
||||
|
||||
public function testQueryForRetrieveFields($driver, $query)
|
||||
{
|
||||
$con = $this->getConnection($driver);
|
||||
|
||||
$table_name = $this->prefix . 'blog';
|
||||
|
||||
$this
|
||||
->if($schema = \dbSchema::init($con))
|
||||
->and($schema->getColumns($table_name))
|
||||
->then()
|
||||
->mock($con)->call('select')
|
||||
->withIdenticalArguments($query)
|
||||
->once();
|
||||
}
|
||||
|
||||
/*
|
||||
* providers
|
||||
**/
|
||||
protected function testQueryForCreateTableDataProvider()
|
||||
{
|
||||
$query['pgsql'] = sprintf('CREATE TABLE "%sblog" (' . "\n", $this->prefix);
|
||||
$query['pgsql'] .= 'status smallint NOT NULL DEFAULT -2 ' . "\n";
|
||||
$query['pgsql'] .= ')';
|
||||
|
||||
$query['mysqli'] = sprintf('CREATE TABLE `%sblog` (' . "\n", $this->prefix);
|
||||
$query['mysqli'] .= '`status` smallint NOT NULL DEFAULT -2 ' . "\n";
|
||||
$query['mysqli'] .= ') ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin ';
|
||||
|
||||
$query['mysqlimb4'] = sprintf('CREATE TABLE `%sblog` (' . "\n", $this->prefix);
|
||||
$query['mysqlimb4'] .= '`status` smallint NOT NULL DEFAULT -2 ' . "\n";
|
||||
$query['mysqlimb4'] .= ') ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
|
||||
|
||||
return [
|
||||
['pgsql', $query['pgsql']],
|
||||
['mysqli', $query['mysqli']],
|
||||
['mysqlimb4', $query['mysqlimb4']],
|
||||
];
|
||||
}
|
||||
|
||||
protected function testQueryForRetrieveFieldsDataProvider()
|
||||
{
|
||||
$query['pgsql'] = sprintf("SELECT column_name, udt_name, character_maximum_length, is_nullable, column_default FROM information_schema.columns WHERE table_name = '%sblog' ", $this->prefix);
|
||||
|
||||
$query['mysqli'] = sprintf('SHOW COLUMNS FROM `%sblog`', $this->prefix);
|
||||
|
||||
$query['mysqlimb4'] = $query['mysqli'];
|
||||
|
||||
return [
|
||||
['pgsql', $query['pgsql']],
|
||||
['mysqli', $query['mysqli']],
|
||||
['mysqlimb4', $query['mysqlimb4']],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
# ***** BEGIN LICENSE BLOCK *****
|
||||
# This file is part of Clearbricks.
|
||||
# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
|
||||
# All rights reserved.
|
||||
#
|
||||
# Clearbricks is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Clearbricks is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Clearbricks; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# ***** END LICENSE BLOCK *****
|
||||
|
||||
namespace tests\unit;
|
||||
|
||||
use atoum;
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
require_once CLEARBRICKS_PATH . '/dbschema/class.dbstruct.php';
|
||||
|
||||
class dbStruct extends atoum
|
||||
{
|
||||
private $prefix = 'dc_';
|
||||
|
||||
private function getConnection($driver)
|
||||
{
|
||||
$controller = new \atoum\atoum\mock\controller();
|
||||
$controller->__construct = function () {};
|
||||
|
||||
$class_name = sprintf('\mock\%sConnection', $driver);
|
||||
$con = new $class_name($driver, $controller);
|
||||
$this->calling($con)->driver = $driver;
|
||||
|
||||
return $con;
|
||||
}
|
||||
|
||||
public function testMustEscapeNameInCreateTable($driver, $query)
|
||||
{
|
||||
$con = $this->getConnection($driver);
|
||||
|
||||
$s = new \dbStruct($con, $this->prefix);
|
||||
$s->blog->blog_id('varchar', 32, false);
|
||||
|
||||
$tables = $s->getTables();
|
||||
$tname = $this->prefix . 'blog';
|
||||
|
||||
$this
|
||||
->if($schema = \dbSchema::init($con))
|
||||
->and($schema->createTable($tname, $tables[$tname]->getFields()))
|
||||
->then()
|
||||
->mock($con)->call('execute')
|
||||
->withIdenticalArguments($query)
|
||||
->once();
|
||||
}
|
||||
|
||||
/*
|
||||
* providers
|
||||
**/
|
||||
protected function testMustEscapeNameInCreateTableDataProvider()
|
||||
{
|
||||
$create_query['mysqli'] = sprintf('CREATE TABLE `%sblog` (' . "\n", $this->prefix);
|
||||
$create_query['mysqli'] .= '`blog_id` varchar(32) NOT NULL ' . "\n";
|
||||
$create_query['mysqli'] .= ') ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin ';
|
||||
|
||||
$create_query['mysqlimb4'] = sprintf('CREATE TABLE `%sblog` (' . "\n", $this->prefix);
|
||||
$create_query['mysqlimb4'] .= '`blog_id` varchar(32) NOT NULL ' . "\n";
|
||||
$create_query['mysqlimb4'] .= ') ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
|
||||
|
||||
$create_query['pgsql'] = sprintf('CREATE TABLE "%sblog" (' . "\n", $this->prefix);
|
||||
$create_query['pgsql'] .= 'blog_id varchar(32) NOT NULL ' . "\n" . ')';
|
||||
|
||||
return [
|
||||
['pgsql', $create_query['pgsql']],
|
||||
['mysqli', $create_query['mysqli']],
|
||||
['mysqlimb4', $create_query['mysqlimb4']],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
dc: dotclear
|
||||
cb: clearbicks
|
|
@ -0,0 +1,930 @@
|
|||
<?php
|
||||
|
||||
// See: https://github.com/cure53/DOMPurify/blob/master/test/fixtures/expect.js
|
||||
$dataTest = [
|
||||
[
|
||||
'title' => "Don't remove ARIA attributes if not prohibited",
|
||||
'payload' => '<div aria-labelledby="msg--title" role="dialog" class="msg"><button class="modal-close" aria-label="close" type="button"><i class="icon-close"></i>some button</button></div>',
|
||||
'expected' => '<div aria-labelledby="msg--title" role="dialog" class="msg"><button class="modal-close" aria-label="close" type="button"><i class="icon-close"></i>some button</button></div>',
|
||||
],
|
||||
[
|
||||
'title' => 'safe usage of URI-like attribute values',
|
||||
'payload' => '<b href="javascript:alert(1)" title="javascript:alert(2)"></b>',
|
||||
'expected' => '<b title="javascript:alert(2)"></b>',
|
||||
],
|
||||
[
|
||||
'title' => 'src Attributes for IMG, AUDIO, VIDEO and SOURCE (see #131)',
|
||||
'payload' => '<img src="data:,123"><audio src="data:,456"></audio><video src="data:,789"></video><source src="data:,012"><div src="data:,345">',
|
||||
'expected' => '<img src="data:,123" /><audio src="data:,456"></audio><video src="data:,789"></video><source src="data:,012" /><div>',
|
||||
],
|
||||
[
|
||||
'title' => 'DOM Clobbering against document.createElement() (see #47)',
|
||||
'payload' => '<img src=x name=createElement><img src=y id=createElement>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'DOM Clobbering against an empty cookie',
|
||||
'payload' => '<img src=x name=cookie>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'JavaScript URIs using Unicode LS/PS I',
|
||||
'payload' => "123<a href='\u2028javascript:alert(1)'>I am a dolphin!</a>",
|
||||
'expected' => '123<a href="#">I am a dolphin!</a>',
|
||||
],
|
||||
[
|
||||
'title' => 'JavaScript URIs using Unicode Whitespace',
|
||||
'payload' => "123<a href=' javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href='᠎javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href='​javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a><a href=' javascript:alert(1)'>CLICK</a>",
|
||||
'expected' => '123<a href="#">CLICK</a>',
|
||||
],
|
||||
[
|
||||
'title' => 'Image with data URI src',
|
||||
'payload' => '<img src=data:image/jpeg,ab798ewqxbaudbuoibeqbla>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'Image with data URI src with whitespace',
|
||||
'payload' => "<img src=\"\r\ndata:image/jpeg,ab798ewqxbaudbuoibeqbla\">",
|
||||
'expected' => '<img src="data:image/jpeg,ab798ewqxbaudbuoibeqbla" />',
|
||||
],
|
||||
[
|
||||
'title' => 'Image with JavaScript URI src (DoS on Firefox)',
|
||||
'payload' => "<img src='javascript:while(1){}'>",
|
||||
'expected' => '<img src="#" />',
|
||||
],
|
||||
[
|
||||
'title' => 'Link with data URI href',
|
||||
'payload' => '<a href=data:,evilnastystuff>clickme</a>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'Simple numbers',
|
||||
'payload' => '123456',
|
||||
'expected' => '123456',
|
||||
],
|
||||
[
|
||||
'title' => 'DOM clobbering XSS by @irsdl using attributes',
|
||||
'payload' => "<form onmouseover='alert(1)'><input name=\"attributes\"><input name=\"attributes\">",
|
||||
'expected' => '<form><input name="attributes" /><input name="attributes" />',
|
||||
],
|
||||
[
|
||||
'title' => 'DOM clobbering: getElementById',
|
||||
'payload' => '<img src=x name=getElementById>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'DOM clobbering: location',
|
||||
'payload' => '<a href="#some-code-here" id="location">invisible',
|
||||
'expected' => '<a href="#some-code-here" id="location">invisible',
|
||||
],
|
||||
[
|
||||
'title' => 'onclick, onsubmit, onfocus; DOM clobbering: parentNode',
|
||||
'payload' => '<div onclick=alert(0)><form onsubmit=alert(1)><input onfocus=alert(2) name=parentNode>123</form></div>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'onsubmit, onfocus; DOM clobbering: nodeName',
|
||||
'payload' => '<form onsubmit=alert(1)><input onfocus=alert(2) name=nodeName>123</form>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'onsubmit, onfocus; DOM clobbering: nodeType',
|
||||
'payload' => '<form onsubmit=alert(1)><input onfocus=alert(2) name=nodeType>123</form>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'onsubmit, onfocus; DOM clobbering: children',
|
||||
'payload' => '<form onsubmit=alert(1)><input onfocus=alert(2) name=children>123</form>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'onsubmit, onfocus; DOM clobbering: attributes',
|
||||
'payload' => '<form onsubmit=alert(1)><input onfocus=alert(2) name=attributes>123</form>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'onsubmit, onfocus; DOM clobbering: removeChild',
|
||||
'payload' => '<form onsubmit=alert(1)><input onfocus=alert(2) name=removeChild>123</form>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'onsubmit, onfocus; DOM clobbering: removeAttributeNode',
|
||||
'payload' => '<form onsubmit=alert(1)><input onfocus=alert(2) name=removeAttributeNode>123</form>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'onsubmit, onfocus; DOM clobbering: setAttribute',
|
||||
'payload' => '<form onsubmit=alert(1)><input onfocus=alert(2) name=setAttribute>123</form>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => '>style<',
|
||||
'payload' => '<style>*{color: red}</style>',
|
||||
'expected' => '*{color: red}',
|
||||
],
|
||||
[
|
||||
'title' => 'HTML paragraph with text',
|
||||
'payload' => '<p>hello</p>',
|
||||
'expected' => '<p>hello</p>',
|
||||
],
|
||||
[
|
||||
'title' => 'mXSS Variation I',
|
||||
'payload' => '<listing><img onerror="alert(1);//" src=x><t t></listing>',
|
||||
'expected' => '<img onerror="alert(1);//" src=x>',
|
||||
],
|
||||
[
|
||||
'title' => 'mXSS Variation II',
|
||||
'payload' => "<img src=x id/=' onerror=alert(1)//'>",
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'Textarea and comments enabling img element',
|
||||
'payload' => '<textarea>@shafigullin</textarea><!--</textarea><img src=x onerror=alert(1)>-->',
|
||||
'expected' => '<textarea>@shafigullin</textarea>',
|
||||
],
|
||||
[
|
||||
'title' => 'Img element inside noscript terminated inside comment',
|
||||
'payload' => '<b><noscript><!-- </noscript><img src=x onerror=alert(1) --></noscript>',
|
||||
'expected' => '<b>',
|
||||
],
|
||||
[
|
||||
'title' => 'Img element inside noscript terminated inside attribute',
|
||||
'payload' => '<b><noscript><a alt="</noscript><img src=x onerror=alert(1)>"></noscript>',
|
||||
'expected' => '<b>',
|
||||
],
|
||||
[
|
||||
'title' => 'Img element inside shadow DOM template',
|
||||
'payload' => '<body><template><s><template><s><img src=x onerror=alert(1)>@shafigullin</s></template></s></template>',
|
||||
'expected' => '<template><s><template><s>',
|
||||
],
|
||||
[
|
||||
'title' => 'Low-range-ASCII obfuscated JavaScript URI',
|
||||
'payload' => "<a href=\"\u0001java\u0003script:alert(1)\">@shafigullin<a>",
|
||||
'expected' => '<a href="#">@shafigullin<a>',
|
||||
],
|
||||
[
|
||||
'title' => 'Img inside style inside broken option element',
|
||||
'payload' => "\u0001<option><style></option></select><b><img src=x onerror=alert(1)></style></option>",
|
||||
'expected' => "\u0001<option>",
|
||||
],
|
||||
[
|
||||
'title' => 'Iframe inside option element',
|
||||
'payload' => '<option><iframe></select><b><script>alert(1)</script>',
|
||||
'expected' => '<option><iframe>',
|
||||
],
|
||||
[
|
||||
'title' => 'Closing Iframe and option',
|
||||
'payload' => '</iframe></option>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'Image after style to trick jQuery tag-completion',
|
||||
'payload' => '<b><style><style/><img src=x onerror=alert(1)>',
|
||||
'expected' => '<b>',
|
||||
],
|
||||
[
|
||||
'title' => 'Image after self-closing style to trick jQuery tag-completion',
|
||||
'payload' => '<b><style><style////><img src=x onerror=alert(1)></style>',
|
||||
'expected' => '<b>',
|
||||
],
|
||||
[
|
||||
'title' => 'DOM clobbering attack using name=body',
|
||||
'payload' => '<image name=body><image name=adoptNode>@mmrupp<image name=firstElementChild><svg onload=alert(1)>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'Special esacpes in protocol handler for XSS in Blink',
|
||||
'payload' => "<a href=\"\u0001java\u0003script:alert(1)\">@shafigullin<a>",
|
||||
'expected' => '<a href="#">@shafigullin<a>',
|
||||
],
|
||||
[
|
||||
'title' => 'DOM clobbering attack using activeElement',
|
||||
'payload' => '<image name=activeElement><svg onload=alert(1)>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'DOM clobbering attack using name=body and injecting SVG + keygen',
|
||||
'payload' => '<image name=body><img src=x><svg onload=alert(1); autofocus>, <keygen onfocus=alert(1); autofocus>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'Bypass using multiple unknown attributes',
|
||||
'payload' => '<div onmouseout="javascript:alert(/superevr/)" x=yscript: n>@superevr</div>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'Bypass using event handlers and unknown attributes',
|
||||
'payload' => '<button remove=me onmousedown="javascript:alert(1);" onclick="javascript:alert(1)" >@giutro',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'Bypass using DOM bugs when dealing with JS URIs in arbitrary attributes',
|
||||
'payload' => '<a href="javascript:123" onclick="alert(1)">CLICK ME (bypass by @shafigullin)</a>',
|
||||
'expected' => '<a href="#">CLICK ME (bypass by @shafigullin)</a>',
|
||||
],
|
||||
[
|
||||
'title' => 'Bypass using DOM bugs when dealing with JS URIs in arbitrary attributes (II)',
|
||||
'payload' => '<isindex x="javascript:" onmouseover="alert(1)" label="variation of bypass by @giutro">',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'Bypass using unknown attributes III',
|
||||
'payload' => '<div wow=removeme onmouseover=alert(1)>text',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'Bypass using unknown attributes IV',
|
||||
'payload' => '<input x=javascript: autofocus onfocus=alert(1)><svg id=1 onload=alert(1)></svg>',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'Bypass using unknown attributes V',
|
||||
'payload' => '<isindex src="javascript:" onmouseover="alert(1)" label="bypass by @giutro" />',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'Bypass using JS URI in href',
|
||||
'payload' => '<a href="javascript:123" onclick="alert(1)">CLICK ME (bypass by @shafigullin)</a>',
|
||||
'expected' => '<a href="#">CLICK ME (bypass by @shafigullin)</a>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<form action=\"javasc\nript:alert(1)\"><button>XXX</button></form>",
|
||||
'expected' => '<form action="#"><button>XXX</button></form>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"1\"><form id=\"foobar\"></form><button form=\"foobar\" formaction=\"javascript:alert(1)\">X</button>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="1"><form id="foobar"></form><button form="foobar" formaction="#">X</button>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"2\"><meta charset=\"x-imap4-modified-utf7\">&ADz&AGn&AG0&AEf&ACA&AHM&AHI&AGO&AD0&AGn&ACA&AG8Abg&AGUAcgByAG8AcgA9AGEAbABlAHIAdAAoADEAKQ&ACAAPABi//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="2">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"3\"><meta charset=\"x-imap4-modified-utf7\">&<script&S1&TS&1>alert&A7&(1)&R&UA;&&<&A9&11/script&X&>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="3">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"4\">0?<script>Worker(\"#\").onmessage=function(_)eval(_.data)</script> :postMessage(importScripts('data:;base64,cG9zdE1lc3NhZ2UoJ2FsZXJ0KDEpJyk'))//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="4">0?Worker("#").onmessage=function(_)eval(_.data)',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"5\"><script>crypto.generateCRMFRequest('CN=0',0,0,null,'alert(5)',384,null,'rsa-dual-use')</script>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"5\">crypto.generateCRMFRequest('CN=0',0,0,null,'alert(5)',384,null,'rsa-dual-use')",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"6\"><script>({set/**/$($){_/**/setter=$,_=1}}).$=alert</script>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="6">({set/**/$($){_/**/setter=$,_=1}}).$=alert',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"7\"><input onfocus=alert(7) autofocus>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="7">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"8\"><input onblur=alert(8) autofocus><input autofocus>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="8">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"9\"><a style=\"-o-link:'javascript:alert(9)';-o-link-source:current\">X</a>//[\"'`-->]]>]</div>\n\n<div id=\"10\"><video poster=javascript:alert(10)//></video>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"9\"><a style=\"-o-link:'javascript:alert(9)';-o-link-source:current\">X</a>",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"11\"><svg xmlns=\"http://www.w3.org/2000/svg\"><g onload=\"javascript:alert(11)\"></g></svg>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="11">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"12\"><body onscroll=alert(12)><br><br><br><br><br><br>...<br><br><br><br><input autofocus>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="12">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"13\"><x repeat=\"template\" repeat-start=\"999999\">0<y repeat=\"template\" repeat-start=\"999999\">1</y></x>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="13">01',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"14\"><input pattern=^((a+.)a)+$ value=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="14">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"15\"><script>({0:#0=alert/#0#/#0#(0)})</script>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="15">({0:#0=alert/#0#/#0#(0)})',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"16\">X<x style=`behavior:url(#default#time2)` onbegin=`alert(16)` >//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="16">X',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"17\"><?xml-stylesheet href=\"javascript:alert(17)\"?><root/>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="17">>?xml-stylesheet href="javascript:alert(17)"?<',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"18\"><script xmlns=\"http://www.w3.org/1999/xhtml\">alert(1)</script>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="18">alert(1)',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"19\"><meta charset=\"x-mac-farsi\">\u00BCscript \u00BEalert(19)//\u00BC/script \u00BE//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="19">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"20\"><script>ReferenceError.prototype.__defineGetter__('name', function(){alert(20)}),x</script>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"20\">ReferenceError.prototype.__defineGetter__('name', function(){alert(20)}),x",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"21\"><script>Object.__noSuchMethod__ = Function,[{}][0].constructor._('alert(21)')()</script>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"21\">Object.__noSuchMethod__ = Function,[{}][0].constructor._('alert(21)')()",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"22\"><input onblur=focus() autofocus><input>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="22">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"23\"><form id=foobar onforminput=alert(23)><input></form><button form=test onformchange=alert(2)>X</button>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="23">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"24\">1<set/xmlns=`urn:schemas-microsoft-com:time` style=`behAvior:url(#default#time2)` attributename=`innerhtml` to=`<img/src=\"x\"onerror=alert(24)>`>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="24">1',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"25\"><script src=\"#\">{alert(25)}</script>;1//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="25">{alert(25)}',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"26\">+ADw-html+AD4APA-body+AD4APA-div+AD4-top secret+ADw-/div+AD4APA-/body+AD4APA-/html+AD4-.toXMLString().match(/.*/m),alert(RegExp.input);//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="26">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"27\"><style>p[foo=bar{}*{-o-link:'javascript:alert(27)'}{}*{-o-link-source:current}*{background:red}]{background:green};</style>//[\"'`-->]]>]</div><div id=\"28\">1<animate/xmlns=urn:schemas-microsoft-com:time style=behavior:url(#default#time2) attributename=innerhtml values=<img/src=\".\"onerror=alert(28)>>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"27\">p[foo=bar{}*{-o-link:'javascript:alert(27)'}{}*{-o-link-source:current}*{background:red}]{background:green};",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"29\"><link rel=stylesheet href=data:,*%7bx:expression(alert(29))%7d//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="29">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"30\"><style>@import \"data:,*%7bx:expression(alert(30))%7D\";</style>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="30">@import "data:,*%7bx:expression(alert(30))%7D";',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"31\"><frameset onload=alert(31)>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="31">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"32\"><table background=\"javascript:alert(32)\"></table>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="32"><table></table>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"33\"><a style=\"pointer-events:none;position:absolute;\"><a style=\"position:absolute;\" onclick=\"alert(33);\">XXX</a></a><a href=\"javascript:alert(2)\">XXX</a>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="33"><a style="pointer-events:none;position:absolute;"><a style="position:absolute;">XXX</a></a><a href="#">XXX</a>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"34\">1<vmlframe xmlns=urn:schemas-microsoft-com:vml style=behavior:url(#default#vml);position:absolute;width:100%;height:100% src=test.vml#xss></vmlframe>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="34">1',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"35\">1<a href=#><line xmlns=urn:schemas-microsoft-com:vml style=behavior:url(#default#vml);position:absolute href=javascript:alert(35) strokecolor=white strokeweight=1000px from=0 to=1000 /></a>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="35">1',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"36\"><a style=\"behavior:url(#default#AnchorClick);\" folder=\"javascript:alert(36)\">XXX</a>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="36"><a style="behavior:url(#default#AnchorClick);">XXX</a>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"37\"><!--<img src=\"--><img src=x onerror=alert(37)//\">//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="37">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"38\"><comment><img src=\"</comment><img src=x onerror=alert(38)//\">//[\"'`-->]]>]</div><div id=\"39\"><!-- up to Opera 11.52, FF 3.6.28 -->",
|
||||
'expected' => '<div id="38">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => '<![><img src="]><img src=x onerror=alert(39)//">',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<!-- IE9+, FF4+, Opera 11.60+, Safari 4.0.4+, GC7+ -->\n<svg><![CDATA[><image xlink:href=\"]]><img src=x onerror=alert(2)//\"></svg>//[\"'`-->]]>]</div>",
|
||||
'expected' => "\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"40\"><style><img src=\"</style><img src=x onerror=alert(40)//\">//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="40">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => '<div id="41"><li style=list-style:url() onerror=alert(41)></li>',
|
||||
'expected' => '<div id="41">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div style=content:url(data:image/svg+xml,%3Csvg/%3E);visibility:hidden onload=alert(41)></div>//[\"'`-->]]>]</div>",
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"42\"><head><base href=\"javascript://\"/></head><body><a href=\"/. /,alert(42)//#\">XXX</a></body>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="42"><a href="/./,alert(42)//#">XXX</a>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => '<div id="43"><?xml version="1.0" standalone="no"?>',
|
||||
'expected' => '<div id="43">>?xml version="1.0" standalone="no"?<',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<style type=\"text/css\">\n@font-face {font-family: y; src: url(\"font.svg#x\") format(\"svg\");} body {font: 100px \"y\";}\n</style>\n</head>\n<body>Hello</body>\n</html>//[\"'`-->]]>]</div>",
|
||||
'expected' => "\n\n\n@font-face {font-family: y; src: url("font.svg#x") format("svg");} body {font: 100px "y";}\n\n\nHello\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"44\"><style>*[{}@import'test.css?]{color: green;}</style>X//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"44\">*[{}@import'test.css?]{color: green;}",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"45\"><div style=\"font-family:'foo[a];color:red;';\">XXX</div>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"45\"><div style=\"font-family:'foo[a];color:red;';\">XXX</div>",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"46\"><div style=\"font-family:foo}color=red;\">XXX</div>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="46"><div style="font-family:foo}color=red;">XXX</div>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"47\"><svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(47)</script></svg>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="47">alert(47)',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"48\"><SCRIPT FOR=document EVENT=onreadystatechange>alert(48)</SCRIPT>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="48">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"49\"><OBJECT CLASSID=\"clsid:333C7BC4-460F-11D0-BC04-0080C7055A83\"><PARAM NAME=\"DataURL\" VALUE=\"javascript:alert(49)\"></OBJECT>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="49"><OBJECT><PARAM />',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"50\"><object data=\"data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==\"></object>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="50"><object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></object>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"51\"><embed src=\"data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==\"></embed>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="51"><embed src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==" />',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"52\"><x style=\"behavior:url(test.sct)\">//[\"'`-->]]>]</div><div id=\"53\"><xml id=\"xss\" src=\"test.htc\"></xml>",
|
||||
'expected' => '<div id="52">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<label dataformatas=\"html\" datasrc=\"#xss\" datafld=\"payload\"></label>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<label></label>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"54\"><script>[{'a':Object.prototype.__defineSetter__('b',function(){alert(arguments[0])}),'b':['secret']}]</script>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"54\">[{'a':Object.prototype.__defineSetter__('b',function(){alert(arguments[0])}),'b':['secret']}]",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"55\"><video><source onerror=\"alert(55)\">//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="55"><video><source />',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"56\"><video onerror=\"alert(56)\"><source></source></video>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="56"><video><source /></video>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"57\"><b <script>alert(57)//</script>0</script></b>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="57">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"58\"><b><script<b></b><alert(58)</script </b></b>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="58"><b>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"59\"><div id=\"div1\"><input value=\"``onmouseover=alert(59)\"></div> <div id=\"div2\"></div><script>document.getElementById(\"div2\").innerHTML = document.getElementById(\"div1\").innerHTML;</script>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="59"><div id="div1"><input value="``onmouseover=alert(59)" />',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"60\"><div style=\"[a]color[b]:[c]red\">XXX</div>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="60"><div style="[a]color[b]:[c]red">XXX</div>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"62\"><!-- IE 6-8 -->\n<x '=\"foo\"><x foo='><img src=x onerror=alert(62)//'>\n<!-- IE 6-9 -->\n<! '=\"foo\"><x foo='><img src=x onerror=alert(2)//'>\n<? '=\"foo\"><x foo='><img src=x onerror=alert(3)//'>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"62\">\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"63\"><embed src=\"javascript:alert(63)\"></embed> // O10.10\u2193, OM10.0\u2193, GC6\u2193, FF\n<img src=\"javascript:alert(2)\">\n<image src=\"javascript:alert(2)\"> // IE6, O10.10\u2193, OM10.0\u2193\n<script src=\"javascript:alert(3)\"></script> // IE6, O11.01\u2193, OM10.1\u2193//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"63\"><embed src=\"#\" /> // O10.10\u2193, OM10.0\u2193, GC6\u2193, FF\n<img src=\"#\" />\n // IE6, O10.10\u2193, OM10.0\u2193\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"64\"><!DOCTYPE x[<!ENTITY x SYSTEM \"http://html5sec.org/test.xxe\">]><y>&x;</y>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="64">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"65\"><svg onload=\"javascript:alert(65)\" xmlns=\"http://www.w3.org/2000/svg\"></svg>//[\"'`-->]]>]</div><div id=\"66\"><?xml version=\"1.0\"?>",
|
||||
'expected' => '<div id="65">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<?xml-stylesheet type=\"text/xsl\" href=\"data:,%3Cxsl:transform version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' id='xss'%3E%3Cxsl:output method='html'/%3E%3Cxsl:template match='/'%3E%3Cscript%3Ealert(66)%3C/script%3E%3C/xsl:template%3E%3C/xsl:transform%3E\"?>\n<root/>//[\"'`-->]]>]</div>\n<div id=\"67\"><!DOCTYPE x [\n <!ATTLIST img xmlns CDATA \"http://www.w3.org/1999/xhtml\" src CDATA \"xx\"\n onerror CDATA \"alert(67)\"\n onload CDATA \"alert(2)\">\n]><img />//[\"'`-->]]>]</div>",
|
||||
'expected' => ">?xml-stylesheet type="text/xsl" href="data:,%3Cxsl:transform version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' id='xss'%3E%3Cxsl:output method='html'/%3E%3Cxsl:template match='/'%3E%3Cscript%3Ealert(66)%3C/script%3E%3C/xsl:template%3E%3C/xsl:transform%3E"?<\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"68\"><doc xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:html=\"http://www.w3.org/1999/xhtml\">\n <html:style /><x xlink:href=\"javascript:alert(68)\" xlink:type=\"simple\">XXX</x>\n</doc>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"68\">\n XXX\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"69\"><card xmlns=\"http://www.wapforum.org/2001/wml\"><onevent type=\"ontimer\"><go href=\"javascript:alert(69)\"/></onevent><timer value=\"1\"/></card>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="69">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"70\"><div style=width:1px;filter:glow onfilterchange=alert(70)>x</div>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="70">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"71\"><// style=x:expression\u00028alert(71)\u00029>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="71">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"72\"><form><button formaction=\"javascript:alert(72)\">X</button>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="72"><form><button formaction="#">X</button>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"73\"><event-source src=\"event.php\" onload=\"alert(73)\">//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="73">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"74\"><a href=\"javascript:alert(74)\"><event-source src=\"data:application/x-dom-event-stream,Event:click%0Adata:XXX%0A%0A\" /></a>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="74"><a href="#"></a>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"75\"><script<{alert(75)}/></script </>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="75">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"76\"><?xml-stylesheet type=\"text/css\"?><!DOCTYPE x SYSTEM \"test.dtd\"><x>&x;</x>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="76">>?xml-stylesheet type="text/css"?<',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"77\"><?xml-stylesheet type=\"text/css\"?><root style=\"x:expression(alert(77))\"/>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="77">>?xml-stylesheet type="text/css"?<',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"78\"><?xml-stylesheet type=\"text/xsl\" href=\"#\"?><img xmlns=\"x-schema:test.xdr\"/>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="78">>?xml-stylesheet type="text/xsl" href="#"?<<img />',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"79\"><object allowscriptaccess=\"always\" data=\"x\"></object>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="79"><object data="x"></object>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"80\"><style>*{x:\uFF45\uFF58\uFF50\uFF52\uFF45\uFF53\uFF53\uFF49\uFF4F\uFF4E(alert(80))}</style>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"80\">*{x:\uFF45\uFF58\uFF50\uFF52\uFF45\uFF53\uFF53\uFF49\uFF4F\uFF4E(alert(80))}",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"81\"><x xmlns:xlink=\"http://www.w3.org/1999/xlink\" xlink:actuate=\"onLoad\" xlink:href=\"javascript:alert(81)\" xlink:type=\"simple\"/>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="81">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"82\"><?xml-stylesheet type=\"text/css\" href=\"data:,*%7bx:expression(write(2));%7d\"?>//[\"'`-->]]>]</div><div id=\"83\"><x:template xmlns:x=\"http://www.wapforum.org/2001/wml\" x:ontimer=\"$(x:unesc)j$(y:escape)a$(z:noecs)v$(x)a$(y)s$(z)cript\$x:alert(83)\"><x:timer value=\"1\"/></x:template>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="82">>?xml-stylesheet type="text/css" href="data:,*%7bx:expression(write(2));%7d"?<',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"84\"><x xmlns:ev=\"http://www.w3.org/2001/xml-events\" ev:event=\"load\" ev:handler=\"javascript:alert(84)//#x\"/>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="84">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"85\"><x xmlns:ev=\"http://www.w3.org/2001/xml-events\" ev:event=\"load\" ev:handler=\"test.evt#x\"/>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="85">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"86\"><body oninput=alert(86)><input autofocus>//[\"'`-->]]>]</div><div id=\"87\"><svg xmlns=\"http://www.w3.org/2000/svg\">\n<a xmlns:xlink=\"http://www.w3.org/1999/xlink\" xlink:href=\"javascript:alert(87)\"><rect width=\"1000\" height=\"1000\" fill=\"white\"/></a>\n</svg>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="86">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"89\"><svg xmlns=\"http://www.w3.org/2000/svg\">\n<set attributeName=\"onmouseover\" to=\"alert(89)\"/>\n<animate attributeName=\"onunload\" to=\"alert(89)\"/>\n</svg>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"89\">\n\n\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"90\"><!-- Up to Opera 10.63 -->\n<div style=content:url(test2.svg)></div>\n\n<!-- Up to Opera 11.64 - see link below -->\n\n<!-- Up to Opera 12.x -->\n<div style=\"background:url(test5.svg)\">PRESS ENTER</div>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"90\">\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"91\">[A]\n<? foo=\"><script>alert(91)</script>\">\n<! foo=\"><script>alert(91)</script>\">\n</ foo=\"><script>alert(91)</script>\">\n[B]\n<? foo=\"><x foo='?><script>alert(91)</script>'>\">\n[C]\n<! foo=\"[[[x]]\"><x foo=\"]foo><script>alert(91)</script>\">\n[D]\n<% foo><x foo=\"%><script>alert(91)</script>\">//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"91\">[A]\n>? foo=">alert(91)">\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"92\"><div style=\"background:url(http://foo.f/f oo/;color:red/*/foo.jpg);\">X</div>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="92"><div style="background:url(http://foo.f/f oo/;color:red/*/foo.jpg);">X</div>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"93\"><div style=\"list-style:url(http://foo.f)\u0010url(javascript:alert(93));\">X</div>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"93\"><div style=\"list-style:url(http://foo.f)\u0010url(javascript:alert(93));\">X</div>",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"94\"><svg xmlns=\"http://www.w3.org/2000/svg\">\n<handler xmlns:ev=\"http://www.w3.org/2001/xml-events\" ev:event=\"load\">alert(94)</handler>\n</svg>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"94\">\nalert(94)\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"95\"><svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n<feImage>\n<set attributeName=\"xlink:href\" to=\"data:image/svg+xml;charset=utf-8;base64,\nPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxzY3JpcHQ%2BYWxlcnQoMSk8L3NjcmlwdD48L3N2Zz4NCg%3D%3D\"/>\n</feImage>\n</svg>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"95\">\n\n\n\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"96\"><iframe src=mhtml:http://html5sec.org/test.html!xss.html></iframe>\n<iframe src=mhtml:http://html5sec.org/test.gif!xss.html></iframe>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="96">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"97\"><!-- IE 5-9 -->\n<div id=d><x xmlns=\"><iframe onload=alert(97)\"></div>\n<script>d.innerHTML+='';</script>\n<!-- IE 10 in IE5-9 Standards mode -->\n<div id=d><x xmlns='\"><iframe onload=alert(2)//'></div>\n<script>d.innerHTML+='';</script>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"97\">\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"98\"><div id=d><div style=\"font-family:'sans\u0017\u0002F\u0002A\u0012\u0002A\u0002F\u0003B color\u0003Ared\u0003B'\">X</div></div>\n<script>with(document.getElementById(\"d\"))innerHTML=innerHTML</script>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="98">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"99\">XXX<style>\n\n*{color:gre/**/en !/**/important} /* IE 6-9 Standards mode */\n\n<!--\n--><!--*{color:red} /* all UA */\n\n*{background:url(xx //**/\red/*)} /* IE 6-7 Standards mode */\n\n</style>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="99">XXX',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"100\"><img[a][b]src=x[d]onerror[c]=[e]\"alert(100)\">//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="100">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"101\"><a href=\"[a]java[b]script[c]:alert(101)\">XXX</a>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="101"><a href="[a]java[b]script[c]:alert(101)">XXX</a>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"102\"><img src=\"x` `<script>alert(102)</script>\"` `>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="102">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"103\"><script>history.pushState(0,0,'/i/am/somewhere_else');</script>//[\"'`-->]]>]</div><div id=\"104\"><svg xmlns=\"http://www.w3.org/2000/svg\" id=\"foo\">\n<x xmlns=\"http://www.w3.org/2001/xml-events\" event=\"load\" observer=\"foo\" handler=\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Chandler%20xml%3Aid%3D%22bar%22%20type%3D%22application%2Fecmascript%22%3E alert(104) %3C%2Fhandler%3E%0A%3C%2Fsvg%3E%0A#bar\"/>\n</svg>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"103\">history.pushState(0,0,'/i/am/somewhere_else');",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"105\"><iframe src=\"data:image/svg-xml,%1F%8B%08%00%00%00%00%00%02%03%B3)N.%CA%2C(Q%A8%C8%CD%C9%2B%B6U%CA())%B0%D2%D7%2F%2F%2F%D7%2B7%D6%CB%2FJ%D77%B4%B4%B4%D4%AF%C8(%C9%CDQ%B2K%CCI-*%D10%D4%B4%D1%87%E8%B2%03\"></iframe>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="105"><iframe src="data:image/svg-xml,%1F%8B%08%00%00%00%00%00%02%03%B3)N.%CA%2C(Q%A8%C8%CD%C9%2B%B6U%CA())%B0%D2%D7%2F%2F%2F%D7%2B7%D6%CB%2FJ%D77%B4%B4%B4%D4%AF%C8(%C9%CDQ%B2K%CCI-*%D10%D4%B4%D1%87%E8%B2%03"></iframe>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"106\"><img src onerror /\" '\"= alt=alert(106)//\">//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="106">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"107\"><title onpropertychange=alert(107)></title><title title=></title>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="107">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"108\"><!-- IE 5-8 standards mode -->\n<a href=http://foo.bar/#x=`y></a><img alt=\"`><img src=xx onerror=alert(108)></a>\">\n<!-- IE 5-9 standards mode -->\n<!a foo=x=`y><img alt=\"`><img src=xx onerror=alert(2)//\">\n<?a foo=x=`y><img alt=\"`><img src=xx onerror=alert(3)//\">//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"108\">\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"109\"><svg xmlns=\"http://www.w3.org/2000/svg\">\n<a id=\"x\"><rect fill=\"white\" width=\"1000\" height=\"1000\"/></a>\n<rect fill=\"white\" style=\"clip-path:url(test3.svg#a);fill:url(#b);filter:url(#c);marker:url(#d);mask:url(#e);stroke:url(#f);\"/>\n</svg>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"109\">\n<a id=\"x\"></a>\n\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"110\"><svg xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M0,0\" style=\"marker-start:url(test4.svg#a)\"/>\n</svg>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"110\">\n\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"111\"><div style=\"background:url(/f#[a]oo/;color:red/*/foo.jpg);\">X</div>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="111"><div style="background:url(/f#[a]oo/;color:red/*/foo.jpg);">X</div>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"112\"><div style=\"font-family:foo{bar;background:url(http://foo.f/oo};color:red/*/foo.jpg);\">X</div>//[\"'`-->]]>]</div><div id=\"113\"><div id=\"x\">XXX</div>\n<style>\n\n#x{font-family:foo[bar;color:green;}\n\n#y];color:red;{}\n\n</style>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="112"><div style="font-family:foo{bar;background:url(http://foo.f/oo};color:red/*/foo.jpg);">X</div>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"114\"><x style=\"background:url('x[a];color:red;/*')\">XXX</x>//[\"'`-->]]>]</div><div id=\"115\"><!--[if]><script>alert(115)</script -->\n<!--[if<img src=x onerror=alert(2)//]> -->//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="114">XXX',
|
||||
],
|
||||
[
|
||||
'title' => 'XML',
|
||||
'payload' => "<div id=\"116\"><div id=\"x\">x</div>\n<xml:namespace prefix=\"t\">\n<import namespace=\"t\" implementation=\"#default#time2\">\n<t:set attributeName=\"innerHTML\" targetElement=\"x\" to=\"<img\u000Bsrc=x\u000Bonerror\u000B=alert(116)>\">//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"116\"><div id=\"x\">x</div>\n\n\n",
|
||||
],
|
||||
[
|
||||
'title' => 'iframe',
|
||||
'payload' => "<div id=\"117\"><a href=\"http://attacker.org\">\n <iframe src=\"http://example.org/\"></iframe>\n</a>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"117\"><a href=\"http://attacker.org\">\n <iframe src=\"http://example.org/\"></iframe>\n</a>",
|
||||
],
|
||||
[
|
||||
'title' => 'Drag & drop',
|
||||
'payload' => "<div id=\"118\"><div draggable=\"true\" ondragstart=\"event.dataTransfer.setData('text/plain','malicious code');\">\n <h1>Drop me</h1>\n</div>\n<iframe src=\"http://www.example.org/dropHere.html\"></iframe>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"118\"><div draggable=\"true\">\n <h1>Drop me</h1>\n</div>\n<iframe src=\"http://www.example.org/dropHere.html\"></iframe>",
|
||||
],
|
||||
[
|
||||
'title' => 'view-source',
|
||||
'payload' => '<div id="119"><iframe src="view-source:http://www.example.org/" frameborder="0" style="width:400px;height:180px"></iframe>',
|
||||
'expected' => '<div id="119"><iframe src="#" frameborder="0" style="width:400px;height:180px"></iframe>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<textarea type=\"text\" cols=\"50\" rows=\"10\"></textarea>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<textarea cols="50" rows="10"></textarea>',
|
||||
],
|
||||
[
|
||||
'title' => 'window.open',
|
||||
'payload' => "<div id=\"120\"><script>\nfunction makePopups(){\n for (i=1;i<6;i++) {\n window.open('popup.html','spam'+i,'width=50,height=50');\n }\n}\n</script>\n<body>\n<a href=\"#\" onclick=\"makePopups()\">Spam</a>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"120\">\nfunction makePopups(){\n for (i=1;i",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"121\"><html xmlns=\"http://www.w3.org/1999/xhtml\"\nxmlns:svg=\"http://www.w3.org/2000/svg\">\n<body style=\"background:gray\">\n<iframe src=\"http://example.com/\" style=\"width:800px; height:350px; border:none; mask: url(#maskForClickjacking);\"/>\n<svg:svg>\n<svg:mask id=\"maskForClickjacking\" maskUnits=\"objectBoundingBox\" maskContentUnits=\"objectBoundingBox\">\n <svg:rect x=\"0.0\" y=\"0.0\" width=\"0.373\" height=\"0.3\" fill=\"white\"/>\n <svg:circle cx=\"0.45\" cy=\"0.7\" r=\"0.075\" fill=\"white\"/>\n</svg:mask>\n</svg:svg>\n</body>\n</html>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"121\">\n\n<iframe src=\"http://example.com/\" style=\"width:800px; height:350px; border:none; mask: url(#maskForClickjacking);\"></iframe>\n\n\n \n \n\n\n\n",
|
||||
],
|
||||
[
|
||||
'title' => 'iframe (sandboxed)',
|
||||
'payload' => "<div id=\"122\"><iframe sandbox=\"allow-same-origin allow-forms allow-scripts\" src=\"http://example.org/\"></iframe>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="122"><iframe sandbox="allow-same-origin allow-forms allow-scripts" src="http://example.org/"></iframe>',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"123\"><span class=foo>Some text</span>\n<a class=bar href=\"http://www.example.org\">www.example.org</a>\n<script src=\"http://code.jquery.com/jquery-1.4.4.js\"></script>\n<script>\n$(\"span.foo\").click(function() {\nalert('foo');\n$(\"a.bar\").click();\n});\n$(\"a.bar\").click(function() {\nalert('bar');\nlocation=\"http://html5sec.org\";\n});\n</script>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="123">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"124\"><script src=\"/example.com\foo.js\"></script> // Safari 5.0, Chrome 9, 10\n<script src=\"\\example.com\foo.js\"></script> // Safari 5.0//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="124">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"125\"><?xml version=\"1.0\"?><?xml-stylesheet type=\"text/xml\" href=\"#stylesheet\"?><!DOCTYPE doc [<!ATTLIST xsl:stylesheet id ID #REQUIRED>]><svg xmlns=\"http://www.w3.org/2000/svg\"> <xsl:stylesheet id=\"stylesheet\" version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"> <xsl:template match=\"/\"> <iframe xmlns=\"http://www.w3.org/1999/xhtml\" src=\"javascript:alert(125)\"></iframe> </xsl:template> </xsl:stylesheet> <circle fill=\"red\" r=\"40\"></circle></svg>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="125">>?xml version="1.0"?<>?xml-stylesheet type="text/xml" href="#stylesheet"?<',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"126\"><object id=\"x\" classid=\"clsid:CB927D12-4FF7-4a9e-A169-56E4B8A75598\"></object>\n<object classid=\"clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B\" onqt_error=\"alert(126)\" style=\"behavior:url(#x);\"><param name=postdomevents /></object>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"126\"><object id=\"x\" classid=\"#\"></object>\n<object classid=\"#\" style=\"behavior:url(#x);\">",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"127\"><svg xmlns=\"http://www.w3.org/2000/svg\" id=\"x\">\n<listener event=\"load\" handler=\"#y\" xmlns=\"http://www.w3.org/2001/xml-events\" observer=\"x\"/>\n<handler id=\"y\">alert(127)</handler>\n</svg>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"127\">\n\nalert(127)\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"128\"><svg><style><img/src=x onerror=alert(128)// </b>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="128">',
|
||||
],
|
||||
[
|
||||
'title' => 'Inline SVG (data-uri)',
|
||||
'payload' => "<div id=\"129\"><svg><image style='filter:url(\"data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22><script>parent.alert(129)</script></svg>\")'>\n<!--\nSame effect with\n<image filter='...'>\n-->\n</svg>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="129">',
|
||||
],
|
||||
[
|
||||
'title' => 'MathML',
|
||||
'payload' => "<div id=\"130\"><math href=\"javascript:alert(130)\">CLICKME</math>\n<math>\n<!-- up to FF 13 -->\n<maction actiontype=\"statusline#http://google.com\" xlink:href=\"javascript:alert(2)\">CLICKME</maction>\n\n<!-- FF 14+ -->\n<maction actiontype=\"statusline\" xlink:href=\"javascript:alert(3)\">CLICKME<mtext>http://http://google.com</mtext></maction>\n</math>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"130\">CLICKME\n\n\nCLICKME\n\n\nCLICKMEhttp://http://google.com\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"132\"><!doctype html>\n<form>\n<label>type a,b,c,d - watch the network tab/traffic (JS is off, latest NoScript)</label>\n<br>\n<input name=\"secret\" type=\"password\">\n</form>\n<!-- injection --><svg height=\"50px\">\n<image xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n<set attributeName=\"xlink:href\" begin=\"accessKey(a)\" to=\"//example.com/?a\" />\n<set attributeName=\"xlink:href\" begin=\"accessKey(b)\" to=\"//example.com/?b\" />\n<set attributeName=\"xlink:href\" begin=\"accessKey(c)\" to=\"//example.com/?c\" />\n<set attributeName=\"xlink:href\" begin=\"accessKey(d)\" to=\"//example.com/?d\" />\n</image>\n</svg>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="132">',
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"133\"><!-- `<img/src=xxx onerror=alert(133)//--!>//[\"'`-->]]>]</div>",
|
||||
'expected' => '<div id="133">',
|
||||
],
|
||||
[
|
||||
'title' => 'XMP',
|
||||
'payload' => "<div id=\"134\"><xmp>\n<%\n</xmp>\n<img alt='%></xmp><img src=xx onerror=alert(134)//'>\n\n<script>\nx='<%'\n</script> %>/\nalert(2)\n</script>\n\nXXX\n<style>\n*['<!--']{}\n</style>\n-->{}\n*{color:red}</style>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"134\">\n",
|
||||
],
|
||||
[
|
||||
'title' => 'SVG',
|
||||
'payload' => "<div id=\"135\"><?xml-stylesheet type=\"text/xsl\" href=\"#\" ?>\n<stylesheet xmlns=\"http://www.w3.org/TR/WD-xsl\">\n<template match=\"/\">\n<eval>new ActiveXObject('htmlfile').parentWindow.alert(135)</eval>\n<if expr=\"new ActiveXObject('htmlfile').parentWindow.alert(2)\"></if>\n</template>\n</stylesheet>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"135\">>?xml-stylesheet type="text/xsl" href="#" ?<\n\n<template>\nnew ActiveXObject('htmlfile').parentWindow.alert(135)\n\n</template>\n",
|
||||
],
|
||||
[
|
||||
'title' => '',
|
||||
'payload' => "<div id=\"136\"><form action=\"x\" method=\"post\">\n<input name=\"username\" value=\"admin\" />\n<input name=\"password\" type=\"password\" value=\"secret\" />\n<input name=\"injected\" value=\"injected\" dirname=\"password\" />\n<input type=\"submit\">\n</form>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"136\"><form action=\"x\" method=\"post\">\n<input name=\"username\" value=\"admin\" />\n<input name=\"password\" type=\"password\" value=\"secret\" />\n<input name=\"injected\" value=\"injected\" />\n<input type=\"submit\" />\n",
|
||||
],
|
||||
[
|
||||
'title' => 'SVG',
|
||||
'payload' => "<div id=\"137\"><svg>\n<a xmlns:xlink=\"http://www.w3.org/1999/xlink\" xlink:href=\"?\">\n<circle r=\"400\"></circle>\n<animate attributeName=\"xlink:href\" begin=\"0\" from=\"javascript:alert(137)\" to=\"&\" />\n</a>//[\"'`-->]]>]</div>",
|
||||
'expected' => "<div id=\"137\">\n<a>\n\n\n</a>",
|
||||
],
|
||||
[
|
||||
'title' => 'Removing name attr from img with id can crash Safari',
|
||||
'payload' => '<img name="bar" id="foo">',
|
||||
'expected' => '<img name="bar" id="foo" />',
|
||||
],
|
||||
[
|
||||
'title' => 'DOM clobbering: submit',
|
||||
'payload' => '<input name=submit>123',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'DOM clobbering: acceptCharset',
|
||||
'payload' => '<input name=acceptCharset>123',
|
||||
'expected' => '',
|
||||
],
|
||||
[
|
||||
'title' => 'Testing support for sizes and srcset',
|
||||
'payload' => '<img src="small.jpg" srcset="medium.jpg 1000w, large.jpg 2000w">',
|
||||
'expected' => '<img src="small.jpg" srcset="medium.jpg 1000w, large.jpg 2000w" />',
|
||||
],
|
||||
[
|
||||
'title' => "See #264 and Edge's weird attribute name errors",
|
||||
'payload' => '<div =""></div>',
|
||||
'expected' => '',
|
||||
],
|
||||
];
|
|
@ -0,0 +1,285 @@
|
|||
<?php
|
||||
// test suite from https://code.iamcal.com/php/rfc822/tests/
|
||||
// Commented expected values are those that PHP filter_var() failed to validate/unvalidate
|
||||
$emailTest = [
|
||||
['payload' => 'first.last@iana.org', 'expected' => true],
|
||||
['payload' => '1234567890123456789012345678901234567890123456789012345678901234@iana.org', 'expected' => true],
|
||||
['payload' => 'first.last@sub.do,com', 'expected' => false],
|
||||
['payload' => '"first\"last"@iana.org', 'expected' => true],
|
||||
['payload' => 'first\@last@iana.org', 'expected' => false],
|
||||
['payload' => '"first@last"@iana.org', 'expected' => true],
|
||||
['payload' => '"first\\last"@iana.org', 'expected' => true],
|
||||
['payload' => 'x@x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x2', 'expected' => true],
|
||||
['payload' => '1234567890123456789012345678901234567890123456789012345678@12345678901234567890123456789012345678901234567890123456789.12345678901234567890123456789012345678901234567890123456789.123456789012345678901234567890123456789012345678901234567890123.iana.org', 'expected' => true],
|
||||
['payload' => 'first.last@[12.34.56.78]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:::12.34.56.78]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:1111:2222:3333::4444:12.34.56.78]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:1111:2222:3333:4444:5555:6666:12.34.56.78]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:::1111:2222:3333:4444:5555:6666]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:1111:2222:3333::4444:5555:6666]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:1111:2222:3333:4444:5555:6666::]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:1111:2222:3333:4444:5555:6666:7777:8888]', 'expected' => true],
|
||||
['payload' => 'first.last@x23456789012345678901234567890123456789012345678901234567890123.iana.org', 'expected' => true],
|
||||
['payload' => 'first.last@3com.com', 'expected' => true],
|
||||
['payload' => 'first.last@123.iana.org', 'expected' => true],
|
||||
['payload' => '123456789012345678901234567890123456789012345678901234567890@12345678901234567890123456789012345678901234567890123456789.12345678901234567890123456789012345678901234567890123456789.12345678901234567890123456789012345678901234567890123456789.12345.iana.org', 'expected' => false],
|
||||
['payload' => 'first.last', 'expected' => false],
|
||||
['payload' => '12345678901234567890123456789012345678901234567890123456789012345@iana.org', 'expected' => false],
|
||||
['payload' => '.first.last@iana.org', 'expected' => false],
|
||||
['payload' => 'first.last.@iana.org', 'expected' => false],
|
||||
['payload' => 'first..last@iana.org', 'expected' => false],
|
||||
['payload' => '"first"last"@iana.org', 'expected' => false],
|
||||
['payload' => '"first\last"@iana.org', 'expected' => true],
|
||||
['payload' => '"""@iana.org', 'expected' => false],
|
||||
['payload' => '"\"@iana.org', 'expected' => false],
|
||||
['payload' => '""@iana.org', 'expected' => true], // [30] false),
|
||||
['payload' => 'first\\@last@iana.org', 'expected' => false],
|
||||
['payload' => 'first.last@', 'expected' => false],
|
||||
['payload' => 'x@x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456789.x23456', 'expected' => false],
|
||||
['payload' => 'first.last@[.12.34.56.78]', 'expected' => false],
|
||||
['payload' => 'first.last@[12.34.56.789]', 'expected' => false],
|
||||
['payload' => 'first.last@[::12.34.56.78]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv5:::12.34.56.78]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:1111:2222:3333::4444:5555:12.34.56.78]', 'expected' => false], // [38] true),
|
||||
['payload' => 'first.last@[IPv6:1111:2222:3333:4444:5555:12.34.56.78]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:1111:2222:3333:4444:5555:6666:7777:12.34.56.78]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:1111:2222:3333:4444:5555:6666:7777]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:1111:2222:3333:4444:5555:6666:7777:8888:9999]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:1111:2222::3333::4444:5555:6666]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:1111:2222:3333::4444:5555:6666:7777]', 'expected' => false], // [44] true),
|
||||
['payload' => 'first.last@[IPv6:1111:2222:333x::4444:5555]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:1111:2222:33333::4444:5555]', 'expected' => false],
|
||||
['payload' => 'first.last@example.123', 'expected' => false], // [47] true),
|
||||
['payload' => 'first.last@com', 'expected' => false], // [48] true),
|
||||
['payload' => 'first.last@-xample.com', 'expected' => false],
|
||||
['payload' => 'first.last@exampl-.com', 'expected' => false],
|
||||
['payload' => 'first.last@x234567890123456789012345678901234567890123456789012345678901234.iana.org', 'expected' => false],
|
||||
['payload' => '"Abc\@def"@iana.org', 'expected' => true],
|
||||
['payload' => '"Fred\ Bloggs"@iana.org', 'expected' => true],
|
||||
['payload' => '"Joe.\\Blow"@iana.org', 'expected' => true],
|
||||
['payload' => '"Abc@def"@iana.org', 'expected' => true],
|
||||
['payload' => '"Fred Bloggs"@iana.org', 'expected' => false], // [56] true),
|
||||
['payload' => 'user+mailbox@iana.org', 'expected' => true],
|
||||
['payload' => 'customer/department=shipping@iana.org', 'expected' => true],
|
||||
['payload' => '$A12345@iana.org', 'expected' => true],
|
||||
['payload' => '!def!xyz%abc@iana.org', 'expected' => true],
|
||||
['payload' => '_somename@iana.org', 'expected' => true],
|
||||
['payload' => 'dclo@us.ibm.com', 'expected' => true],
|
||||
['payload' => 'abc\@def@iana.org', 'expected' => false],
|
||||
['payload' => 'abc\\@iana.org', 'expected' => false],
|
||||
['payload' => 'peter.piper@iana.org', 'expected' => true],
|
||||
['payload' => 'Doug\ \"Ace\"\ Lovell@iana.org', 'expected' => false],
|
||||
['payload' => '"Doug \"Ace\" L."@iana.org', 'expected' => false], // [67] true),
|
||||
['payload' => 'abc@def@iana.org', 'expected' => false],
|
||||
['payload' => 'abc\\@def@iana.org', 'expected' => false],
|
||||
['payload' => 'abc\@iana.org', 'expected' => false],
|
||||
['payload' => '@iana.org', 'expected' => false],
|
||||
['payload' => 'doug@', 'expected' => false],
|
||||
['payload' => '"qu@iana.org', 'expected' => false],
|
||||
['payload' => 'ote"@iana.org', 'expected' => false],
|
||||
['payload' => '.dot@iana.org', 'expected' => false],
|
||||
['payload' => 'dot.@iana.org', 'expected' => false],
|
||||
['payload' => 'two..dot@iana.org', 'expected' => false],
|
||||
['payload' => '"Doug "Ace" L."@iana.org', 'expected' => false],
|
||||
['payload' => 'Doug\ \"Ace\"\ L\.@iana.org', 'expected' => false],
|
||||
['payload' => 'hello world@iana.org', 'expected' => false],
|
||||
['payload' => 'gatsby@f.sc.ot.t.f.i.tzg.era.l.d.', 'expected' => false],
|
||||
['payload' => 'test@iana.org', 'expected' => true],
|
||||
['payload' => 'TEST@iana.org', 'expected' => true],
|
||||
['payload' => '1234567890@iana.org', 'expected' => true],
|
||||
['payload' => 'test+test@iana.org', 'expected' => true],
|
||||
['payload' => 'test-test@iana.org', 'expected' => true],
|
||||
['payload' => 't*est@iana.org', 'expected' => true],
|
||||
['payload' => '+1~1+@iana.org', 'expected' => true],
|
||||
['payload' => '{_test_}@iana.org', 'expected' => true],
|
||||
['payload' => '"[[ test ]]"@iana.org', 'expected' => false], // [90] true),
|
||||
['payload' => 'test.test@iana.org', 'expected' => true],
|
||||
['payload' => '"test.test"@iana.org', 'expected' => true],
|
||||
['payload' => 'test."test"@iana.org', 'expected' => true],
|
||||
['payload' => '"test@test"@iana.org', 'expected' => true],
|
||||
['payload' => 'test@123.123.123.x123', 'expected' => true],
|
||||
['payload' => 'test@123.123.123.123', 'expected' => false], // [96] true),
|
||||
['payload' => 'test@[123.123.123.123]', 'expected' => true],
|
||||
['payload' => 'test@example.iana.org', 'expected' => true],
|
||||
['payload' => 'test@example.example.iana.org', 'expected' => true],
|
||||
['payload' => 'test.iana.org', 'expected' => false],
|
||||
['payload' => 'test.@iana.org', 'expected' => false],
|
||||
['payload' => 'test..test@iana.org', 'expected' => false],
|
||||
['payload' => '.test@iana.org', 'expected' => false],
|
||||
['payload' => 'test@test@iana.org', 'expected' => false],
|
||||
['payload' => 'test@@iana.org', 'expected' => false],
|
||||
['payload' => '-- test --@iana.org', 'expected' => false],
|
||||
['payload' => '[test]@iana.org', 'expected' => false],
|
||||
['payload' => '"test\test"@iana.org', 'expected' => true],
|
||||
['payload' => '"test"test"@iana.org', 'expected' => false],
|
||||
['payload' => '()[]\;:,><@iana.org', 'expected' => false],
|
||||
['payload' => 'test@.', 'expected' => false],
|
||||
['payload' => 'test@example.', 'expected' => false],
|
||||
['payload' => 'test@.org', 'expected' => false],
|
||||
['payload' => 'test@123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012.com', 'expected' => false],
|
||||
['payload' => 'test@example', 'expected' => false], // [115] true),
|
||||
['payload' => 'test@[123.123.123.123', 'expected' => false],
|
||||
['payload' => 'test@123.123.123.123]', 'expected' => false],
|
||||
['payload' => 'NotAnEmail', 'expected' => false],
|
||||
['payload' => '@NotAnEmail', 'expected' => false],
|
||||
['payload' => '"test\\blah"@iana.org', 'expected' => true],
|
||||
['payload' => '"test\blah"@iana.org', 'expected' => true],
|
||||
['payload' => '"test\ blah"@iana.org', 'expected' => true],
|
||||
['payload' => '"test blah"@iana.org', 'expected' => true], // [123] false),
|
||||
['payload' => '"test\"blah"@iana.org', 'expected' => true],
|
||||
['payload' => '"test"blah"@iana.org', 'expected' => false],
|
||||
['payload' => 'customer/department@iana.org', 'expected' => true],
|
||||
['payload' => '_Yosemite.Sam@iana.org', 'expected' => true],
|
||||
['payload' => '~@iana.org', 'expected' => true],
|
||||
['payload' => '.wooly@iana.org', 'expected' => false],
|
||||
['payload' => 'wo..oly@iana.org', 'expected' => false],
|
||||
['payload' => 'pootietang.@iana.org', 'expected' => false],
|
||||
['payload' => '.@iana.org', 'expected' => false],
|
||||
['payload' => '"Austin@Powers"@iana.org', 'expected' => true],
|
||||
['payload' => 'Ima.Fool@iana.org', 'expected' => true],
|
||||
['payload' => '"Ima.Fool"@iana.org', 'expected' => true],
|
||||
['payload' => '"Ima Fool"@iana.org', 'expected' => false], // [136] true),
|
||||
['payload' => 'Ima Fool@iana.org', 'expected' => false],
|
||||
['payload' => 'phil.h\@\@ck@haacked.com', 'expected' => false],
|
||||
['payload' => '"first"."last"@iana.org', 'expected' => true],
|
||||
['payload' => '"first".middle."last"@iana.org', 'expected' => true],
|
||||
['payload' => '"first\\"last"@iana.org', 'expected' => true], // [141] false),
|
||||
['payload' => '"first".last@iana.org', 'expected' => true],
|
||||
['payload' => 'first."last"@iana.org', 'expected' => true],
|
||||
['payload' => '"first"."middle"."last"@iana.org', 'expected' => true],
|
||||
['payload' => '"first.middle"."last"@iana.org', 'expected' => true],
|
||||
['payload' => '"first.middle.last"@iana.org', 'expected' => true],
|
||||
['payload' => '"first..last"@iana.org', 'expected' => true],
|
||||
['payload' => 'foo@[\1.2.3.4]', 'expected' => false],
|
||||
['payload' => '"first\\\"last"@iana.org', 'expected' => false], // [149] true),
|
||||
['payload' => 'first."mid\dle"."last"@iana.org', 'expected' => true],
|
||||
['payload' => 'Test. Folding. Whitespace@iana.org', 'expected' => false], // [151] true),
|
||||
['payload' => 'first."".last@iana.org', 'expected' => true], // [152] false),
|
||||
['payload' => 'first\last@iana.org', 'expected' => false],
|
||||
['payload' => 'Abc\@def@iana.org', 'expected' => false],
|
||||
['payload' => 'Fred\ Bloggs@iana.org', 'expected' => false],
|
||||
['payload' => 'Joe.\\Blow@iana.org', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:1111:2222:3333:4444:5555:6666:12.34.567.89]', 'expected' => false],
|
||||
['payload' => '"test\ blah"@iana.org', 'expected' => false],
|
||||
['payload' => '"test blah"@iana.org', 'expected' => false], // [159] true),
|
||||
['payload' => '{^c\@**Dog^}@cartoon.com', 'expected' => false],
|
||||
['payload' => '(foo)cal(bar)@(baz)iamcal.com(quux)', 'expected' => false], // [161] true),
|
||||
['payload' => 'cal@iamcal(woo).(yay)com', 'expected' => false], // [162] true),
|
||||
['payload' => '"foo"(yay)@(hoopla)[1.2.3.4]', 'expected' => false],
|
||||
['payload' => 'cal(woo(yay)hoopla)@iamcal.com', 'expected' => false], // [164] true),
|
||||
['payload' => 'cal(foo\@bar)@iamcal.com', 'expected' => false], // [165] true),
|
||||
['payload' => 'cal(foo\)bar)@iamcal.com', 'expected' => false], // [166] true),
|
||||
['payload' => 'cal(foo(bar)@iamcal.com', 'expected' => false],
|
||||
['payload' => 'cal(foo)bar)@iamcal.com', 'expected' => false],
|
||||
['payload' => 'cal(foo\)@iamcal.com', 'expected' => false],
|
||||
['payload' => 'first().last@iana.org', 'expected' => false], // [170] true),
|
||||
['payload' => 'first.( middle )last@iana.org', 'expected' => false], // [171] true),
|
||||
['payload' => 'first(12345678901234567890123456789012345678901234567890)last@(1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890)iana.org', 'expected' => false],
|
||||
['payload' => 'first(Welcome to the ("wonderful" (!)) world of email)@iana.org', 'expected' => false], // [173] true),
|
||||
['payload' => 'pete(his account)@silly.test(his host)', 'expected' => false], // [174] true),
|
||||
['payload' => 'c@(Chris\'s host.)public.example', 'expected' => false], // [175] true),
|
||||
['payload' => 'jdoe@machine(comment). example', 'expected' => false], // [176] true),
|
||||
['payload' => '1234 @ local(blah) .machine .example', 'expected' => false], // [177] true),
|
||||
['payload' => 'first(middle)last@iana.org', 'expected' => false],
|
||||
['payload' => 'first(abc.def).last@iana.org', 'expected' => false], // [179] true),
|
||||
['payload' => 'first(a"bc.def).last@iana.org', 'expected' => false], // [180] true),
|
||||
['payload' => 'first.(")middle.last(")@iana.org', 'expected' => false], // [181] true),
|
||||
['payload' => 'first(abc("def".ghi).mno)middle(abc("def".ghi).mno).last@(abc("def".ghi).mno)example(abc("def".ghi).mno).(abc("def".ghi).mno)com(abc("def".ghi).mno)', 'expected' => false],
|
||||
['payload' => 'first(abc\(def)@iana.org', 'expected' => false], // [183] true),
|
||||
['payload' => 'first.last@x(1234567890123456789012345678901234567890123456789012345678901234567890).com', 'expected' => false], // [184] true),
|
||||
['payload' => 'a(a(b(c)d(e(f))g)h(i)j)@iana.org', 'expected' => false], // [185] true),
|
||||
['payload' => 'a(a(b(c)d(e(f))g)(h(i)j)@iana.org', 'expected' => false],
|
||||
['payload' => 'name.lastname@domain.com', 'expected' => true],
|
||||
['payload' => '.@', 'expected' => false],
|
||||
['payload' => 'a@b', 'expected' => false], // [189] true),
|
||||
['payload' => '@bar.com', 'expected' => false],
|
||||
['payload' => '@@bar.com', 'expected' => false],
|
||||
['payload' => 'a@bar.com', 'expected' => true],
|
||||
['payload' => 'aaa.com', 'expected' => false],
|
||||
['payload' => 'aaa@.com', 'expected' => false],
|
||||
['payload' => 'aaa@.123', 'expected' => false],
|
||||
['payload' => 'aaa@[123.123.123.123]', 'expected' => true],
|
||||
['payload' => 'aaa@[123.123.123.123]a', 'expected' => false],
|
||||
['payload' => 'aaa@[123.123.123.333]', 'expected' => false],
|
||||
['payload' => 'a@bar.com.', 'expected' => false],
|
||||
['payload' => 'a@bar', 'expected' => false], // [200] true),
|
||||
['payload' => 'a-b@bar.com', 'expected' => true],
|
||||
['payload' => '+@b.c', 'expected' => true],
|
||||
['payload' => '+@b.com', 'expected' => true],
|
||||
['payload' => 'a@-b.com', 'expected' => false],
|
||||
['payload' => 'a@b-.com', 'expected' => false],
|
||||
['payload' => '-@..com', 'expected' => false],
|
||||
['payload' => '-@a..com', 'expected' => false],
|
||||
['payload' => 'a@b.co-foo.uk', 'expected' => true],
|
||||
['payload' => '"hello my name is"@stutter.com', 'expected' => false], // [209] true),
|
||||
['payload' => '"Test \"Fail\" Ing"@iana.org', 'expected' => false], // [210] true),
|
||||
['payload' => 'valid@about.museum', 'expected' => true],
|
||||
['payload' => 'invalid@about.museum-', 'expected' => false],
|
||||
['payload' => 'shaitan@my-domain.thisisminekthx', 'expected' => true],
|
||||
['payload' => 'test@...........com', 'expected' => false],
|
||||
['payload' => 'foobar@192.168.0.1', 'expected' => false], // [215] true),
|
||||
['payload' => '"Joe\\Blow"@iana.org', 'expected' => true],
|
||||
['payload' => 'Invalid \ Folding \ Whitespace@iana.org', 'expected' => false],
|
||||
['payload' => 'HM2Kinsists@(that comments are allowed)this.is.ok', 'expected' => false], // [218] true),
|
||||
['payload' => 'user%uucp!path@berkeley.edu', 'expected' => true],
|
||||
['payload' => '"first(last)"@iana.org', 'expected' => true],
|
||||
['payload' => ' ( x ) first ( x ) . ( x) last ( x ) @iana.org', 'expected' => false], // [221] true),
|
||||
['payload' => 'first.last @iana.org', 'expected' => false], // [222] true),
|
||||
['payload' => 'test. obs@syntax.com', 'expected' => false], // [223] true),
|
||||
['payload' => 'test. obs@syntax.com', 'expected' => false],
|
||||
['payload' => '"Unicode NULL \␀"@char.com', 'expected' => false], // [225] true),
|
||||
['payload' => '"Unicode NULL ␀"@char.com', 'expected' => false],
|
||||
['payload' => 'Unicode NULL \␀@char.com', 'expected' => false],
|
||||
['payload' => 'cdburgess+!#$%&\'*-/=?+_{}|~test@gmail.com', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:::a2:a3:a4:b1:b2:b3:b4]', 'expected' => false], // [229] true),
|
||||
['payload' => 'first.last@[IPv6:a1:a2:a3:a4:b1:b2:b3::]', 'expected' => false], // [230] true),
|
||||
['payload' => 'first.last@[IPv6::]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:::]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6::::]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6::b4]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:::b4]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6::::b4]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6::b3:b4]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:::b3:b4]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6::::b3:b4]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1::b4]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:a1:::b4]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1:]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1::]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:a1:::]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1:a2:]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1:a2::]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:a1:a2:::]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:0123:4567:89ab:cdef::]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:0123:4567:89ab:CDEF::]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:::a3:a4:b1:ffff:11.22.33.44]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:::a2:a3:a4:b1:ffff:11.22.33.44]', 'expected' => false], // [251] true),
|
||||
['payload' => 'first.last@[IPv6:a1:a2:a3:a4::11.22.33.44]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:a1:a2:a3:a4:b1::11.22.33.44]', 'expected' => false], // [253] true),
|
||||
['payload' => 'first.last@[IPv6::11.22.33.44]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6::::11.22.33.44]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1:11.22.33.44]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1::11.22.33.44]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:a1:::11.22.33.44]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1:a2::11.22.33.44]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:a1:a2:::11.22.33.44]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:0123:4567:89ab:cdef::11.22.33.44]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:0123:4567:89ab:cdef::11.22.33.xx]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:0123:4567:89ab:CDEF::11.22.33.44]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:0123:4567:89ab:CDEFF::11.22.33.44]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1::a4:b1::b4:11.22.33.44]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1::11.22.33]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1::11.22.33.44.55]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1::b211.22.33.44]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1::b2:11.22.33.44]', 'expected' => true],
|
||||
['payload' => 'first.last@[IPv6:a1::b2::11.22.33.44]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1::b3:]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6::a2::b4]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1:a2:a3:a4:b1:b2:b3:]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6::a2:a3:a4:b1:b2:b3:b4]', 'expected' => false],
|
||||
['payload' => 'first.last@[IPv6:a1:a2:a3:a4::b1:b2:b3:b4]', 'expected' => false],
|
||||
['payload' => 'test@test.com', 'expected' => true],
|
||||
['payload' => 'test@example.com ', 'expected' => false],
|
||||
['payload' => 'test@xn--example.com', 'expected' => true],
|
||||
['payload' => 'test@Bücher.ch', 'expected' => false] // [279] true)
|
||||
];
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue