diff --git a/README.md b/README.md index e5f8ead0..9a87769b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Requirements PHP 8.0 is explicitly supported. PHP 7.x should be compatable. 2. MySQL/MariaDB server >= 5.5.3 3. [Composer](https://getcomposer.org/) (To install various packages) -4. [mbstring](http://www.php.net/manual/en/mbstring.installation.php) +4. [mbstring](http://www.php.net/manual/en/mbstring.installation.php) 5. [PHP GD](http://www.php.net/manual/en/intro.image.php) 6. [PHP PDO](http://www.php.net/manual/en/intro.pdo.php) @@ -44,7 +44,7 @@ Installation development version with: git clone git://git.leftypol.org/leftypol/leftypol.git - + 2. run ```composer install``` inside the directory 3. Navigate to ```install.php``` in your web browser and follow the prompts. @@ -80,7 +80,7 @@ find support from a variety of sources: * For support, reply to the sticky on our [/tech/](https://leftypol.org/tech/) board. ### Tinyboard support -vichan, and by extension lainchan and leftypol, is based on a Tinyboard, so both engines have very much in common. These links may be helpful for you as well: +vichan, and by extension lainchan and leftypol, is based on a Tinyboard, so both engines have very much in common. These links may be helpful for you as well: * Tinyboard documentation can be found [here](https://web.archive.org/web/20121016074303/http://tinyboard.org/docs/?p=Main_Page). @@ -120,7 +120,10 @@ leftypol API leftypol provides by default a 4chan-compatible JSON API, just like vichan. For documentation on this, see: https://github.com/vichan-devel/vichan-API/ . +Testing +---------- +You can run PHPUnit tests with `./vendor/bin/phpunit tests` with PHP 7.4 to 8.2. + License -------- See LICENSE.md. - diff --git a/composer.json b/composer.json index 21fff51a..bf0f7f42 100644 --- a/composer.json +++ b/composer.json @@ -5,14 +5,14 @@ "require": { "twig/twig": "^1.44.2", "lifo/ip": "^1.0", - "gettext/gettext": "^1.0", + "gettext/gettext": "^5.7", "mrclay/minify": "^2.1.6" }, "autoload": { "classmap": ["inc/"], "files": [ - "inc/anti-bot.php", "inc/bootstrap.php", + "inc/anti-bot.php", "inc/context.php", "inc/display.php", "inc/template.php", @@ -47,5 +47,8 @@ "name": "leftypol contributors", "homepage": "https://git.leftypol.org/leftypol/leftypol/" } - ] + ], + "require-dev": { + "phpunit/phpunit": "^9" + } } diff --git a/composer.lock b/composer.lock index 46f82a2a..3089f79a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,37 +4,42 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "346d80deda89b0298a414b565213f312", + "content-hash": "d529eeba68d215cb209fb7a2e425923b", "packages": [ { "name": "gettext/gettext", - "version": "v1.1.5", + "version": "v5.7.3", "source": { "type": "git", "url": "https://github.com/php-gettext/Gettext.git", - "reference": "1bdf755a1b49f0614d6fc29f446df567eb62cd5c" + "reference": "95820f020e4f2f05e0bbaa5603e4c6ec3edc50f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-gettext/Gettext/zipball/1bdf755a1b49f0614d6fc29f446df567eb62cd5c", - "reference": "1bdf755a1b49f0614d6fc29f446df567eb62cd5c", + "url": "https://api.github.com/repos/php-gettext/Gettext/zipball/95820f020e4f2f05e0bbaa5603e4c6ec3edc50f1", + "reference": "95820f020e4f2f05e0bbaa5603e4c6ec3edc50f1", "shasum": "" }, "require": { - "php": ">=5.3.0" + "gettext/languages": "^2.3", + "php": "^7.2|^8.0" + }, + "require-dev": { + "brick/varexporter": "^0.3.5", + "friendsofphp/php-cs-fixer": "^3.2", + "oscarotero/php-cs-fixer-config": "^2.0", + "phpunit/phpunit": "^8.0|^9.0", + "squizlabs/php_codesniffer": "^3.0" }, "type": "library", "autoload": { - "psr-0": { - "Gettext": "" - }, - "files": [ - "Gettext/translator_functions.php" - ] + "psr-4": { + "Gettext\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "AGPL-3.0" + "MIT" ], "authors": [ { @@ -44,20 +49,111 @@ "role": "Developer" } ], - "description": "PHP - JS gettext conversor", - "homepage": "https://github.com/oscarotero/Gettext", + "description": "PHP gettext manager", + "homepage": "https://github.com/php-gettext/Gettext", "keywords": [ "JS", "gettext", "i18n", + "mo", + "po", "translation" ], "support": { "email": "oom@oscarotero.com", - "issues": "https://github.com/oscarotero/Gettext/issues", - "source": "https://github.com/php-gettext/Gettext/tree/v1.1.5" + "issues": "https://github.com/php-gettext/Gettext/issues", + "source": "https://github.com/php-gettext/Gettext/tree/v5.7.3" }, - "time": "2014-10-22T15:53:45+00:00" + "funding": [ + { + "url": "https://paypal.me/oscarotero", + "type": "custom" + }, + { + "url": "https://github.com/oscarotero", + "type": "github" + }, + { + "url": "https://www.patreon.com/misteroom", + "type": "patreon" + } + ], + "time": "2024-12-01T10:18:08+00:00" + }, + { + "name": "gettext/languages", + "version": "2.12.1", + "source": { + "type": "git", + "url": "https://github.com/php-gettext/Languages.git", + "reference": "0b0b0851c55168e1dfb14305735c64019732b5f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-gettext/Languages/zipball/0b0b0851c55168e1dfb14305735c64019732b5f1", + "reference": "0b0b0851c55168e1dfb14305735c64019732b5f1", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4" + }, + "bin": [ + "bin/export-plural-rules", + "bin/import-cldr-data" + ], + "type": "library", + "autoload": { + "psr-4": { + "Gettext\\Languages\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michele Locati", + "email": "mlocati@gmail.com", + "role": "Developer" + } + ], + "description": "gettext languages with plural rules", + "homepage": "https://github.com/php-gettext/Languages", + "keywords": [ + "cldr", + "i18n", + "internationalization", + "l10n", + "language", + "languages", + "localization", + "php", + "plural", + "plural rules", + "plurals", + "translate", + "translations", + "unicode" + ], + "support": { + "issues": "https://github.com/php-gettext/Languages/issues", + "source": "https://github.com/php-gettext/Languages/tree/2.12.1" + }, + "funding": [ + { + "url": "https://paypal.me/mlocati", + "type": "custom" + }, + { + "url": "https://github.com/mlocati", + "type": "github" + } + ], + "time": "2025-03-19T11:14:02+00:00" }, { "name": "lifo/ip", @@ -318,13 +414,1755 @@ "time": "2021-11-25T13:31:46+00:00" } ], - "packages-dev": [], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-02-12T12:17:51+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.4.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.32", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:23:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.22", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.12.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-12-05T13:48:26+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], + "platform": {}, + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/inc/Data/FiltersParseResult.php b/inc/Data/FiltersParseResult.php new file mode 100644 index 00000000..1eb1acd8 --- /dev/null +++ b/inc/Data/FiltersParseResult.php @@ -0,0 +1,13 @@ +> + */ + public array $body; + /** + * @var array + */ + public array $subject; + /** + * @var array + */ + public array $name; + /** + * @var array + */ + public array $board; + /** + * @var array + */ + public array $flag; + public ?int $id; + public ?int $thread; + public float $weight; +} diff --git a/inc/Data/SearchQueries.php b/inc/Data/SearchQueries.php index abf910fb..330df0b9 100644 --- a/inc/Data/SearchQueries.php +++ b/inc/Data/SearchQueries.php @@ -6,20 +6,25 @@ class SearchQueries { private \PDO $pdo; private int $queries_per_minutes_single; private int $queries_per_minutes_all; + private bool $auto_gc; + private function checkFloodImpl(string $ip, string $phrase): bool { - $now = time(); + $now = \time(); + $expiry_limit = \time() - ($this->queries_per_minutes_all * 60); - $query = $this->pdo->prepare("SELECT COUNT(*) FROM `search_queries` WHERE `ip` = :ip AND `time` > :time"); + $query = $this->pdo->prepare("SELECT COUNT(*) FROM `search_queries` WHERE `ip` = :ip AND `time` > :time AND `time` <= :expiry_limit"); $query->bindValue(':ip', $ip); - $query->bindValue(':time', $now - ($this->queries_per_minutes_single * 60)); + $query->bindValue(':time', $now - ($this->queries_per_minutes_single * 60), \PDO::PARAM_INT); + $query->bindValue(':expiry_limit', $expiry_limit, \PDO::PARAM_INT); $query->execute(); if ($query->fetchColumn() > $this->queries_per_minutes_single) { return false; } - $query = $this->pdo->prepare("SELECT COUNT(*) FROM `search_queries` WHERE `time` > :time"); - $query->bindValue(':time', $now - ($this->queries_per_minutes_all * 60)); + $query = $this->pdo->prepare("SELECT COUNT(*) FROM `search_queries` WHERE `time` > :time AND `time` <= :expiry_limit"); + $query->bindValue(':time', $now - ($this->queries_per_minutes_all * 60), \PDO::PARAM_INT); + $query->bindValue(':expiry_limit', $expiry_limit, \PDO::PARAM_INT); $query->execute(); if ($query->fetchColumn() > $this->queries_per_minutes_all) { return false; @@ -27,24 +32,31 @@ class SearchQueries { $query = $this->pdo->prepare("INSERT INTO `search_queries` VALUES (:ip, :time, :query)"); $query->bindValue(':ip', $ip); - $query->bindValue(':time', $now); + $query->bindValue(':time', $now, \PDO::PARAM_INT); $query->bindValue(':query', $phrase); $query->execute(); - // Cleanup search queries table - $query = prepare("DELETE FROM `search_queries` WHERE `time` <= :time"); - $query->bindValue(':time', time() - ($this->queries_per_minutes_all * 60)); - $query->execute(); + if ($this->auto_gc) { + $this->purgeExpired(); + } return true; } - public function __construct(\PDO $pdo, int $queries_per_minutes_single, int $queries_per_minutes_all) { + public function __construct(\PDO $pdo, int $queries_per_minutes_single, int $queries_per_minutes_all, bool $auto_gc) { $this->pdo = $pdo; $this->queries_per_minutes_single = $queries_per_minutes_single; $this->queries_per_minutes_all = $queries_per_minutes_all; + $this->auto_gc = $auto_gc; } + /** + * Check if the IP-query pair overflows the limit. + * + * @param string $ip Source IP. + * @param string $phrase The search query. + * @return bool True if the request goes over the limit + */ public function checkFlood(string $ip, string $phrase): bool { $this->pdo->beginTransaction(); try { @@ -56,4 +68,12 @@ class SearchQueries { throw $e; } } + + public function purgeExpired(): int { + // Cleanup search queries table. + $query = prepare("DELETE FROM `search_queries` WHERE `time` <= :expiry_limit"); + $query->bindValue(':expiry_limit', \time() - ($this->queries_per_minutes_all * 60), \PDO::PARAM_INT); + $query->execute(); + return $query->rowCount(); + } } diff --git a/inc/Data/UserPostQueries.php b/inc/Data/UserPostQueries.php index 1c203431..d863eb5f 100644 --- a/inc/Data/UserPostQueries.php +++ b/inc/Data/UserPostQueries.php @@ -13,6 +13,36 @@ class UserPostQueries { private \PDO $pdo; + + /** + * Escapes wildcards from LIKE operators using the default escape character. + */ + private static function escapeLike(string $str): string { + // Escape any existing escape characters. + $str = \str_replace('\\', '\\\\', $str); + // Escape wildcard characters. + $str = \str_replace('%', '\\%', $str); + $str = \str_replace('_', '\\_', $str); + return $str; + } + + /** + * Joins the fragments of filter into a single LIKE operand string. + * Given prefix = cat and fragments_count = 3, we get "%:cat0%:cat1%:cat2%" + * + * @param string $prefix The prefix for the parameter binding + * @param int $fragments_count MUST BE >= 1. + * @return string + */ + private static function joinFragments(string $prefix, int $fragments_count): string { + // With prefix = cat and fragments = 2 it becomes: "%:cat0%:cat1%" + $s = '%'; + for ($i = 0; $i < $fragments_count; $i++) { + $s .= ":$prefix$i%"; + } + return $s; + } + public function __construct(\PDO $pdo) { $this->pdo = $pdo; } @@ -156,4 +186,89 @@ class UserPostQueries { } }); } + + /** + * Search among the user posts with the given filters. + * The subject, name and elements of the bodies filters are fragments which are joined together with wildcards, to + * allow for more flexible filtering. + * + * @param string $board The board where to search in. + * @param array $subject Fragments of the subject filter. + * @param array $name Fragments of the name filter. + * @param array $flags An array of the flag names to search among the HTML. + * @param ?int $id Post id filter. + * @param ?int $thread Thread id filter. + * @param array> $bodies An array whose element are arrays containing the fragments of multiple body filters, each + * searched independently from the others + * @param integer $limit The maximum number of results. + * @throws PDOException On error. + * @return array + */ + public function searchPosts(string $board, array $subject, array $name, array $flags, ?int $id, ?int $thread, array $bodies, int $limit): array { + $where_acc = []; + + if (!empty($subject)) { + $like_op = self::joinFragments('subj', \count($subject)); + $where_acc[] = "WHERE subject LIKE '$like_op'"; + } + if (!empty($name)) { + $like_op = self::joinFragments('name', \count($name)); + $where_acc[] = "WHERE name LIKE '$like_op'"; + } + if (!empty($flags)) { + $flag_acc = []; + for ($i = 0; $i < \count($flags); $i++) { + // Yes, vichan stores the flag inside the generated HTML. Now you know why it's slow as shit. + // English lacks the words to express my feelings about it in a satisfying manner. + $flag_acc[] = "LIKE = '%:flag$i%'"; + } + $where_acc[] = 'WHERE body_nomarkup (' . \implode(' OR ', $flag_acc) . ')'; + } + if ($id !== null) { + $where_acc[] = 'WHERE id = :id'; + } + if ($thread !== null) { + $where_acc[] = 'WHERE thread = :thread'; + } + for ($i = 0; $i < \count($bodies); $i++) { + $body = $bodies[$i]; + $like_op = self::joinFragments("body_{$i}_", \count($body)); + $where_acc[] = "WHERE body LIKE '$like_op'"; + } + + if (empty($where_acc)) { + return []; + } + + $sql = "SELECT * FROM `posts_$board` " . \implode(' AND ', $where_acc) . ' LIMIT :limit'; + $query = $this->pdo->prepare($sql); + + for ($i = 0; $i < \count($subject); $i++) { + $query->bindValue(":subj$i", $subject[$i]); + } + for ($i = 0; $i < \count($name); $i++) { + $query->bindValue(":name$i", $name[$i]); + } + for ($i = 0; $i < \count($flags); $i++) { + $query->bindValue(":flag$i", $flags[$i]); + } + if ($id !== null) { + $query->bindValue(':id', $id, \PDO::PARAM_INT); + } + if ($thread !== null) { + $query->bindValue(':thread', $thread, \PDO::PARAM_INT); + } + for ($body_i = 0; $body_i < \count($bodies); $body_i++) { + $body = $bodies[$body_i]; + + for ($i = 0; $i < \count($body); $i++) { + $query->bindValue(":body_{$body_i}_{$i}", $body[$i]); + } + } + + $query->bindValue(':limit', $limit, \PDO::PARAM_INT); + + $query->execute(); + return $query->fetchAll(\PDO::FETCH_ASSOC); + } } diff --git a/inc/Polyfill/GettextWrapper.php b/inc/Polyfill/GettextWrapper.php new file mode 100644 index 00000000..36e924ac --- /dev/null +++ b/inc/Polyfill/GettextWrapper.php @@ -0,0 +1,71 @@ +loadFile($moFile); + + // Optionally convert to codeset if set (not enforced by the library, just stored) + self::$translations[$domain][$locale] = $translations; + } + } + + $translations = self::$translations[$domain][$locale]; + + if ($translations instanceof Translations) { + $entry = $translations->find(null, $message); + if ($entry === null) { + return $message; + } + return $entry->getTranslation(); + } + + return $message; + } +} diff --git a/inc/Service/SearchService.php b/inc/Service/SearchService.php new file mode 100644 index 00000000..49694ffb --- /dev/null +++ b/inc/Service/SearchService.php @@ -0,0 +1,392 @@ + '\\', + '\\*' => '*', + '\\"' => '"' + ]); + } + + /** + * Split the filter into fragments along the wildcards, handling escaping. + * + * @param string $str The full filter. + * @return array + */ + private static function split(string $str): array { + // Split the fragments + return \preg_split('/(?:\\\\\\\\)*\\\\\*|(?:\\\\\\\\)*\*+/', $str); + } + + private static function weightByContent(array $fragments): float { + $w = 0; + + foreach ($fragments as $fragment) { + $short = \strlen($fragment) < 4; + if (\in_array($fragment, self::COMMON_WORDS)) { + $w += $short ? 16 : 6; + } elseif ($short) { + $w += 6; + } + } + + return $w; + } + + private static function filterAndWeight(string $filter): array { + $fragments = self::split($filter); + $acc = []; + $total_len = 0; + + foreach ($fragments as $fragment) { + $fragment = self::trim(self::unescape($fragment)); + + if (!empty($fragment)) { + $total_len += \strlen($fragment); + $acc[] = $fragment; + } + } + + // Interword wildcards + $interword = \min(\count($fragments) - 1, 0); + // Wildcards over the total length of the word. Ergo the number of fragments minus 1. + $perc = $interword / $total_len * 100; + $wildcard_weight = $perc + \count($fragments) * 2; + + return [ $acc, $total_len, $wildcard_weight ]; + } + + /** + * Gets a subset of the flags which match every filter. + * + * @param array $fragments User provided fragments to search in the flags. + * @param array $flags An array of flags. + * @return array An array of flags + */ + private static function matchFlags(array $flags, array $fragments): array { + return \array_filter($flags, function ($str) use ($fragments) { + // Saves the last position. We use this to ensure the fragments are one after the other. + $last_ret = 0; + foreach ($fragments as $fragment) { + if ($last_ret + 1 > \strlen($fragment)) { + // Cannot possibly match. + return false; + } + + $last_ret = \stripos($str, $fragment, $last_ret + 1); + if ($last_ret === false) { + // Exclude flags that don't much even a single fragment. + return false; + } + } + return true; + }); + } + + /** + * Parses a raw search query. + * + * @param string $raw_query Raw user query. Phrases are searched in the post bodies. The user can specify also + * additional filters in the : format. + * Available filters: + * - board: the board, value can be quoted + * - subject: post subject, value can be quoted, supports wildcards + * - name: post name, value can be quoted, supports wildcards + * - flag: post flag, value can be quoted, supports wildcards + * - id: post id, must be numeric + * - thread: thread id, must be numeric + * The remaining text is split into chunks and searched in the post body. + * @return FiltersParseResult + */ + public function parse(string $raw_query): FiltersParseResult{ + $tres = self::truncateQuery($raw_query, $this->max_query_length); + if ($tres === null) { + throw new \RuntimeException('Could not truncate query'); + } + + $pres = \preg_match_all( + '/(?: + \b(board): + (?: + "([^"]+)" # [2] board: "quoted" + | + ([^\s"]+) # [3] board: unquoted + ) + | + \b(subject|name|flag): + (?: + "((?:\\\\\\\\|\\\\\"|\\\\\*|[^"\\\\])*)" # [5] quoted with wildcards + | + ((?:\\\\\\\\|\\\\\*|[^\s\\\\])+) # [6] unquoted with wildcards + ) + | + \b(id|thread): + (\d+) # [8] numeric only + | + "((?:\\\\\\\\|\\\\\"|\\\\\*|[^"\\\\])*)" # [9] quoted free text + | + ([^"\s]+(?:\s+(?!\b(?:board|subject|name|flag|id|thread):)[^"\s]+)*) # [10] unquoted free text block + )/iux', + $tres, + $matches, + \PREG_SET_ORDER + ); + if ($pres === false) { + throw new \RuntimeException('Could not decode the query'); + } + + $filters = new FiltersParseResult(); + + foreach ($matches as $m) { + if (!empty($m[1])) { + // board (no wildcards). + $value = \trim(!empty($m[2]) ? $m[2] : $m[3], '/'); + + $filters->board = $value; + } elseif (!empty($m[4])) { + // subject, name, flag (with wildcards). + $key = \strtolower($m[4]); + $value = !empty($m[5]) ? $m[5] : $m[6]; + + if ($key === 'name') { + $filters->name = $value; + } elseif ($key === 'subject') { + $filters->subject = $value; + } else { + $filters->flag = $value; + } + } elseif (!empty($m[7])) { + $key = \strtolower($m[7]); + $value = (int)$m[8]; + + if ($key === 'id') { + $filters->id = $value; + } else { + $filters->thread = $value; + } + } elseif (!empty($m[9]) || !empty($m[10])) { + $value = !empty($m[9]) ? $m[9] : $m[10]; + + $filters->body[] = $value; + } + } + + return $filters; + } + + /** + * @param LogDriver $log Log river. + * @param UserPostQueries $user_queries User posts queries. + * @param SearchQueries $search_queries Search queries for flood detection. + * @param ?array $flag_map The key-value map of user flags, or null to disable flag search. + * @param float $max_weight The maximum weight of the parsed user query. Body filters that go beyond this limit are discarded. + * @param int $max_query_length Maximum length of the raw input query before it's truncated. + * @param int $post_limit Maximum number of results. + * @param ?array $searchable_board_uris The uris of the board that can be searched. Null to search all the boards. + */ + public function __construct( + LogDriver $log, + UserPostQueries $user_queries, + ?array $flag_map, + float $max_weight, + int $max_query_length, + int $post_limit, + ?array $searchable_board_uris + ) { + $this->log = $log; + $this->user_queries = $user_queries; + $this->flag_map = $flag_map; + $this->max_weight = $max_weight; + $this->max_query_length = $max_query_length; + $this->post_limit = $post_limit; + $this->searchable_board_uris = $searchable_board_uris ?? listBoards(true); + } + + /** + * Reduces the user provided filters and assigns them a total weight. + * + * @param FiltersParseResult $filters The filters to sanitize, reduce and weight. + * @return SearchFilters + */ + public function reduceAndWeight(FiltersParseResult $filters): SearchFilters { + $weighted = new SearchFilters(); + + if ($filters->subject !== null) { + list($fragments, $total_len, $wildcard_weight) = self::filterAndWeight($filters->subject); + + if ($total_len > self::MAX_LENGTH_SUBJECT) { + $weighted->subject = []; + } else { + $weighted->subject = $fragments; + $weighted->weight = $wildcard_weight; + } + } + if ($filters->name !== null) { + list($fragments, $total_len, $wildcard_weight) = self::filterAndWeight($filters->name); + + if ($total_len > self::MAX_LENGTH_NAME) { + $weighted->name = []; + } else { + $weighted->name = $fragments; + $weighted->weight += $wildcard_weight; + } + } + if ($filters->flag !== null) { + $weighted->flag = []; + + if ($this->flag_map !== null && !empty($this->flag_map)) { + $max_flag_length = \array_reduce($this->flag_map, fn($max, $str) => \max($max, \strlen($str)), 0); + + list($fragments, $total_len, $wildcard_weight) = self::filterAndWeight($filters->flag); + + // Add 2 to account for possible wildcards on the ends. + if ($total_len <= $max_flag_length + 2) { + $weighted->flag = $fragments; + $weighted->weight += $wildcard_weight; + } + } + } + $weighted->id = $filters->id; + $weighted->thread = $filters->thread; + if ($filters->body !== null) { + foreach ($filters->body as $str) { + list($fragments, $total_len, $wildcard_weight) = self::filterAndWeight($str); + $content_weight = self::weightByContent($fragments); + $str_weight = $content_weight + $wildcard_weight; + + if ($str_weight + $weighted->weight <= $this->max_weight) { + $weighted->weight += $str_weight; + $filters->body[] = $fragments; + } + } + } + + return $weighted; + } + + /** + * Run a search on user posts with the given filters. + * + * @param SearchFilters $filters An array of filters made by {@see self::parse()}. + * @param ?string $fallback_board Fallback board if there isn't a board filter. + * @return array Data array straight from the PDO, with all the fields in posts.sql + */ + public function search(string $ip, string $raw_query, SearchFilters $filters, ?string $fallback_board): array { + $board = $filters->board ?? $fallback_board; + if ($board === null) { + return []; + } + + if (!\in_array($board, $this->searchable_board_uris)) { + return []; + } + + $weight_perc = ($filters->weight / $this->max_weight) * 100; + if ($weight_perc > 85) { + /// Over 85 of the weight. + $this->log->log(LogDriver::NOTICE, "$ip search: weight $weight_perc ({$filters->weight}) query '$raw_query'"); + } else { + $this->log->log(LogDriver::INFO, "$ip search: weight $weight_perc ({$filters->weight}) query '$raw_query'"); + } + + $flags = $filters->flag !== null ? $this->matchFlags($this->flag_map, $filters->flag) : null; + + return $this->user_queries->searchPosts( + $board, + $filters->subject, + $filters->name, + $flags, + $filters->id, + $filters->thread, + $filters->body, + $this->post_limit + ); + } + + /** + * Check if the IP-query pair passes the limit. + * + * @param string $ip Source IP. + * @param string $phrase The search query. + * @return bool True if the request goes over the limit. + */ + public function checkFlood(string $ip, string $raw_query) { + return $this->search_queries->checkFlood($ip, $raw_query); + } + + /** + * Returns the uris of the boards that may be searched. + */ + public function getSearchableBoards(): array { + return $this->searchable_board_uris; + } +} diff --git a/inc/config.php b/inc/config.php index fb4b4493..2f43f614 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1856,7 +1856,15 @@ // Limit of search results $config['search']['search_limit'] = 100; - // Boards for searching + // Maximum weigth of the search query. + // Body search filters are discarded if they make the query heavier than this. + $config['search']['max_weight'] = 80; + + // Maximum length of the user sent search query. + // Characters beyond the limit are truncated and ignored. + $config['search']['max_length'] = 768; + + // Uncomment to limit the search feature to the given boards by uri. //$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e'); // Enable public logs? 0: NO, 1: YES, 2: YES, but drop names diff --git a/inc/context.php b/inc/context.php index 11a153ec..63b6fb9d 100644 --- a/inc/context.php +++ b/inc/context.php @@ -1,8 +1,10 @@ function($c) { + $config = $c->get('config'); + if ($config['user_flag']) { + $flags = $config['user_flags']; + } elseif ($config['country_flags']) { + $flags = Flags::EMBEDDED_FLAGS; + } else { + $flags = null; + } + + $board_uris = $config['search']['boards'] ?? null; + + return new SearchService( + $c->get(LogDriver::class), + $c->get(UserPostQueries::class), + $flags, + $config['search']['max_weight'], + $config['search']['max_length'], + $config['search']['search_limit'], + $board_uris + ); + }, ReportQueries::class => function($c) { $auto_maintenance = (bool)$c->get('config')['auto_maintenance']; $pdo = $c->get(\PDO::class); @@ -78,5 +102,14 @@ function build_context(array $config): Context { return new UserPostQueries($c->get(\PDO::class)); }, IpNoteQueries::class => fn($c) => new IpNoteQueries($c->get(\PDO::class), $c->get(CacheDriver::class)), + SearchQueries::class => function($c) { + $config = $c->get('config'); + return new SearchQueries( + $c->get(\PDO::class), + $config['search']['queries_per_minutes'], + $config['search']['queries_per_minutes_all'], + $config['auto_maintenance'] + ); + } ]); } diff --git a/inc/functions.php b/inc/functions.php index c9f6657e..3252786e 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -21,27 +21,14 @@ mb_internal_encoding('UTF-8'); loadConfig(); function init_locale($locale) { - if (extension_loaded('gettext')) { - if (setlocale(LC_ALL, $locale) === false) { - //$error('The specified locale (' . $locale . ') does not exist on your platform!'); - // Fall back to C.UTF-8 instead of normal C, so we support unicode instead of just ASCII - setlocale(LC_ALL, "C.UTF-8"); - setlocale(LC_CTYPE, "C.UTF-8"); - } - bindtextdomain('tinyboard', './inc/locale'); - bind_textdomain_codeset('tinyboard', 'UTF-8'); - textdomain('tinyboard'); - } else { - if (_setlocale(LC_ALL, $locale) === false) { - error('The specified locale (' . $locale . ') does not exist on your platform!'); - // Fall back to C.UTF-8 instead of normal C, so we support unicode instead of just ASCII - _setlocale(LC_ALL, "C.UTF-8"); - _setlocale(LC_CTYPE, "C.UTF-8"); - } - _bindtextdomain('tinyboard', './inc/locale'); - _bind_textdomain_codeset('tinyboard', 'UTF-8'); - _textdomain('tinyboard'); + if (setlocale(LC_ALL, $locale) === false) { + // Fall back to C.UTF-8 instead of normal C, so we support unicode instead of just ASCII + setlocale(LC_ALL, "C.UTF-8"); + setlocale(LC_CTYPE, "C.UTF-8"); } + bindtextdomain('tinyboard', './inc/locale'); + bind_textdomain_codeset('tinyboard', 'UTF-8'); + textdomain('tinyboard'); } $current_locale = 'en'; @@ -113,8 +100,9 @@ function loadConfig() { $config[$key] = array(); } - if (!file_exists('inc/instance-config.php')) + if (!file_exists('inc/instance-config.php') && !\defined('PHPUNIT_COMPOSER_INSTALL')) { error('Tinyboard is not configured! Create inc/instance-config.php.'); + } // Initialize locale as early as possible @@ -149,7 +137,9 @@ function loadConfig() { require 'inc/config.php'; - require 'inc/instance-config.php'; + if (file_exists('inc/instance-config.php') && !\defined('PHPUNIT_COMPOSER_INSTALL')) { + require 'inc/instance-config.php'; + } if (isset($board['dir']) && file_exists($board['dir'] . '/config.php')) { require $board['dir'] . '/config.php'; diff --git a/inc/lib/gettext/AUTHORS b/inc/lib/gettext/AUTHORS deleted file mode 100644 index da6ade7b..00000000 --- a/inc/lib/gettext/AUTHORS +++ /dev/null @@ -1,3 +0,0 @@ -Danilo Segan -Nico Kaiser (contributed most changes between 1.0.2 and 1.0.3, bugfix for 1.0.5) -Steven Armstrong (gettext.inc, leading to 1.0.6) diff --git a/inc/lib/gettext/COPYING b/inc/lib/gettext/COPYING deleted file mode 100644 index 5b6e7c66..00000000 --- a/inc/lib/gettext/COPYING +++ /dev/null @@ -1,340 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Library General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program 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. - - This program 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 this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Library General -Public License instead of this License. diff --git a/inc/lib/gettext/Makefile b/inc/lib/gettext/Makefile deleted file mode 100644 index f389de51..00000000 --- a/inc/lib/gettext/Makefile +++ /dev/null @@ -1,38 +0,0 @@ -PACKAGE = php-gettext-$(VERSION) -VERSION = 1.0.11 - -DIST_FILES = \ - gettext.php \ - gettext.inc \ - streams.php \ - AUTHORS \ - README \ - COPYING \ - Makefile \ - examples/index.php \ - examples/pigs_dropin.php \ - examples/pigs_fallback.php \ - examples/locale/sr_CS/LC_MESSAGES/messages.po \ - examples/locale/sr_CS/LC_MESSAGES/messages.mo \ - examples/locale/de_CH/LC_MESSAGES/messages.po \ - examples/locale/de_CH/LC_MESSAGES/messages.mo \ - examples/update \ - tests/LocalesTest.php \ - tests/ParsingTest.php - -check: - phpunit --verbose tests - -dist: check - if [ -d $(PACKAGE) ]; then \ - rm -rf $(PACKAGE); \ - fi; \ - mkdir $(PACKAGE); \ - if [ -d $(PACKAGE) ]; then \ - cp -rp --parents $(DIST_FILES) $(PACKAGE); \ - tar cvzf $(PACKAGE).tar.gz $(PACKAGE); \ - rm -rf $(PACKAGE); \ - fi; - -clean: - rm -f $(PACKAGE).tar.gz diff --git a/inc/lib/gettext/README b/inc/lib/gettext/README deleted file mode 100644 index bca4f916..00000000 --- a/inc/lib/gettext/README +++ /dev/null @@ -1,161 +0,0 @@ -PHP-gettext 1.0 (https://launchpad.net/php-gettext) - -Copyright 2003, 2006, 2009 -- Danilo "angry with PHP[1]" Segan -Licensed under GPLv2 (or any later version, see COPYING) - -[1] PHP is actually cyrillic, and translates roughly to - "works-doesn't-work" (UTF-8: Ради-Не-Ради) - - -Introduction - - How many times did you look for a good translation tool, and - found out that gettext is best for the job? Many times. - - How many times did you try to use gettext in PHP, but failed - miserably, because either your hosting provider didn't support - it, or the server didn't have adequate locale? Many times. - - Well, this is a solution to your needs. It allows using gettext - tools for managing translations, yet it doesn't require gettext - library at all. It parses generated MO files directly, and thus - might be a bit slower than the (maybe provided) gettext library. - - PHP-gettext is a simple reader for GNU gettext MO files. Those - are binary containers for translations, produced by GNU msgfmt. - -Why? - - I got used to having gettext work even without gettext - library. It's there in my favourite language Python, so I was - surprised that I couldn't find it in PHP. I even Googled for it, - but to no avail. - - So, I said, what the heck, I'm going to write it for this - disguisting language of PHP, because I'm often constrained to it. - -Features - - o Support for simple translations - Just define a simple alias for translate() function (suggested - use of _() or gettext(); see provided example). - - o Support for ngettext calls (plural forms, see a note under bugs) - You may also use plural forms. Translations in MO files need to - provide this, and they must also provide "plural-forms" header. - Please see 'info gettext' for more details. - - o Support for reading straight files, or strings (!!!) - Since I can imagine many different backends for reading in the MO - file data, I used imaginary abstract class StreamReader to do all - the input (check streams.php). For your convenience, I've already - provided two classes for reading files: FileReader and - StringReader (CachedFileReader is a combination of the two: it - loads entire file contents into a string, and then works on that). - See example below for usage. You can for instance use StringReader - when you read in data from a database, or you can create your own - derivative of StreamReader for anything you like. - - -Bugs - - Report them on https://bugs.launchpad.net/php-gettext - -Usage - - Put files streams.php and gettext.php somewhere you can load them - from, and require 'em in where you want to use them. - - Then, create one 'stream reader' (a class that provides functions - like read(), seekto(), currentpos() and length()) which will - provide data for the 'gettext_reader', with eg. - $streamer = new FileStream('data.mo'); - - Then, use that as a parameter to gettext_reader constructor: - $wohoo = new gettext_reader($streamer); - - If you want to disable pre-loading of entire message catalog in - memory (if, for example, you have a multi-thousand message catalog - which you'll use only occasionally), use "false" for second - parameter to gettext_reader constructor: - $wohoo = new gettext_reader($streamer, false); - - From now on, you have all the benefits of gettext data at your - disposal, so may run: - print $wohoo->translate("This is a test"); - print $wohoo->ngettext("%d bird", "%d birds", $birds); - - You might need to pass parameter "-k" to xgettext to make it - extract all the strings. In above example, try with - xgettext -ktranslate -kngettext:1,2 file.php - what should create messages.po which contains two messages for - translation. - - I suggest creating simple aliases for these functions (see - example/pigs.php for how do I do it, which means it's probably a - bad way). - - -Usage with gettext.inc (standard gettext interfaces emulation) - - Check example in examples/pig_dropin.php, basically you include - gettext.inc and use all the standard gettext interfaces as - documented on: - - http://www.php.net/gettext - - The only catch is that you can check return value of setlocale() - to see if your locale is system supported or not. - - -Example - - See in examples/ subdirectory. There are a couple of files. - pigs.php is an example, serbian.po is a translation to Serbian - language, and serbian.mo is generated with - msgfmt -o serbian.mo serbian.po - There is also simple "update" script that can be used to generate - POT file and to update the translation using msgmerge. - -TODO: - - o Improve speed to be even more comparable to the native gettext - implementation. - - o Try to use hash tables in MO files: with pre-loading, would it - be useful at all? - -Never-asked-questions: - - o Why did you mark this as version 1.0 when this is the first code - release? - - Well, it's quite simple. I consider that the first released thing - should be labeled "version 1" (first, right?). Zero is there to - indicate that there's zero improvement and/or change compared to - "version 1". - - I plan to use version numbers 1.0.* for small bugfixes, and to - release 1.1 as "first stable release of version 1". - - This may trick someone that this is actually useful software, but - as with any other free software, I take NO RESPONSIBILITY for - creating such a masterpiece that will smoke crack, trash your - hard disk, and make lasers in your CD device dance to the tune of - Mozart's 40th Symphony (there is one like that, right?). - - o Can I...? - - Yes, you can. This is free software (as in freedom, free speech), - and you might do whatever you wish with it, provided you do not - limit freedom of others (GPL). - - I'm considering licensing this under LGPL, but I *do* want - *every* PHP-gettext user to contribute and respect ideas of free - software, so don't count on it happening anytime soon. - - I'm sorry that I'm taking away your freedom of taking others' - freedom away, but I believe that's neglible as compared to what - freedoms you could take away. ;-) - - Uhm, whatever. diff --git a/inc/lib/gettext/gettext.inc b/inc/lib/gettext/gettext.inc deleted file mode 100644 index cb21fea5..00000000 --- a/inc/lib/gettext/gettext.inc +++ /dev/null @@ -1,536 +0,0 @@ - - Copyright (c) 2009 Danilo Segan - - Drop in replacement for native gettext. - - This file is part of PHP-gettext. - - PHP-gettext 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. - - PHP-gettext 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 PHP-gettext; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -*/ -/* -LC_CTYPE 0 -LC_NUMERIC 1 -LC_TIME 2 -LC_COLLATE 3 -LC_MONETARY 4 -LC_MESSAGES 5 -LC_ALL 6 -*/ - -// LC_MESSAGES is not available if php-gettext is not loaded -// while the other constants are already available from session extension. -if (!defined('LC_MESSAGES')) { - define('LC_MESSAGES', 5); -} - -require(dirname(__FILE__) . '/streams.php'); -require(dirname(__FILE__) . '/gettext.php'); - - -// Variables - -global $text_domains, $default_domain, $LC_CATEGORIES, $EMULATEGETTEXT, $CURRENTLOCALE; -$text_domains = array(); -$default_domain = 'messages'; -$LC_CATEGORIES = array('LC_CTYPE', 'LC_NUMERIC', 'LC_TIME', 'LC_COLLATE', 'LC_MONETARY', 'LC_MESSAGES', 'LC_ALL'); -$EMULATEGETTEXT = 0; -$CURRENTLOCALE = ''; - -/* Class to hold a single domain included in $text_domains. */ -class domain { - var $l10n; - var $path; - var $codeset; -} - -// Utility functions - -/** - * Return a list of locales to try for any POSIX-style locale specification. - */ -function get_list_of_locales($locale) { - /* Figure out all possible locale names and start with the most - * specific ones. I.e. for sr_CS.UTF-8@latin, look through all of - * sr_CS.UTF-8@latin, sr_CS@latin, sr@latin, sr_CS.UTF-8, sr_CS, sr. - */ - $locale_names = array(); - $lang = NULL; - $country = NULL; - $charset = NULL; - $modifier = NULL; - if ($locale) { - if (preg_match("/^(?P[a-z]{2,3})" // language code - ."(?:_(?P[A-Z]{2}))?" // country code - ."(?:\.(?P[-A-Za-z0-9_]+))?" // charset - ."(?:@(?P[-A-Za-z0-9_]+))?$/", // @ modifier - $locale, $matches)) { - - if (isset($matches["lang"])) $lang = $matches["lang"]; - if (isset($matches["country"])) $country = $matches["country"]; - if (isset($matches["charset"])) $charset = $matches["charset"]; - if (isset($matches["modifier"])) $modifier = $matches["modifier"]; - - if ($modifier) { - if ($country) { - if ($charset) - array_push($locale_names, "${lang}_$country.$charset@$modifier"); - array_push($locale_names, "${lang}_$country@$modifier"); - } elseif ($charset) - array_push($locale_names, "${lang}.$charset@$modifier"); - array_push($locale_names, "$lang@$modifier"); - } - if ($country) { - if ($charset) - array_push($locale_names, "${lang}_$country.$charset"); - array_push($locale_names, "${lang}_$country"); - } elseif ($charset) - array_push($locale_names, "${lang}.$charset"); - array_push($locale_names, $lang); - } - - // If the locale name doesn't match POSIX style, just include it as-is. - if (!in_array($locale, $locale_names)) - array_push($locale_names, $locale); - } - return $locale_names; -} - -/** - * Utility function to get a StreamReader for the given text domain. - */ -function _get_reader($domain=null, $category=5, $enable_cache=true) { - global $text_domains, $default_domain, $LC_CATEGORIES; - if (!isset($domain)) $domain = $default_domain; - if (!isset($text_domains[$domain]->l10n)) { - // get the current locale - $locale = _setlocale(LC_MESSAGES, 0); - $bound_path = isset($text_domains[$domain]->path) ? - $text_domains[$domain]->path : './'; - $subpath = $LC_CATEGORIES[$category] ."/$domain.mo"; - - $locale_names = get_list_of_locales($locale); - $input = null; - foreach ($locale_names as $locale) { - $full_path = $bound_path . $locale . "/" . $subpath; - if (file_exists($full_path)) { - $input = new FileReader($full_path); - break; - } - } - - if (!array_key_exists($domain, $text_domains)) { - // Initialize an empty domain object. - $text_domains[$domain] = new domain(); - } - $text_domains[$domain]->l10n = new gettext_reader($input, - $enable_cache); - } - return $text_domains[$domain]->l10n; -} - -/** - * Returns whether we are using our emulated gettext API or PHP built-in one. - */ -function locale_emulation() { - global $EMULATEGETTEXT; - return $EMULATEGETTEXT; -} - -/** - * Checks if the current locale is supported on this system. - */ -function _check_locale_and_function($function=false) { - global $EMULATEGETTEXT; - if ($function and !function_exists($function)) - return false; - return !$EMULATEGETTEXT; -} - -/** - * Get the codeset for the given domain. - */ -function _get_codeset($domain=null) { - global $text_domains, $default_domain, $LC_CATEGORIES; - if (!isset($domain)) $domain = $default_domain; - return (isset($text_domains[$domain]->codeset))? $text_domains[$domain]->codeset : ini_get('mbstring.internal_encoding'); -} - -/** - * Convert the given string to the encoding set by bind_textdomain_codeset. - */ -function _encode($text) { - $source_encoding = mb_detect_encoding($text); - $target_encoding = _get_codeset(); - if ($source_encoding != $target_encoding) { - return mb_convert_encoding($text, $target_encoding, $source_encoding); - } - else { - return $text; - } -} - - -// Custom implementation of the standard gettext related functions - -/** - * Returns passed in $locale, or environment variable $LANG if $locale == ''. - */ -function _get_default_locale($locale) { - if ($locale == '') // emulate variable support - return getenv('LANG'); - else - return $locale; -} - -/** - * Sets a requested locale, if needed emulates it. - */ -function _setlocale($category, $locale) { - global $CURRENTLOCALE, $EMULATEGETTEXT; - if ($locale === 0) { // use === to differentiate between string "0" - if ($CURRENTLOCALE != '') - return $CURRENTLOCALE; - else - // obey LANG variable, maybe extend to support all of LC_* vars - // even if we tried to read locale without setting it first - return _setlocale($category, $CURRENTLOCALE); - } else { - if (function_exists('setlocale')) { - $ret = setlocale($category, $locale); - if (($locale == '' and !$ret) or // failed setting it by env - ($locale != '' and $ret != $locale)) { // failed setting it - // Failed setting it according to environment. - $CURRENTLOCALE = _get_default_locale($locale); - $EMULATEGETTEXT = 1; - } else { - $CURRENTLOCALE = $ret; - $EMULATEGETTEXT = 0; - } - } else { - // No function setlocale(), emulate it all. - $CURRENTLOCALE = _get_default_locale($locale); - $EMULATEGETTEXT = 1; - } - // Allow locale to be changed on the go for one translation domain. - global $text_domains, $default_domain; - if (array_key_exists($default_domain, $text_domains)) { - unset($text_domains[$default_domain]->l10n); - } - return $CURRENTLOCALE; - } -} - -/** - * Sets the path for a domain. - */ -function _bindtextdomain($domain, $path) { - global $text_domains; - // ensure $path ends with a slash ('/' should work for both, but lets still play nice) - if (substr(php_uname(), 0, 7) == "Windows") { - if ($path[strlen($path)-1] != '\\' and $path[strlen($path)-1] != '/') - $path .= '\\'; - } else { - if ($path[strlen($path)-1] != '/') - $path .= '/'; - } - if (!array_key_exists($domain, $text_domains)) { - // Initialize an empty domain object. - $text_domains[$domain] = new domain(); - } - $text_domains[$domain]->path = $path; -} - -/** - * Specify the character encoding in which the messages from the DOMAIN message catalog will be returned. - */ -function _bind_textdomain_codeset($domain, $codeset) { - global $text_domains; - $text_domains[$domain]->codeset = $codeset; -} - -/** - * Sets the default domain. - */ -function _textdomain($domain) { - global $default_domain; - $default_domain = $domain; -} - -/** - * Lookup a message in the current domain. - */ -function _gettext($msgid) { - $l10n = _get_reader(); - return _encode($l10n->translate($msgid)); -} - -/** - * Alias for gettext. - */ -function __($msgid) { - return _gettext($msgid); -} - -/** - * Plural version of gettext. - */ -function _ngettext($singular, $plural, $number) { - $l10n = _get_reader(); - return _encode($l10n->ngettext($singular, $plural, $number)); -} - -/** - * Override the current domain. - */ -function _dgettext($domain, $msgid) { - $l10n = _get_reader($domain); - return _encode($l10n->translate($msgid)); -} - -/** - * Plural version of dgettext. - */ -function _dngettext($domain, $singular, $plural, $number) { - $l10n = _get_reader($domain); - return _encode($l10n->ngettext($singular, $plural, $number)); -} - -/** - * Overrides the domain and category for a single lookup. - */ -function _dcgettext($domain, $msgid, $category) { - $l10n = _get_reader($domain, $category); - return _encode($l10n->translate($msgid)); -} -/** - * Plural version of dcgettext. - */ -function _dcngettext($domain, $singular, $plural, $number, $category) { - $l10n = _get_reader($domain, $category); - return _encode($l10n->ngettext($singular, $plural, $number)); -} - -/** - * Context version of gettext. - */ -function _pgettext($context, $msgid) { - $l10n = _get_reader(); - return _encode($l10n->pgettext($context, $msgid)); -} - -/** - * Override the current domain in a context gettext call. - */ -function _dpgettext($domain, $context, $msgid) { - $l10n = _get_reader($domain); - return _encode($l10n->pgettext($context, $msgid)); -} - -/** - * Overrides the domain and category for a single context-based lookup. - */ -function _dcpgettext($domain, $context, $msgid, $category) { - $l10n = _get_reader($domain, $category); - return _encode($l10n->pgettext($context, $msgid)); -} - -/** - * Context version of ngettext. - */ -function _npgettext($context, $singular, $plural) { - $l10n = _get_reader(); - return _encode($l10n->npgettext($context, $singular, $plural)); -} - -/** - * Override the current domain in a context ngettext call. - */ -function _dnpgettext($domain, $context, $singular, $plural) { - $l10n = _get_reader($domain); - return _encode($l10n->npgettext($context, $singular, $plural)); -} - -/** - * Overrides the domain and category for a plural context-based lookup. - */ -function _dcnpgettext($domain, $context, $singular, $plural, $category) { - $l10n = _get_reader($domain, $category); - return _encode($l10n->npgettext($context, $singular, $plural)); -} - - - -// Wrappers to use if the standard gettext functions are available, -// but the current locale is not supported by the system. -// Use the standard impl if the current locale is supported, use the -// custom impl otherwise. - -function T_setlocale($category, $locale) { - return _setlocale($category, $locale); -} - -function T_bindtextdomain($domain, $path) { - if (_check_locale_and_function()) return bindtextdomain($domain, $path); - else return _bindtextdomain($domain, $path); -} -function T_bind_textdomain_codeset($domain, $codeset) { - // bind_textdomain_codeset is available only in PHP 4.2.0+ - if (_check_locale_and_function('bind_textdomain_codeset')) - return bind_textdomain_codeset($domain, $codeset); - else return _bind_textdomain_codeset($domain, $codeset); -} -function T_textdomain($domain) { - if (_check_locale_and_function()) return textdomain($domain); - else return _textdomain($domain); -} -function T_gettext($msgid) { - if (_check_locale_and_function()) return gettext($msgid); - else return _gettext($msgid); -} -function T_($msgid) { - if (_check_locale_and_function()) return _($msgid); - return __($msgid); -} -function T_ngettext($singular, $plural, $number) { - if (_check_locale_and_function()) - return ngettext($singular, $plural, $number); - else return _ngettext($singular, $plural, $number); -} -function T_dgettext($domain, $msgid) { - if (_check_locale_and_function()) return dgettext($domain, $msgid); - else return _dgettext($domain, $msgid); -} -function T_dngettext($domain, $singular, $plural, $number) { - if (_check_locale_and_function()) - return dngettext($domain, $singular, $plural, $number); - else return _dngettext($domain, $singular, $plural, $number); -} -function T_dcgettext($domain, $msgid, $category) { - if (_check_locale_and_function()) - return dcgettext($domain, $msgid, $category); - else return _dcgettext($domain, $msgid, $category); -} -function T_dcngettext($domain, $singular, $plural, $number, $category) { - if (_check_locale_and_function()) - return dcngettext($domain, $singular, $plural, $number, $category); - else return _dcngettext($domain, $singular, $plural, $number, $category); -} - -function T_pgettext($context, $msgid) { - if (_check_locale_and_function('pgettext')) - return pgettext($context, $msgid); - else - return _pgettext($context, $msgid); -} - -function T_dpgettext($domain, $context, $msgid) { - if (_check_locale_and_function('dpgettext')) - return dpgettext($domain, $context, $msgid); - else - return _dpgettext($domain, $context, $msgid); -} - -function T_dcpgettext($domain, $context, $msgid, $category) { - if (_check_locale_and_function('dcpgettext')) - return dcpgettext($domain, $context, $msgid, $category); - else - return _dcpgettext($domain, $context, $msgid, $category); -} - -function T_npgettext($context, $singular, $plural, $number) { - if (_check_locale_and_function('npgettext')) - return npgettext($context, $singular, $plural, $number); - else - return _npgettext($context, $singular, $plural, $number); -} - -function T_dnpgettext($domain, $context, $singular, $plural, $number) { - if (_check_locale_and_function('dnpgettext')) - return dnpgettext($domain, $context, $singular, $plural, $number); - else - return _dnpgettext($domain, $context, $singular, $plural, $number); -} - -function T_dcnpgettext($domain, $context, $singular, $plural, - $number, $category) { - if (_check_locale_and_function('dcnpgettext')) - return dcnpgettext($domain, $context, $singular, - $plural, $number, $category); - else - return _dcnpgettext($domain, $context, $singular, - $plural, $number, $category); -} - - - -// Wrappers used as a drop in replacement for the standard gettext functions - -if (!function_exists('gettext')) { - function bindtextdomain($domain, $path) { - return _bindtextdomain($domain, $path); - } - function bind_textdomain_codeset($domain, $codeset) { - return _bind_textdomain_codeset($domain, $codeset); - } - function textdomain($domain) { - return _textdomain($domain); - } - function gettext($msgid) { - return _gettext($msgid); - } - function _($msgid) { - return __($msgid); - } - function ngettext($singular, $plural, $number) { - return _ngettext($singular, $plural, $number); - } - function dgettext($domain, $msgid) { - return _dgettext($domain, $msgid); - } - function dngettext($domain, $singular, $plural, $number) { - return _dngettext($domain, $singular, $plural, $number); - } - function dcgettext($domain, $msgid, $category) { - return _dcgettext($domain, $msgid, $category); - } - function dcngettext($domain, $singular, $plural, $number, $category) { - return _dcngettext($domain, $singular, $plural, $number, $category); - } - function pgettext($context, $msgid) { - return _pgettext($context, $msgid); - } - function npgettext($context, $singular, $plural, $number) { - return _npgettext($context, $singular, $plural, $number); - } - function dpgettext($domain, $context, $msgid) { - return _dpgettext($domain, $context, $msgid); - } - function dnpgettext($domain, $context, $singular, $plural, $number) { - return _dnpgettext($domain, $context, $singular, $plural, $number); - } - function dcpgettext($domain, $context, $msgid, $category) { - return _dcpgettext($domain, $context, $msgid, $category); - } - function dcnpgettext($domain, $context, $singular, $plural, - $number, $category) { - return _dcnpgettext($domain, $context, $singular, $plural, - $number, $category); - } -} - -?> diff --git a/inc/lib/gettext/gettext.php b/inc/lib/gettext/gettext.php deleted file mode 100755 index 5064047c..00000000 --- a/inc/lib/gettext/gettext.php +++ /dev/null @@ -1,432 +0,0 @@ -. - Copyright (c) 2005 Nico Kaiser - - This file is part of PHP-gettext. - - PHP-gettext 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. - - PHP-gettext 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 PHP-gettext; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -*/ - -/** - * Provides a simple gettext replacement that works independently from - * the system's gettext abilities. - * It can read MO files and use them for translating strings. - * The files are passed to gettext_reader as a Stream (see streams.php) - * - * This version has the ability to cache all strings and translations to - * speed up the string lookup. - * While the cache is enabled by default, it can be switched off with the - * second parameter in the constructor (e.g. whenusing very large MO files - * that you don't want to keep in memory) - */ -class gettext_reader { - //public: - var $error = 0; // public variable that holds error code (0 if no error) - - //private: - var $BYTEORDER = 0; // 0: low endian, 1: big endian - var $STREAM = NULL; - var $short_circuit = false; - var $enable_cache = false; - var $originals = NULL; // offset of original table - var $translations = NULL; // offset of translation table - var $pluralheader = NULL; // cache header field for plural forms - var $total = 0; // total string count - var $table_originals = NULL; // table for original strings (offsets) - var $table_translations = NULL; // table for translated strings (offsets) - var $cache_translations = NULL; // original -> translation mapping - - - /* Methods */ - - - /** - * Reads a 32bit Integer from the Stream - * - * @access private - * @return Integer from the Stream - */ - function readint() { - if ($this->BYTEORDER == 0) { - // low endian - $input=unpack('V', $this->STREAM->read(4)); - return array_shift($input); - } else { - // big endian - $input=unpack('N', $this->STREAM->read(4)); - return array_shift($input); - } - } - - function read($bytes) { - return $this->STREAM->read($bytes); - } - - /** - * Reads an array of Integers from the Stream - * - * @param int count How many elements should be read - * @return Array of Integers - */ - function readintarray($count) { - if ($this->BYTEORDER == 0) { - // low endian - return unpack('V'.$count, $this->STREAM->read(4 * $count)); - } else { - // big endian - return unpack('N'.$count, $this->STREAM->read(4 * $count)); - } - } - - /** - * Constructor - * - * @param object Reader the StreamReader object - * @param boolean enable_cache Enable or disable caching of strings (default on) - */ - function gettext_reader($Reader, $enable_cache = true) { - // If there isn't a StreamReader, turn on short circuit mode. - if (! $Reader || isset($Reader->error) ) { - $this->short_circuit = true; - return; - } - - // Caching can be turned off - $this->enable_cache = $enable_cache; - - $MAGIC1 = "\x95\x04\x12\xde"; - $MAGIC2 = "\xde\x12\x04\x95"; - - $this->STREAM = $Reader; - $magic = $this->read(4); - if ($magic == $MAGIC1) { - $this->BYTEORDER = 1; - } elseif ($magic == $MAGIC2) { - $this->BYTEORDER = 0; - } else { - $this->error = 1; // not MO file - return false; - } - - // FIXME: Do we care about revision? We should. - $revision = $this->readint(); - - $this->total = $this->readint(); - $this->originals = $this->readint(); - $this->translations = $this->readint(); - } - - /** - * Loads the translation tables from the MO file into the cache - * If caching is enabled, also loads all strings into a cache - * to speed up translation lookups - * - * @access private - */ - function load_tables() { - if (is_array($this->cache_translations) && - is_array($this->table_originals) && - is_array($this->table_translations)) - return; - - /* get original and translations tables */ - if (!is_array($this->table_originals)) { - $this->STREAM->seekto($this->originals); - $this->table_originals = $this->readintarray($this->total * 2); - } - if (!is_array($this->table_translations)) { - $this->STREAM->seekto($this->translations); - $this->table_translations = $this->readintarray($this->total * 2); - } - - if ($this->enable_cache) { - $this->cache_translations = array (); - /* read all strings in the cache */ - for ($i = 0; $i < $this->total; $i++) { - $this->STREAM->seekto($this->table_originals[$i * 2 + 2]); - $original = $this->STREAM->read($this->table_originals[$i * 2 + 1]); - $this->STREAM->seekto($this->table_translations[$i * 2 + 2]); - $translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]); - $this->cache_translations[$original] = $translation; - } - } - } - - /** - * Returns a string from the "originals" table - * - * @access private - * @param int num Offset number of original string - * @return string Requested string if found, otherwise '' - */ - function get_original_string($num) { - $length = $this->table_originals[$num * 2 + 1]; - $offset = $this->table_originals[$num * 2 + 2]; - if (! $length) - return ''; - $this->STREAM->seekto($offset); - $data = $this->STREAM->read($length); - return (string)$data; - } - - /** - * Returns a string from the "translations" table - * - * @access private - * @param int num Offset number of original string - * @return string Requested string if found, otherwise '' - */ - function get_translation_string($num) { - $length = $this->table_translations[$num * 2 + 1]; - $offset = $this->table_translations[$num * 2 + 2]; - if (! $length) - return ''; - $this->STREAM->seekto($offset); - $data = $this->STREAM->read($length); - return (string)$data; - } - - /** - * Binary search for string - * - * @access private - * @param string string - * @param int start (internally used in recursive function) - * @param int end (internally used in recursive function) - * @return int string number (offset in originals table) - */ - function find_string($string, $start = -1, $end = -1) { - if (($start == -1) or ($end == -1)) { - // find_string is called with only one parameter, set start end end - $start = 0; - $end = $this->total; - } - if (abs($start - $end) <= 1) { - // We're done, now we either found the string, or it doesn't exist - $txt = $this->get_original_string($start); - if ($string == $txt) - return $start; - else - return -1; - } else if ($start > $end) { - // start > end -> turn around and start over - return $this->find_string($string, $end, $start); - } else { - // Divide table in two parts - $half = (int)(($start + $end) / 2); - $cmp = strcmp($string, $this->get_original_string($half)); - if ($cmp == 0) - // string is exactly in the middle => return it - return $half; - else if ($cmp < 0) - // The string is in the upper half - return $this->find_string($string, $start, $half); - else - // The string is in the lower half - return $this->find_string($string, $half, $end); - } - } - - /** - * Translates a string - * - * @access public - * @param string string to be translated - * @return string translated string (or original, if not found) - */ - function translate($string) { - if ($this->short_circuit) - return $string; - $this->load_tables(); - - if ($this->enable_cache) { - // Caching enabled, get translated string from cache - if (array_key_exists($string, $this->cache_translations)) - return $this->cache_translations[$string]; - else - return $string; - } else { - // Caching not enabled, try to find string - $num = $this->find_string($string); - if ($num == -1) - return $string; - else - return $this->get_translation_string($num); - } - } - - /** - * Sanitize plural form expression for use in PHP eval call. - * - * @access private - * @return string sanitized plural form expression - */ - function sanitize_plural_expression($expr) { - // Get rid of disallowed characters. - $expr = preg_replace('@[^a-zA-Z0-9_:;\(\)\?\|\&=!<>+*/\%-]@', '', $expr); - - // Add parenthesis for tertiary '?' operator. - $expr .= ';'; - $res = ''; - $p = 0; - for ($i = 0; $i < strlen($expr); $i++) { - $ch = $expr[$i]; - switch ($ch) { - case '?': - $res .= ' ? ('; - $p++; - break; - case ':': - $res .= ') : ('; - break; - case ';': - $res .= str_repeat( ')', $p) . ';'; - $p = 0; - break; - default: - $res .= $ch; - } - } - return $res; - } - - /** - * Parse full PO header and extract only plural forms line. - * - * @access private - * @return string verbatim plural form header field - */ - function extract_plural_forms_header_from_po_header($header) { - if (preg_match("/(^|\n)plural-forms: ([^\n]*)\n/i", $header, $regs)) - $expr = $regs[2]; - else - $expr = "nplurals=2; plural=n == 1 ? 0 : 1;"; - return $expr; - } - - /** - * Get possible plural forms from MO header - * - * @access private - * @return string plural form header - */ - function get_plural_forms() { - // lets assume message number 0 is header - // this is true, right? - $this->load_tables(); - - // cache header field for plural forms - if (! is_string($this->pluralheader)) { - if ($this->enable_cache) { - $header = $this->cache_translations[""]; - } else { - $header = $this->get_translation_string(0); - } - $expr = $this->extract_plural_forms_header_from_po_header($header); - $this->pluralheader = $this->sanitize_plural_expression($expr); - } - return $this->pluralheader; - } - - /** - * Detects which plural form to take - * - * @access private - * @param n count - * @return int array index of the right plural form - */ - function select_string($n) { - $string = $this->get_plural_forms(); - $string = str_replace('nplurals',"\$total",$string); - $string = str_replace("n",$n,$string); - $string = str_replace('plural',"\$plural",$string); - - $total = 0; - $plural = 0; - - eval("$string"); - if ($plural >= $total) $plural = $total - 1; - return $plural; - } - - /** - * Plural version of gettext - * - * @access public - * @param string single - * @param string plural - * @param string number - * @return translated plural form - */ - function ngettext($single, $plural, $number) { - if ($this->short_circuit) { - if ($number != 1) - return $plural; - else - return $single; - } - - // find out the appropriate form - $select = $this->select_string($number); - - // this should contains all strings separated by NULLs - $key = $single . chr(0) . $plural; - - - if ($this->enable_cache) { - if (! array_key_exists($key, $this->cache_translations)) { - return ($number != 1) ? $plural : $single; - } else { - $result = $this->cache_translations[$key]; - $list = explode(chr(0), $result); - return $list[$select]; - } - } else { - $num = $this->find_string($key); - if ($num == -1) { - return ($number != 1) ? $plural : $single; - } else { - $result = $this->get_translation_string($num); - $list = explode(chr(0), $result); - return $list[$select]; - } - } - } - - function pgettext($context, $msgid) { - $key = $context . chr(4) . $msgid; - $ret = $this->translate($key); - if (strpos($ret, "\004") !== FALSE) { - return $msgid; - } else { - return $ret; - } - } - - function npgettext($context, $singular, $plural, $number) { - $key = $context . chr(4) . $singular; - $ret = $this->ngettext($key, $plural, $number); - if (strpos($ret, "\004") !== FALSE) { - return $singular; - } else { - return $ret; - } - - } -} - -?> diff --git a/inc/lib/gettext/streams.php b/inc/lib/gettext/streams.php deleted file mode 100644 index 3cdc1584..00000000 --- a/inc/lib/gettext/streams.php +++ /dev/null @@ -1,167 +0,0 @@ -. - - This file is part of PHP-gettext. - - PHP-gettext 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. - - PHP-gettext 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 PHP-gettext; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -*/ - - - // Simple class to wrap file streams, string streams, etc. - // seek is essential, and it should be byte stream -class StreamReader { - // should return a string [FIXME: perhaps return array of bytes?] - function read($bytes) { - return false; - } - - // should return new position - function seekto($position) { - return false; - } - - // returns current position - function currentpos() { - return false; - } - - // returns length of entire stream (limit for seekto()s) - function length() { - return false; - } -}; - -class StringReader { - var $_pos; - var $_str; - - function StringReader($str='') { - $this->_str = $str; - $this->_pos = 0; - } - - function read($bytes) { - $data = substr($this->_str, $this->_pos, $bytes); - $this->_pos += $bytes; - if (strlen($this->_str)<$this->_pos) - $this->_pos = strlen($this->_str); - - return $data; - } - - function seekto($pos) { - $this->_pos = $pos; - if (strlen($this->_str)<$this->_pos) - $this->_pos = strlen($this->_str); - return $this->_pos; - } - - function currentpos() { - return $this->_pos; - } - - function length() { - return strlen($this->_str); - } - -}; - - -class FileReader { - var $_pos; - var $_fd; - var $_length; - - function FileReader($filename) { - if (file_exists($filename)) { - - $this->_length=filesize($filename); - $this->_pos = 0; - $this->_fd = fopen($filename,'rb'); - if (!$this->_fd) { - $this->error = 3; // Cannot read file, probably permissions - return false; - } - } else { - $this->error = 2; // File doesn't exist - return false; - } - } - - function read($bytes) { - if ($bytes) { - fseek($this->_fd, $this->_pos); - - // PHP 5.1.1 does not read more than 8192 bytes in one fread() - // the discussions at PHP Bugs suggest it's the intended behaviour - $data = ''; - while ($bytes > 0) { - $chunk = fread($this->_fd, $bytes); - $data .= $chunk; - $bytes -= strlen($chunk); - } - $this->_pos = ftell($this->_fd); - - return $data; - } else return ''; - } - - function seekto($pos) { - fseek($this->_fd, $pos); - $this->_pos = ftell($this->_fd); - return $this->_pos; - } - - function currentpos() { - return $this->_pos; - } - - function length() { - return $this->_length; - } - - function close() { - fclose($this->_fd); - } - -}; - -// Preloads entire file in memory first, then creates a StringReader -// over it (it assumes knowledge of StringReader internals) -class CachedFileReader extends StringReader { - function CachedFileReader($filename) { - if (file_exists($filename)) { - - $length=filesize($filename); - $fd = fopen($filename,'rb'); - - if (!$fd) { - $this->error = 3; // Cannot read file, probably permissions - return false; - } - $this->_str = fread($fd, $length); - fclose($fd); - - } else { - $this->error = 2; // File doesn't exist - return false; - } - } -}; - - -?> diff --git a/inc/polyfill.php b/inc/polyfill.php index 4368c8e8..30e96a16 100644 --- a/inc/polyfill.php +++ b/inc/polyfill.php @@ -2,9 +2,37 @@ // PHP 8.0 +use Vichan\Polyfill\GettextWrapper; + if (!function_exists('str_starts_with')) { function str_starts_with(string $haystack, string $needle): bool { // https://wiki.php.net/rfc/add_str_starts_with_and_ends_with_functions#str_starts_with return \strncmp($haystack, $needle, \strlen($needle)) === 0; } } + +if (!\extension_loaded('gettext')) { + function bindtextdomain(string $domain, string $directory): string { + return GettextWrapper::bindTextDomain($domain, $directory); + } + + function bind_textdomain_codeset(string $domain, string $codeset): ?string { + return GettextWrapper::bindTextDomainCodeset($domain, $codeset); + } + + function textdomain(?string $domain = null) { + return GettextWrapper::textDomain($domain); + } + + //function setlocale(string $locale): void { + // GettextWrapper::setLocale($locale); + //} + + function translate(string $message): string { + return GettextWrapper::translate($message); + } + + function _(string $message): string { + return GettextWrapper::translate($message); + } +} diff --git a/search.php b/search.php index bfd4b022..02edc242 100644 --- a/search.php +++ b/search.php @@ -1,174 +1,70 @@ get(SearchService::class); -if (isset($config['search']['boards'])) { - $boards = $config['search']['boards']; -} else { - $boards = listBoards(TRUE); -} +if (isset($_GET['search']) && !empty($_GET['search'])) { + $raw_search = $_GET['search']; + $ip = $_SERVER['REMOTE_ADDR']; + $fallback_board = (isset($_GET['board']) && !empty($_GET['board'])) ? $_GET['board'] : null; -$body = Element('search_form.html', Array('boards' => $boards, 'board' => isset($_GET['board']) ? $_GET['board'] : false, 'search' => isset($_GET['search']) ? str_replace('"', '"', utf8tohtml($_GET['search'])) : false)); -if (isset($_GET['search']) && !empty($_GET['search']) && isset($_GET['board']) && in_array($_GET['board'], $boards)) { - $phrase = $_GET['search']; - $_body = ''; - - $query = prepare("SELECT COUNT(*) FROM ``search_queries`` WHERE `ip` = :ip AND `time` > :time"); - $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); - $query->bindValue(':time', time() - ($queries_per_minutes[1] * 60)); - $query->execute() or error(db_error($query)); - if ($query->fetchColumn() > $queries_per_minutes[0]) + if ($search_service->checkFlood($ip, $raw_search)) { error(_('Wait a while before searching again, please.')); - - $query = prepare("SELECT COUNT(*) FROM ``search_queries`` WHERE `time` > :time"); - $query->bindValue(':time', time() - ($queries_per_minutes_all[1] * 60)); - $query->execute() or error(db_error($query)); - if ($query->fetchColumn() > $queries_per_minutes_all[0]) - error(_('Wait a while before searching again, please.')); - - - $query = prepare("INSERT INTO ``search_queries`` VALUES (:ip, :time, :query)"); - $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); - $query->bindValue(':time', time()); - $query->bindValue(':query', $phrase); - $query->execute() or error(db_error($query)); - - _syslog(LOG_NOTICE, 'Searched /' . $_GET['board'] . '/ for "' . $phrase . '"'); - - // Cleanup search queries table - $query = prepare("DELETE FROM ``search_queries`` WHERE `time` <= :time"); - $query->bindValue(':time', time() - ($queries_per_minutes_all[1] * 60)); - $query->execute() or error(db_error($query)); - - openBoard($_GET['board']); - - $filters = Array(); - - function search_filters($m) { - global $filters; - $name = $m[2]; - $value = isset($m[4]) ? $m[4] : $m[3]; - - if (!in_array($name, array('id', 'thread', 'subject', 'name'))) { - // unknown filter - return $m[0]; - } - - $filters[$name] = $value; - - return $m[1]; } - $phrase = trim(preg_replace_callback('/(^|\s)(\w+):("(.*)?"|[^\s]*)/', 'search_filters', $phrase)); + // Actually do the search. + $parse_res = $search_service->parse($raw_search); + $filters = $search_service->reduceAndWeight($parse_res); + $search_res = $search_service->search($ip, $raw_search, $filters, $fallback_board); - if (!preg_match('/[^*^\s]/', $phrase) && empty($filters)) { - _syslog(LOG_WARNING, 'Query too broad.'); - $body .= '

(Query too broad.)

'; - echo Element('page.html', Array( - 'config'=>$config, - 'title'=>'Search', - 'body'=>$body, - )); - exit; - } - // Escape escape character - $phrase = str_replace('!', '!!', $phrase); + // Needed to set a global variable further down the stack, plus the template. + $actual_board = $filter->board ?? $fallback_board; - // Remove SQL wildcard - $phrase = str_replace('%', '!%', $phrase); + $body = Element('search_form.html', [ + 'boards' => $search_service->getSearchableBoards(), + 'board' => $_GET['board'], + 'search' => \str_replace('"', '"', utf8tohtml($_GET['search'])) + ]); - // Use asterisk as wildcard to suit convention - $phrase = str_replace('*', '%', $phrase); - - // Remove `, it's used by table prefix magic - $phrase = str_replace('`', '!`', $phrase); - - $like = ''; - $match = Array(); - - // Find exact phrases - if (preg_match_all('/"(.+?)"/', $phrase, $m)) { - foreach($m[1] as &$quote) { - $phrase = str_replace("\"{$quote}\"", '', $phrase); - $match[] = $pdo->quote($quote); - } - } - - $words = explode(' ', $phrase); - foreach($words as &$word) { - if (empty($word)) { - continue; - } - $match[] = $pdo->quote($word); - } - - $like = ''; - foreach($match as &$phrase) { - if (!empty($like)) { - $like .= ' AND '; - } - $phrase = preg_replace('/^\'(.+)\'$/', '\'%$1%\'', $phrase); - $like .= '`body` LIKE ' . $phrase . ' ESCAPE \'!\''; - } - - foreach($filters as $name => $value) { - if (!empty($like)) { - $like .= ' AND '; - } - $like .= '`' . $name . '` = '. $pdo->quote($value); - } - - $like = str_replace('%', '%%', $like); - - $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE " . $like . " ORDER BY `time` DESC LIMIT :limit", $board['uri'])); - $query->bindValue(':limit', $search_limit, PDO::PARAM_INT); - $query->execute() or error(db_error($query)); - - if ($query->rowCount() == $search_limit) { - _syslog(LOG_WARNING, 'Query too broad.'); - $body .= '

('._('Query too broad.').')

'; - echo Element('page.html', Array( - 'config'=>$config, - 'title'=>'Search', - 'body'=>$body, - )); - exit; - } - - $temp = ''; - while ($post = $query->fetch()) { - if (!$post['thread']) { - $po = new Thread($post); - } else { - $po = new Post($post); - } - $temp .= $po->build(true) . '
'; - } - - if (!empty($temp)) - $_body .= '
' . - sprintf(ngettext('%d result in', '%d results in', $query->rowCount()), - $query->rowCount()) . ' ' . - sprintf($config['board_abbreviation'], $board['uri']) . ' - ' . $board['title'] . - '' . $temp . '
'; - - $body .= '
'; - if (!empty($_body)) { - $body .= $_body; + if (empty($search_res)) { + $body .= '

(' . _('No results.') . ')

'; } else { - $body .= '

('._('No results.').')

'; + $body .= '
'; + + openBoard($actual_board); + + $posts_html = ''; + foreach ($search_res as $post) { + if (!$post['thread']) { + $po = new Thread($post); + } else { + $po = new Post($post); + } + $posts_html .= $po->build(true) . '
'; + } + + $body .= '
' . + sprintf(ngettext('%d result in', '%d results in', \count($search_res)), \count($search_res)) . ' ' . + sprintf($config['board_abbreviation'], $board['uri']) . ' - ' . $board['title'] . + '' . $posts_html . '
'; } +} else { + $body = Element('search_form.html', [ + 'boards' => $search_service->getSearchableBoards(), + 'board' => false, + 'search' => false + ]); } echo Element('page.html', Array( diff --git a/tests/ProtectIPTest.php b/tests/ProtectIPTest.php index 878eaa06..1eecc217 100644 --- a/tests/ProtectIPTest.php +++ b/tests/ProtectIPTest.php @@ -2,7 +2,6 @@ use PHPUnit\Framework\TestCase; ob_start(); -define('TINYBOARD', true); require_once "inc/mod/pages.php"; ob_end_clean(); diff --git a/tests/SearchServiceTest.php b/tests/SearchServiceTest.php new file mode 100644 index 00000000..16769f31 --- /dev/null +++ b/tests/SearchServiceTest.php @@ -0,0 +1,64 @@ +createMock(LogDriver::class), + $this->createMock(UserPostQueries::class), + null, + 100, + 250, + 100, + ); + + $filters = $srv->parse("free world all large board:kino board:\"poly\" name:coolie maybe subject:\"subj\" flag:\"pirate\" id:76 thread:8 but not so much"); + Framework\assertTrue($filters->body === [ 'free world all large', 'maybe', 'but not so much' ]); + Framework\assertTrue($filters->subject === 'subj'); + Framework\assertTrue($filters->name === 'coolie'); + Framework\assertTrue($filters->flag === 'pirate'); + Framework\assertTrue($filters->id === 76); + Framework\assertTrue($filters->thread === 8); + } + + public function testWeight() { + $user_queries = $this->createMock(UserPostQueries::class); + $user_queries->method('escapeSearchPosts') + ->willReturnMap([ + [ 'abcd', 'abcd' ], + [ 'abc', 'abc' ], + [ 'a*cd', 'a\\*cd' ], + [ 'a*c', 'a\\*c' ], + ]); + + $srv = new SearchService( + new StderrLogDriver('test', LogDriver::DEBUG), + $user_queries, + null, + 100, + 250, + 100, + ); + + $f = $srv->parse('abcd'); + $no_wildcards = $srv->reduceAndWeight($f)->weight; + + $f = $srv->parse('abc*'); + $end_wildcard = $srv->reduceAndWeight($f)->weight; + + $f = $srv->parse('a*cd'); + $middle_wildcard = $srv->reduceAndWeight($f)->weight; + + $f = $srv->parse('a*c*'); + $wildcards = $srv->reduceAndWeight($f)->weight; + + Framework\assertTrue($no_wildcards < $end_wildcard); + Framework\assertTrue($end_wildcard < $middle_wildcard); + Framework\assertTrue($middle_wildcard < $wildcards); + } +} diff --git a/tests/bannersTest.php b/tests/bannersTest.php deleted file mode 100644 index e432dffe..00000000 --- a/tests/bannersTest.php +++ /dev/null @@ -1,24 +0,0 @@ -lain-bottom.png '; - - //capture input - ob_start(); - listBannersInDir("banners"); - $output = ob_get_contents(); - ob_end_clean(); - //end input - - //assertion - $this->assertEquals($output,$expected); - } -} diff --git a/tools/maintenance.php b/tools/maintenance.php index 7f3c248b..3d5233be 100644 --- a/tools/maintenance.php +++ b/tools/maintenance.php @@ -4,6 +4,7 @@ */ use Vichan\Data\ReportQueries; +use Vichan\Data\SearchQueries; require dirname(__FILE__) . '/inc/cli.php'; @@ -45,9 +46,17 @@ if ($config['cache']['enabled'] === 'fs') { $fs_cache->collect(); $delta = microtime(true) - $start; echo "Deleted $deleted_count expired filesystem cache items in $delta seconds!\n"; - $time_tot = $delta; + $time_tot += $delta; $deleted_tot = $deleted_count; } +echo "Clearing old search log...\n"; +$search_queries = $ctx->get(SearchQueries::class); +$start = microtime(true); +$deleted_count = $search_queries->purgeExpired(); +$delta = microtime(true) - $start; +$time_tot += $delta; +$deleted_tot = $deleted_count; + $time_tot = number_format((float)$time_tot, 4, '.', ''); modLog("Deleted $deleted_tot expired entries in {$time_tot}s with maintenance tool");