From eb2b394ff707477f2b0b09b25ddbd86eefd05d38 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sat, 19 Nov 2022 12:51:33 +0100 Subject: [PATCH 01/28] Support for `Kirby\Query` classes --- src/Kql/Kql.php | 20 +----------- src/Kql/Query.php | 77 +++++++++-------------------------------------- 2 files changed, 15 insertions(+), 82 deletions(-) diff --git a/src/Kql/Kql.php b/src/Kql/Kql.php index bbfa631..9630046 100644 --- a/src/Kql/Kql.php +++ b/src/Kql/Kql.php @@ -70,25 +70,7 @@ public static function query(string $query, $model = null) $site = $kirby->site(); $model = $model ?? $site; - $query = new Query($query, [ - 'collection' => function (string $id) use ($kirby) { - return $kirby->collection($id); - }, - 'file' => function (string $id) use ($kirby) { - return $kirby->file($id); - }, - 'kirby' => $kirby, - 'page' => function (string $id) use ($site) { - return $site->find($id); - }, - 'site' => $site, - 'user' => function (string $id = null) use ($kirby) { - return $kirby->user($id); - }, - $model::CLASS_ALIAS => $model - ]); - - return $query->result(); + return Query::factory($query)->resolve([$model::CLASS_ALIAS => $model]); } public static function render($value) diff --git a/src/Kql/Query.php b/src/Kql/Query.php index 7cd012b..0dc6385 100644 --- a/src/Kql/Query.php +++ b/src/Kql/Query.php @@ -2,71 +2,22 @@ namespace Kirby\Kql; -use Kirby\Toolkit\Query as BaseQuery; - +use Kirby\Query\Query as BaseQuery; + +/** + * Extends the core Query class with the KQL-specific + * functionalities to intercept the segments chain calls + * + * @package Kirby KQL + * @author Nico Hoffmann + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://getkirby.com/license + */ class Query extends BaseQuery { - protected function interceptor($object) - { - return Interceptor::replace($object); - } - - /** - * Resolves the query if anything - * can be found. Otherwise returns null. - * - * @param string $query - * @return mixed - */ - protected function resolve(string $query) + public function intercept(mixed $result): mixed { - // direct key access in arrays - if (is_array($this->data) === true && array_key_exists($query, $this->data) === true) { - $value = $this->data[$query]; - - // closure resolver - if (is_a($value, 'Closure') === true) { - $value = $value(); - } - - return $this->interceptor($value); - } - - $parts = $this->parts($query); - $data = $this->data; - $value = null; - - while (count($parts)) { - $part = array_shift($parts); - $info = $this->part($part); - $method = $info['method']; - $value = null; - - if (is_array($data)) { - $value = $data[$method] ?? null; - } elseif (is_object($data)) { - $data = $this->interceptor($data); - - if (method_exists($data, $method) || method_exists($data, '__call')) { - $value = $data->$method(...$info['args']); - } - } elseif (is_scalar($data)) { - return $data; - } else { - return null; - } - - if (is_a($value, 'Closure') === true) { - $value = $value(...$info['args']); - } - - if (is_array($value) === true) { - $data = $value; - } elseif (is_object($value) === true) { - $data = $this->interceptor($value); - } - } - - return $value; + return Interceptor::replace($result); } } From bf9a5f20b36a8943155ff19984444c43223d344a Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sun, 20 Nov 2022 13:52:14 +0100 Subject: [PATCH 02/28] Remove vendor from repo --- .editorconfig | 2 +- .gitattributes | 29 + .gitignore | 30 +- composer.json | 15 +- composer.lock | 863 +++++++++++++++++++++++- tests/KqlTest.php | 158 ----- vendor/autoload.php | 12 - vendor/composer/ClassLoader.php | 572 ---------------- vendor/composer/InstalledVersions.php | 352 ---------- vendor/composer/autoload_classmap.php | 44 -- vendor/composer/autoload_namespaces.php | 9 - vendor/composer/autoload_psr4.php | 10 - vendor/composer/autoload_real.php | 36 - vendor/composer/autoload_static.php | 71 -- vendor/composer/installed.php | 32 - 15 files changed, 911 insertions(+), 1324 deletions(-) create mode 100644 .gitattributes delete mode 100644 tests/KqlTest.php delete mode 100644 vendor/autoload.php delete mode 100644 vendor/composer/ClassLoader.php delete mode 100644 vendor/composer/InstalledVersions.php delete mode 100644 vendor/composer/autoload_classmap.php delete mode 100644 vendor/composer/autoload_namespaces.php delete mode 100644 vendor/composer/autoload_psr4.php delete mode 100644 vendor/composer/autoload_real.php delete mode 100644 vendor/composer/autoload_static.php delete mode 100644 vendor/composer/installed.php diff --git a/.editorconfig b/.editorconfig index fa9ba60..aa49412 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,5 +20,5 @@ insert_final_newline = true [*.yml] indent_style = space -[*.md] +[*.md,*.txt] trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8fd2f97 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,29 @@ +# Git +.gitattributes export-ignore +.github/ export-ignore +.gitignore export-ignore + +# Source files + +# Development files +.editorconfig export-ignore +.eslintrc.js export-ignore +.prettierrc.json export-ignore +composer.lock export-ignore +package-lock.json export-ignore +package.json export-ignore + +# Screenshots +screenshot.png export-ignore + +# Tests +.codecov.yml export-ignore +.composer-require-checker.json export-ignore +.composer-require-checker.json export-ignore +.php-cs-fixer.dist.php export-ignore +phpmd.xml.dist export-ignore +phpunit.xml.dist export-ignore +psalm.xml.dist export-ignore +etc/ export-ignore +stubs/ export-ignore +tests/ export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index 954190f..aec3c3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,13 @@ # OS files .DS_Store -# npm modules +# Vendor files /node_modules +/vendor -# files of Composer dependencies that are not needed for the plugin -/vendor/**/.* -/vendor/**/*.json -/vendor/**/*.txt -/vendor/**/*.md -/vendor/**/*.yml -/vendor/**/*.yaml -/vendor/**/*.xml -/vendor/**/*.dist -/vendor/**/readme.php -/vendor/**/LICENSE -/vendor/**/COPYING -/vendor/**/VERSION -/vendor/**/docs/* -/vendor/**/example/* -/vendor/**/examples/* -/vendor/**/test/* -/vendor/**/tests/* -/vendor/**/php4/* -/vendor/getkirby/composer-installer -/.php_cs.cache -/.phpunit.result.cache +# Cache and temporary files +/.cache /.php-cs-fixer.cache +/.phpunit.result.cache +/tests/coverage +/tests/*/tmp \ No newline at end of file diff --git a/composer.json b/composer.json index a7172ae..24f5edf 100755 --- a/composer.json +++ b/composer.json @@ -17,15 +17,21 @@ { "name": "Bastian Allgeier", "email": "bastian@getkirby.com" + }, + { + "name": "Nico Hoffmann", + "email": "nico@getkirby.com" } ], "require": { + "php": ">=8.0.0 <8.2.0", + "getkirby/cms": ">=3.8.2", "getkirby/composer-installer": "^1.2.1" }, "config": { "optimize-autoloader": true, "allow-plugins": { - "getkirby/composer-installer": false + "getkirby/composer-installer": true } }, "autoload": { @@ -33,7 +39,12 @@ "Kirby\\": "src/" } }, + "extra": { + "installer-name": "versions", + "kirby-cms-path": false + }, "scripts": { - "fix": "php-cs-fixer fix" + "fix": "php-cs-fixer fix", + "test": "phpunit" } } diff --git a/composer.lock b/composer.lock index e5902db..4337daf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,309 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b3661c52f0da1ad6c43e66f6001abb79", + "content-hash": "5803be65e48773b737ab5f06e1f54d28", "packages": [ + { + "name": "claviska/simpleimage", + "version": "3.7.0", + "source": { + "type": "git", + "url": "https://github.com/claviska/SimpleImage.git", + "reference": "abd15ced313c7b8041d7d73d8d2398b4f2510cf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/claviska/SimpleImage/zipball/abd15ced313c7b8041d7d73d8d2398b4f2510cf1", + "reference": "abd15ced313c7b8041d7d73d8d2398b4f2510cf1", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "league/color-extractor": "0.3.*", + "php": ">=5.6.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "claviska": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Cory LaViska", + "homepage": "http://www.abeautifulsite.net/", + "role": "Developer" + } + ], + "description": "A PHP class that makes working with images as simple as possible.", + "support": { + "issues": "https://github.com/claviska/SimpleImage/issues", + "source": "https://github.com/claviska/SimpleImage/tree/3.7.0" + }, + "funding": [ + { + "url": "https://github.com/claviska", + "type": "github" + } + ], + "time": "2022-07-05T13:18:44+00:00" + }, + { + "name": "composer/semver", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-04-01T19:23:25+00:00" + }, + { + "name": "filp/whoops", + "version": "2.14.5", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "a63e5e8f26ebbebf8ed3c5c691637325512eb0dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/a63e5e8f26ebbebf8ed3c5c691637325512eb0dc", + "reference": "a63e5e8f26ebbebf8ed3c5c691637325512eb0dc", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^0.9 || ^1.0", + "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.14.5" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2022-01-07T12:00:00+00:00" + }, + { + "name": "getkirby/cms", + "version": "3.8.2", + "source": { + "type": "git", + "url": "https://github.com/getkirby/kirby.git", + "reference": "f16b0b41db19ab5dbcf22e27f4aa2c35cb2c490c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getkirby/kirby/zipball/f16b0b41db19ab5dbcf22e27f4aa2c35cb2c490c", + "reference": "f16b0b41db19ab5dbcf22e27f4aa2c35cb2c490c", + "shasum": "" + }, + "require": { + "claviska/simpleimage": "3.7.0", + "composer/semver": "3.3.2", + "ext-ctype": "*", + "ext-curl": "*", + "ext-dom": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "filp/whoops": "2.14.5", + "getkirby/composer-installer": "^1.2.1", + "laminas/laminas-escaper": "2.12.0", + "michelf/php-smartypants": "1.8.1", + "php": ">=8.0.0 <8.2.0", + "phpmailer/phpmailer": "6.6.5", + "symfony/polyfill-intl-idn": "1.26.0", + "symfony/polyfill-mbstring": "1.26.0" + }, + "replace": { + "symfony/polyfill-php72": "*" + }, + "suggest": { + "ext-PDO": "Support for using databases", + "ext-apcu": "Support for the Apcu cache driver", + "ext-exif": "Support for exif information from images", + "ext-fileinfo": "Improved mime type detection for files", + "ext-intl": "Improved i18n number formatting", + "ext-memcached": "Support for the Memcached cache driver", + "ext-zip": "Support for ZIP archive file functions", + "ext-zlib": "Sanitization and validation for svgz files" + }, + "type": "kirby-cms", + "extra": { + "unused": [ + "symfony/polyfill-intl-idn" + ] + }, + "autoload": { + "files": [ + "config/setup.php", + "config/helpers.php" + ], + "psr-4": { + "Kirby\\": "src/" + }, + "classmap": [ + "dependencies/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "proprietary" + ], + "authors": [ + { + "name": "Kirby Team", + "email": "support@getkirby.com", + "homepage": "https://getkirby.com" + } + ], + "description": "The Kirby 3 core", + "homepage": "https://getkirby.com", + "keywords": [ + "cms", + "core", + "kirby" + ], + "support": { + "email": "support@getkirby.com", + "forum": "https://forum.getkirby.com", + "issues": "https://github.com/getkirby/kirby/issues", + "source": "https://github.com/getkirby/kirby" + }, + "funding": [ + { + "url": "https://getkirby.com/buy", + "type": "custom" + } + ], + "time": "2022-11-15T12:18:18+00:00" + }, { "name": "getkirby/composer-installer", "version": "1.2.1", @@ -52,6 +353,562 @@ } ], "time": "2020-12-28T12:54:39+00:00" + }, + { + "name": "laminas/laminas-escaper", + "version": "2.12.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-escaper.git", + "reference": "ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490", + "reference": "ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-mbstring": "*", + "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0" + }, + "conflict": { + "zendframework/zend-escaper": "*" + }, + "require-dev": { + "infection/infection": "^0.26.6", + "laminas/laminas-coding-standard": "~2.4.0", + "maglnet/composer-require-checker": "^3.8.0", + "phpunit/phpunit": "^9.5.18", + "psalm/plugin-phpunit": "^0.17.0", + "vimeo/psalm": "^4.22.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Escaper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", + "homepage": "https://laminas.dev", + "keywords": [ + "escaper", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-escaper/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-escaper/issues", + "rss": "https://github.com/laminas/laminas-escaper/releases.atom", + "source": "https://github.com/laminas/laminas-escaper" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-10-10T10:11:09+00:00" + }, + { + "name": "league/color-extractor", + "version": "0.3.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/color-extractor.git", + "reference": "837086ec60f50c84c611c613963e4ad2e2aec806" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/color-extractor/zipball/837086ec60f50c84c611c613963e4ad2e2aec806", + "reference": "837086ec60f50c84c611c613963e4ad2e2aec806", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "php": ">=5.4.0" + }, + "replace": { + "matthecat/colorextractor": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2", + "phpunit/phpunit": "~5" + }, + "type": "library", + "autoload": { + "psr-4": { + "": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathieu Lechat", + "email": "math.lechat@gmail.com", + "homepage": "http://matthecat.com", + "role": "Developer" + } + ], + "description": "Extract colors from an image as a human would do.", + "homepage": "https://github.com/thephpleague/color-extractor", + "keywords": [ + "color", + "extract", + "human", + "image", + "palette" + ], + "support": { + "issues": "https://github.com/thephpleague/color-extractor/issues", + "source": "https://github.com/thephpleague/color-extractor/tree/master" + }, + "time": "2016-12-15T09:30:02+00:00" + }, + { + "name": "michelf/php-smartypants", + "version": "1.8.1", + "source": { + "type": "git", + "url": "https://github.com/michelf/php-smartypants.git", + "reference": "47d17c90a4dfd0ccf1f87e25c65e6c8012415aad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/michelf/php-smartypants/zipball/47d17c90a4dfd0ccf1f87e25c65e6c8012415aad", + "reference": "47d17c90a4dfd0ccf1f87e25c65e6c8012415aad", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Michelf": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Michel Fortin", + "email": "michel.fortin@michelf.ca", + "homepage": "https://michelf.ca/", + "role": "Developer" + }, + { + "name": "John Gruber", + "homepage": "https://daringfireball.net/" + } + ], + "description": "PHP SmartyPants", + "homepage": "https://michelf.ca/projects/php-smartypants/", + "keywords": [ + "dashes", + "quotes", + "spaces", + "typographer", + "typography" + ], + "support": { + "issues": "https://github.com/michelf/php-smartypants/issues", + "source": "https://github.com/michelf/php-smartypants/tree/1.8.1" + }, + "time": "2016-12-13T01:01:17+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.6.5", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "8b6386d7417526d1ea4da9edb70b8352f7543627" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/8b6386d7417526d1ea4da9edb70b8352f7543627", + "reference": "8b6386d7417526d1ea4da9edb70b8352f7543627", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.2", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.6.2", + "yoast/phpunit-polyfills": "^1.0.0" + }, + "suggest": { + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "support": { + "issues": "https://github.com/PHPMailer/PHPMailer/issues", + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.6.5" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2022-10-07T12:23:10+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" } ], "packages-dev": [], @@ -60,7 +917,9 @@ "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, - "platform": [], + "platform": { + "php": ">=8.0.0 <8.2.0" + }, "platform-dev": [], "plugin-api-version": "2.3.0" } diff --git a/tests/KqlTest.php b/tests/KqlTest.php deleted file mode 100644 index f565938..0000000 --- a/tests/KqlTest.php +++ /dev/null @@ -1,158 +0,0 @@ -app = new App([ - 'roots' => [ - 'index' => '/dev/null' - ], - 'site' => [ - 'children' => [ - [ - 'slug' => 'projects' - ], - [ - 'slug' => 'about' - ], - [ - 'slug' => 'contact' - ] - ], - 'content' => [ - 'title' => 'Test Site' - ], - ] - ]); - } - - public function testForbiddenMethod() - { - $this->expectException("Kirby\Exception\PermissionException"); - $this->expectExceptionMessage('The method "Kirby\Cms\Page::delete()" is not allowed in the API context'); - $result = Kql::run('site.children.first.delete'); - } - - public function testRun() - { - $result = Kql::run('site.title'); - $expected = 'Test Site'; - - $this->assertSame($expected, $result); - } - - public function testQuery() - { - $result = Kql::run([ - 'query' => 'site.children', - 'select' => 'slug' - ]); - - $expected = [ - [ - 'slug' => 'projects', - ], - [ - 'slug' => 'about', - ], - [ - 'slug' => 'contact', - ] - ]; - - $this->assertSame($expected, $result); - } - - public function testSelectWithAlias() - { - $result = Kql::run([ - 'select' => [ - 'myTitle' => 'site.title' - ] - ]); - - $expected = [ - 'myTitle' => 'Test Site', - ]; - - $this->assertSame($expected, $result); - } - - public function testSelectWithArray() - { - $result = Kql::run([ - 'select' => ['title', 'url'] - ]); - - $expected = [ - 'title' => 'Test Site', - 'url' => '/' - ]; - - $this->assertSame($expected, $result); - } - - public function testSelectWithBoolean() - { - $result = Kql::run([ - 'select' => [ - 'title' => true - ] - ]); - - $expected = [ - 'title' => 'Test Site' - ]; - - $this->assertSame($expected, $result); - } - - public function testSelectWithQuery() - { - $result = Kql::run([ - 'select' => [ - 'children' => [ - 'query' => 'site.children', - 'select' => 'slug' - ] - ] - ]); - - $expected = [ - 'children' => [ - [ - 'slug' => 'projects', - ], - [ - 'slug' => 'about', - ], - [ - 'slug' => 'contact', - ] - ] - ]; - - $this->assertSame($expected, $result); - } - - public function testSelectWithString() - { - $result = Kql::run([ - 'select' => [ - 'title' => 'site.title.upper' - ] - ]); - - $expected = [ - 'title' => 'TEST SITE' - ]; - - $this->assertSame($expected, $result); - } -} diff --git a/vendor/autoload.php b/vendor/autoload.php deleted file mode 100644 index d2fdda8..0000000 --- a/vendor/autoload.php +++ /dev/null @@ -1,12 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Autoload; - -/** - * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. - * - * $loader = new \Composer\Autoload\ClassLoader(); - * - * // register classes with namespaces - * $loader->add('Symfony\Component', __DIR__.'/component'); - * $loader->add('Symfony', __DIR__.'/framework'); - * - * // activate the autoloader - * $loader->register(); - * - * // to enable searching the include path (eg. for PEAR packages) - * $loader->setUseIncludePath(true); - * - * In this example, if you try to use a class in the Symfony\Component - * namespace or one of its children (Symfony\Component\Console for instance), - * the autoloader will first look for the class under the component/ - * directory, and it will then fallback to the framework/ directory if not - * found before giving up. - * - * This class is loosely based on the Symfony UniversalClassLoader. - * - * @author Fabien Potencier - * @author Jordi Boggiano - * @see https://www.php-fig.org/psr/psr-0/ - * @see https://www.php-fig.org/psr/psr-4/ - */ -class ClassLoader -{ - /** @var ?string */ - private $vendorDir; - - // PSR-4 - /** - * @var array[] - * @psalm-var array> - */ - private $prefixLengthsPsr4 = array(); - /** - * @var array[] - * @psalm-var array> - */ - private $prefixDirsPsr4 = array(); - /** - * @var array[] - * @psalm-var array - */ - private $fallbackDirsPsr4 = array(); - - // PSR-0 - /** - * @var array[] - * @psalm-var array> - */ - private $prefixesPsr0 = array(); - /** - * @var array[] - * @psalm-var array - */ - private $fallbackDirsPsr0 = array(); - - /** @var bool */ - private $useIncludePath = false; - - /** - * @var string[] - * @psalm-var array - */ - private $classMap = array(); - - /** @var bool */ - private $classMapAuthoritative = false; - - /** - * @var bool[] - * @psalm-var array - */ - private $missingClasses = array(); - - /** @var ?string */ - private $apcuPrefix; - - /** - * @var self[] - */ - private static $registeredLoaders = array(); - - /** - * @param ?string $vendorDir - */ - public function __construct($vendorDir = null) - { - $this->vendorDir = $vendorDir; - } - - /** - * @return string[] - */ - public function getPrefixes() - { - if (!empty($this->prefixesPsr0)) { - return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); - } - - return array(); - } - - /** - * @return array[] - * @psalm-return array> - */ - public function getPrefixesPsr4() - { - return $this->prefixDirsPsr4; - } - - /** - * @return array[] - * @psalm-return array - */ - public function getFallbackDirs() - { - return $this->fallbackDirsPsr0; - } - - /** - * @return array[] - * @psalm-return array - */ - public function getFallbackDirsPsr4() - { - return $this->fallbackDirsPsr4; - } - - /** - * @return string[] Array of classname => path - * @psalm-return array - */ - public function getClassMap() - { - return $this->classMap; - } - - /** - * @param string[] $classMap Class to filename map - * @psalm-param array $classMap - * - * @return void - */ - public function addClassMap(array $classMap) - { - if ($this->classMap) { - $this->classMap = array_merge($this->classMap, $classMap); - } else { - $this->classMap = $classMap; - } - } - - /** - * Registers a set of PSR-0 directories for a given prefix, either - * appending or prepending to the ones previously set for this prefix. - * - * @param string $prefix The prefix - * @param string[]|string $paths The PSR-0 root directories - * @param bool $prepend Whether to prepend the directories - * - * @return void - */ - public function add($prefix, $paths, $prepend = false) - { - if (!$prefix) { - if ($prepend) { - $this->fallbackDirsPsr0 = array_merge( - (array) $paths, - $this->fallbackDirsPsr0 - ); - } else { - $this->fallbackDirsPsr0 = array_merge( - $this->fallbackDirsPsr0, - (array) $paths - ); - } - - return; - } - - $first = $prefix[0]; - if (!isset($this->prefixesPsr0[$first][$prefix])) { - $this->prefixesPsr0[$first][$prefix] = (array) $paths; - - return; - } - if ($prepend) { - $this->prefixesPsr0[$first][$prefix] = array_merge( - (array) $paths, - $this->prefixesPsr0[$first][$prefix] - ); - } else { - $this->prefixesPsr0[$first][$prefix] = array_merge( - $this->prefixesPsr0[$first][$prefix], - (array) $paths - ); - } - } - - /** - * Registers a set of PSR-4 directories for a given namespace, either - * appending or prepending to the ones previously set for this namespace. - * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param string[]|string $paths The PSR-4 base directories - * @param bool $prepend Whether to prepend the directories - * - * @throws \InvalidArgumentException - * - * @return void - */ - public function addPsr4($prefix, $paths, $prepend = false) - { - if (!$prefix) { - // Register directories for the root namespace. - if ($prepend) { - $this->fallbackDirsPsr4 = array_merge( - (array) $paths, - $this->fallbackDirsPsr4 - ); - } else { - $this->fallbackDirsPsr4 = array_merge( - $this->fallbackDirsPsr4, - (array) $paths - ); - } - } elseif (!isset($this->prefixDirsPsr4[$prefix])) { - // Register directories for a new namespace. - $length = strlen($prefix); - if ('\\' !== $prefix[$length - 1]) { - throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); - } - $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = (array) $paths; - } elseif ($prepend) { - // Prepend directories for an already registered namespace. - $this->prefixDirsPsr4[$prefix] = array_merge( - (array) $paths, - $this->prefixDirsPsr4[$prefix] - ); - } else { - // Append directories for an already registered namespace. - $this->prefixDirsPsr4[$prefix] = array_merge( - $this->prefixDirsPsr4[$prefix], - (array) $paths - ); - } - } - - /** - * Registers a set of PSR-0 directories for a given prefix, - * replacing any others previously set for this prefix. - * - * @param string $prefix The prefix - * @param string[]|string $paths The PSR-0 base directories - * - * @return void - */ - public function set($prefix, $paths) - { - if (!$prefix) { - $this->fallbackDirsPsr0 = (array) $paths; - } else { - $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; - } - } - - /** - * Registers a set of PSR-4 directories for a given namespace, - * replacing any others previously set for this namespace. - * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param string[]|string $paths The PSR-4 base directories - * - * @throws \InvalidArgumentException - * - * @return void - */ - public function setPsr4($prefix, $paths) - { - if (!$prefix) { - $this->fallbackDirsPsr4 = (array) $paths; - } else { - $length = strlen($prefix); - if ('\\' !== $prefix[$length - 1]) { - throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); - } - $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = (array) $paths; - } - } - - /** - * Turns on searching the include path for class files. - * - * @param bool $useIncludePath - * - * @return void - */ - public function setUseIncludePath($useIncludePath) - { - $this->useIncludePath = $useIncludePath; - } - - /** - * Can be used to check if the autoloader uses the include path to check - * for classes. - * - * @return bool - */ - public function getUseIncludePath() - { - return $this->useIncludePath; - } - - /** - * Turns off searching the prefix and fallback directories for classes - * that have not been registered with the class map. - * - * @param bool $classMapAuthoritative - * - * @return void - */ - public function setClassMapAuthoritative($classMapAuthoritative) - { - $this->classMapAuthoritative = $classMapAuthoritative; - } - - /** - * Should class lookup fail if not found in the current class map? - * - * @return bool - */ - public function isClassMapAuthoritative() - { - return $this->classMapAuthoritative; - } - - /** - * APCu prefix to use to cache found/not-found classes, if the extension is enabled. - * - * @param string|null $apcuPrefix - * - * @return void - */ - public function setApcuPrefix($apcuPrefix) - { - $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; - } - - /** - * The APCu prefix in use, or null if APCu caching is not enabled. - * - * @return string|null - */ - public function getApcuPrefix() - { - return $this->apcuPrefix; - } - - /** - * Registers this instance as an autoloader. - * - * @param bool $prepend Whether to prepend the autoloader or not - * - * @return void - */ - public function register($prepend = false) - { - spl_autoload_register(array($this, 'loadClass'), true, $prepend); - - if (null === $this->vendorDir) { - return; - } - - if ($prepend) { - self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; - } else { - unset(self::$registeredLoaders[$this->vendorDir]); - self::$registeredLoaders[$this->vendorDir] = $this; - } - } - - /** - * Unregisters this instance as an autoloader. - * - * @return void - */ - public function unregister() - { - spl_autoload_unregister(array($this, 'loadClass')); - - if (null !== $this->vendorDir) { - unset(self::$registeredLoaders[$this->vendorDir]); - } - } - - /** - * Loads the given class or interface. - * - * @param string $class The name of the class - * @return true|null True if loaded, null otherwise - */ - public function loadClass($class) - { - if ($file = $this->findFile($class)) { - includeFile($file); - - return true; - } - - return null; - } - - /** - * Finds the path to the file where the class is defined. - * - * @param string $class The name of the class - * - * @return string|false The path if found, false otherwise - */ - public function findFile($class) - { - // class map lookup - if (isset($this->classMap[$class])) { - return $this->classMap[$class]; - } - if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { - return false; - } - if (null !== $this->apcuPrefix) { - $file = apcu_fetch($this->apcuPrefix.$class, $hit); - if ($hit) { - return $file; - } - } - - $file = $this->findFileWithExtension($class, '.php'); - - // Search for Hack files if we are running on HHVM - if (false === $file && defined('HHVM_VERSION')) { - $file = $this->findFileWithExtension($class, '.hh'); - } - - if (null !== $this->apcuPrefix) { - apcu_add($this->apcuPrefix.$class, $file); - } - - if (false === $file) { - // Remember that this class does not exist. - $this->missingClasses[$class] = true; - } - - return $file; - } - - /** - * Returns the currently registered loaders indexed by their corresponding vendor directories. - * - * @return self[] - */ - public static function getRegisteredLoaders() - { - return self::$registeredLoaders; - } - - /** - * @param string $class - * @param string $ext - * @return string|false - */ - private function findFileWithExtension($class, $ext) - { - // PSR-4 lookup - $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; - - $first = $class[0]; - if (isset($this->prefixLengthsPsr4[$first])) { - $subPath = $class; - while (false !== $lastPos = strrpos($subPath, '\\')) { - $subPath = substr($subPath, 0, $lastPos); - $search = $subPath . '\\'; - if (isset($this->prefixDirsPsr4[$search])) { - $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); - foreach ($this->prefixDirsPsr4[$search] as $dir) { - if (file_exists($file = $dir . $pathEnd)) { - return $file; - } - } - } - } - } - - // PSR-4 fallback dirs - foreach ($this->fallbackDirsPsr4 as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { - return $file; - } - } - - // PSR-0 lookup - if (false !== $pos = strrpos($class, '\\')) { - // namespaced class name - $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) - . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); - } else { - // PEAR-like class name - $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; - } - - if (isset($this->prefixesPsr0[$first])) { - foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { - if (0 === strpos($class, $prefix)) { - foreach ($dirs as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { - return $file; - } - } - } - } - } - - // PSR-0 fallback dirs - foreach ($this->fallbackDirsPsr0 as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { - return $file; - } - } - - // PSR-0 include paths. - if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { - return $file; - } - - return false; - } -} - -/** - * Scope isolated include. - * - * Prevents access to $this/self from included files. - * - * @param string $file - * @return void - * @private - */ -function includeFile($file) -{ - include $file; -} diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php deleted file mode 100644 index c6b54af..0000000 --- a/vendor/composer/InstalledVersions.php +++ /dev/null @@ -1,352 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer; - -use Composer\Autoload\ClassLoader; -use Composer\Semver\VersionParser; - -/** - * This class is copied in every Composer installed project and available to all - * - * See also https://getcomposer.org/doc/07-runtime.md#installed-versions - * - * To require its presence, you can require `composer-runtime-api ^2.0` - * - * @final - */ -class InstalledVersions -{ - /** - * @var mixed[]|null - * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null - */ - private static $installed; - - /** - * @var bool|null - */ - private static $canGetVendors; - - /** - * @var array[] - * @psalm-var array}> - */ - private static $installedByVendor = array(); - - /** - * Returns a list of all package names which are present, either by being installed, replaced or provided - * - * @return string[] - * @psalm-return list - */ - public static function getInstalledPackages() - { - $packages = array(); - foreach (self::getInstalled() as $installed) { - $packages[] = array_keys($installed['versions']); - } - - if (1 === \count($packages)) { - return $packages[0]; - } - - return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); - } - - /** - * Returns a list of all package names with a specific type e.g. 'library' - * - * @param string $type - * @return string[] - * @psalm-return list - */ - public static function getInstalledPackagesByType($type) - { - $packagesByType = array(); - - foreach (self::getInstalled() as $installed) { - foreach ($installed['versions'] as $name => $package) { - if (isset($package['type']) && $package['type'] === $type) { - $packagesByType[] = $name; - } - } - } - - return $packagesByType; - } - - /** - * Checks whether the given package is installed - * - * This also returns true if the package name is provided or replaced by another package - * - * @param string $packageName - * @param bool $includeDevRequirements - * @return bool - */ - public static function isInstalled($packageName, $includeDevRequirements = true) - { - foreach (self::getInstalled() as $installed) { - if (isset($installed['versions'][$packageName])) { - return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); - } - } - - return false; - } - - /** - * Checks whether the given package satisfies a version constraint - * - * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: - * - * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') - * - * @param VersionParser $parser Install composer/semver to have access to this class and functionality - * @param string $packageName - * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package - * @return bool - */ - public static function satisfies(VersionParser $parser, $packageName, $constraint) - { - $constraint = $parser->parseConstraints($constraint); - $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); - - return $provided->matches($constraint); - } - - /** - * Returns a version constraint representing all the range(s) which are installed for a given package - * - * It is easier to use this via isInstalled() with the $constraint argument if you need to check - * whether a given version of a package is installed, and not just whether it exists - * - * @param string $packageName - * @return string Version constraint usable with composer/semver - */ - public static function getVersionRanges($packageName) - { - foreach (self::getInstalled() as $installed) { - if (!isset($installed['versions'][$packageName])) { - continue; - } - - $ranges = array(); - if (isset($installed['versions'][$packageName]['pretty_version'])) { - $ranges[] = $installed['versions'][$packageName]['pretty_version']; - } - if (array_key_exists('aliases', $installed['versions'][$packageName])) { - $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); - } - if (array_key_exists('replaced', $installed['versions'][$packageName])) { - $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); - } - if (array_key_exists('provided', $installed['versions'][$packageName])) { - $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); - } - - return implode(' || ', $ranges); - } - - throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); - } - - /** - * @param string $packageName - * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present - */ - public static function getVersion($packageName) - { - foreach (self::getInstalled() as $installed) { - if (!isset($installed['versions'][$packageName])) { - continue; - } - - if (!isset($installed['versions'][$packageName]['version'])) { - return null; - } - - return $installed['versions'][$packageName]['version']; - } - - throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); - } - - /** - * @param string $packageName - * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present - */ - public static function getPrettyVersion($packageName) - { - foreach (self::getInstalled() as $installed) { - if (!isset($installed['versions'][$packageName])) { - continue; - } - - if (!isset($installed['versions'][$packageName]['pretty_version'])) { - return null; - } - - return $installed['versions'][$packageName]['pretty_version']; - } - - throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); - } - - /** - * @param string $packageName - * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference - */ - public static function getReference($packageName) - { - foreach (self::getInstalled() as $installed) { - if (!isset($installed['versions'][$packageName])) { - continue; - } - - if (!isset($installed['versions'][$packageName]['reference'])) { - return null; - } - - return $installed['versions'][$packageName]['reference']; - } - - throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); - } - - /** - * @param string $packageName - * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. - */ - public static function getInstallPath($packageName) - { - foreach (self::getInstalled() as $installed) { - if (!isset($installed['versions'][$packageName])) { - continue; - } - - return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; - } - - throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); - } - - /** - * @return array - * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} - */ - public static function getRootPackage() - { - $installed = self::getInstalled(); - - return $installed[0]['root']; - } - - /** - * Returns the raw installed.php data for custom implementations - * - * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. - * @return array[] - * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} - */ - public static function getRawData() - { - @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); - - if (null === self::$installed) { - // only require the installed.php file if this file is loaded from its dumped location, - // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 - if (substr(__DIR__, -8, 1) !== 'C') { - self::$installed = include __DIR__ . '/installed.php'; - } else { - self::$installed = array(); - } - } - - return self::$installed; - } - - /** - * Returns the raw data of all installed.php which are currently loaded for custom implementations - * - * @return array[] - * @psalm-return list}> - */ - public static function getAllRawData() - { - return self::getInstalled(); - } - - /** - * Lets you reload the static array from another file - * - * This is only useful for complex integrations in which a project needs to use - * this class but then also needs to execute another project's autoloader in process, - * and wants to ensure both projects have access to their version of installed.php. - * - * A typical case would be PHPUnit, where it would need to make sure it reads all - * the data it needs from this class, then call reload() with - * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure - * the project in which it runs can then also use this class safely, without - * interference between PHPUnit's dependencies and the project's dependencies. - * - * @param array[] $data A vendor/composer/installed.php data set - * @return void - * - * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data - */ - public static function reload($data) - { - self::$installed = $data; - self::$installedByVendor = array(); - } - - /** - * @return array[] - * @psalm-return list}> - */ - private static function getInstalled() - { - if (null === self::$canGetVendors) { - self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); - } - - $installed = array(); - - if (self::$canGetVendors) { - foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { - if (isset(self::$installedByVendor[$vendorDir])) { - $installed[] = self::$installedByVendor[$vendorDir]; - } elseif (is_file($vendorDir.'/composer/installed.php')) { - $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; - if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { - self::$installed = $installed[count($installed) - 1]; - } - } - } - } - - if (null === self::$installed) { - // only require the installed.php file if this file is loaded from its dumped location, - // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 - if (substr(__DIR__, -8, 1) !== 'C') { - self::$installed = require __DIR__ . '/installed.php'; - } else { - self::$installed = array(); - } - } - $installed[] = self::$installed; - - return $installed; - } -} diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php deleted file mode 100644 index 570c2d9..0000000 --- a/vendor/composer/autoload_classmap.php +++ /dev/null @@ -1,44 +0,0 @@ - $vendorDir . '/composer/InstalledVersions.php', - 'Kirby\\ComposerInstaller\\CmsInstaller' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/CmsInstaller.php', - 'Kirby\\ComposerInstaller\\Installer' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/Installer.php', - 'Kirby\\ComposerInstaller\\Plugin' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/Plugin.php', - 'Kirby\\ComposerInstaller\\PluginInstaller' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/PluginInstaller.php', - 'Kirby\\Kql\\Help' => $baseDir . '/src/Kql/Help.php', - 'Kirby\\Kql\\Interceptor' => $baseDir . '/src/Kql/Interceptor.php', - 'Kirby\\Kql\\Interceptors\\Cms\\App' => $baseDir . '/src/Kql/Interceptors/Cms/App.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Block' => $baseDir . '/src/Kql/Interceptors/Cms/Block.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Blocks' => $baseDir . '/src/Kql/Interceptors/Cms/Blocks.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint' => $baseDir . '/src/Kql/Interceptors/Cms/Blueprint.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Collection' => $baseDir . '/src/Kql/Interceptors/Cms/Collection.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Content' => $baseDir . '/src/Kql/Interceptors/Cms/Content.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Field' => $baseDir . '/src/Kql/Interceptors/Cms/Field.php', - 'Kirby\\Kql\\Interceptors\\Cms\\File' => $baseDir . '/src/Kql/Interceptors/Cms/File.php', - 'Kirby\\Kql\\Interceptors\\Cms\\FileVersion' => $baseDir . '/src/Kql/Interceptors/Cms/FileVersion.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Files' => $baseDir . '/src/Kql/Interceptors/Cms/Files.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Layout' => $baseDir . '/src/Kql/Interceptors/Cms/Layout.php', - 'Kirby\\Kql\\Interceptors\\Cms\\LayoutColumn' => $baseDir . '/src/Kql/Interceptors/Cms/LayoutColumn.php', - 'Kirby\\Kql\\Interceptors\\Cms\\LayoutColumns' => $baseDir . '/src/Kql/Interceptors/Cms/LayoutColumns.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Layouts' => $baseDir . '/src/Kql/Interceptors/Cms/Layouts.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Model' => $baseDir . '/src/Kql/Interceptors/Cms/Model.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Page' => $baseDir . '/src/Kql/Interceptors/Cms/Page.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Pages' => $baseDir . '/src/Kql/Interceptors/Cms/Pages.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Role' => $baseDir . '/src/Kql/Interceptors/Cms/Role.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Site' => $baseDir . '/src/Kql/Interceptors/Cms/Site.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Structure' => $baseDir . '/src/Kql/Interceptors/Cms/Structure.php', - 'Kirby\\Kql\\Interceptors\\Cms\\StructureObject' => $baseDir . '/src/Kql/Interceptors/Cms/StructureObject.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Translation' => $baseDir . '/src/Kql/Interceptors/Cms/Translation.php', - 'Kirby\\Kql\\Interceptors\\Cms\\User' => $baseDir . '/src/Kql/Interceptors/Cms/User.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Users' => $baseDir . '/src/Kql/Interceptors/Cms/Users.php', - 'Kirby\\Kql\\Interceptors\\Interceptor' => $baseDir . '/src/Kql/Interceptors/Interceptor.php', - 'Kirby\\Kql\\Interceptors\\Toolkit\\Obj' => $baseDir . '/src/Kql/Interceptors/Toolkit/Obj.php', - 'Kirby\\Kql\\Kql' => $baseDir . '/src/Kql/Kql.php', - 'Kirby\\Kql\\Query' => $baseDir . '/src/Kql/Query.php', -); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php deleted file mode 100644 index 15a2ff3..0000000 --- a/vendor/composer/autoload_namespaces.php +++ /dev/null @@ -1,9 +0,0 @@ - array($baseDir . '/src', $vendorDir . '/getkirby/composer-installer/src'), -); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php deleted file mode 100644 index cb70379..0000000 --- a/vendor/composer/autoload_real.php +++ /dev/null @@ -1,36 +0,0 @@ -register(true); - - return $loader; - } -} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php deleted file mode 100644 index 73cb48e..0000000 --- a/vendor/composer/autoload_static.php +++ /dev/null @@ -1,71 +0,0 @@ - - array ( - 'Kirby\\' => 6, - ), - ); - - public static $prefixDirsPsr4 = array ( - 'Kirby\\' => - array ( - 0 => __DIR__ . '/../..' . '/src', - 1 => __DIR__ . '/..' . '/getkirby/composer-installer/src', - ), - ); - - public static $classMap = array ( - 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', - 'Kirby\\ComposerInstaller\\CmsInstaller' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/CmsInstaller.php', - 'Kirby\\ComposerInstaller\\Installer' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/Installer.php', - 'Kirby\\ComposerInstaller\\Plugin' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/Plugin.php', - 'Kirby\\ComposerInstaller\\PluginInstaller' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/PluginInstaller.php', - 'Kirby\\Kql\\Help' => __DIR__ . '/../..' . '/src/Kql/Help.php', - 'Kirby\\Kql\\Interceptor' => __DIR__ . '/../..' . '/src/Kql/Interceptor.php', - 'Kirby\\Kql\\Interceptors\\Cms\\App' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/App.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Block' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Block.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Blocks' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Blocks.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Blueprint.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Collection' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Collection.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Content' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Content.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Field' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Field.php', - 'Kirby\\Kql\\Interceptors\\Cms\\File' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/File.php', - 'Kirby\\Kql\\Interceptors\\Cms\\FileVersion' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/FileVersion.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Files' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Files.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Layout' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Layout.php', - 'Kirby\\Kql\\Interceptors\\Cms\\LayoutColumn' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/LayoutColumn.php', - 'Kirby\\Kql\\Interceptors\\Cms\\LayoutColumns' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/LayoutColumns.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Layouts' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Layouts.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Model' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Model.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Page' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Page.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Pages' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Pages.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Role' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Role.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Site' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Site.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Structure' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Structure.php', - 'Kirby\\Kql\\Interceptors\\Cms\\StructureObject' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/StructureObject.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Translation' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Translation.php', - 'Kirby\\Kql\\Interceptors\\Cms\\User' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/User.php', - 'Kirby\\Kql\\Interceptors\\Cms\\Users' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Cms/Users.php', - 'Kirby\\Kql\\Interceptors\\Interceptor' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Interceptor.php', - 'Kirby\\Kql\\Interceptors\\Toolkit\\Obj' => __DIR__ . '/../..' . '/src/Kql/Interceptors/Toolkit/Obj.php', - 'Kirby\\Kql\\Kql' => __DIR__ . '/../..' . '/src/Kql/Kql.php', - 'Kirby\\Kql\\Query' => __DIR__ . '/../..' . '/src/Kql/Query.php', - ); - - public static function getInitializer(ClassLoader $loader) - { - return \Closure::bind(function () use ($loader) { - $loader->prefixLengthsPsr4 = ComposerStaticInitce81ad819bd8a4fdf01799db40d8b86b::$prefixLengthsPsr4; - $loader->prefixDirsPsr4 = ComposerStaticInitce81ad819bd8a4fdf01799db40d8b86b::$prefixDirsPsr4; - $loader->classMap = ComposerStaticInitce81ad819bd8a4fdf01799db40d8b86b::$classMap; - - }, null, ClassLoader::class); - } -} diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php deleted file mode 100644 index 3966dd6..0000000 --- a/vendor/composer/installed.php +++ /dev/null @@ -1,32 +0,0 @@ - array( - 'name' => 'getkirby/kql', - 'pretty_version' => '1.1.0', - 'version' => '1.1.0.0', - 'reference' => NULL, - 'type' => 'kirby-plugin', - 'install_path' => __DIR__ . '/../../', - 'aliases' => array(), - 'dev' => true, - ), - 'versions' => array( - 'getkirby/composer-installer' => array( - 'pretty_version' => '1.2.1', - 'version' => '1.2.1.0', - 'reference' => 'c98ece30bfba45be7ce457e1102d1b169d922f3d', - 'type' => 'composer-plugin', - 'install_path' => __DIR__ . '/../getkirby/composer-installer', - 'aliases' => array(), - 'dev_requirement' => false, - ), - 'getkirby/kql' => array( - 'pretty_version' => '1.1.0', - 'version' => '1.1.0.0', - 'reference' => NULL, - 'type' => 'kirby-plugin', - 'install_path' => __DIR__ . '/../../', - 'aliases' => array(), - 'dev_requirement' => false, - ), - ), -); From 69e501a9a426a756199b7f16cfdba2c0c0038f48 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sun, 20 Nov 2022 13:58:26 +0100 Subject: [PATCH 03/28] Fix test setup --- .php-cs-fixer.dist.php | 10 +- composer.json | 11 +- index.php | 5 +- phpunit.xml.dist | 35 +++--- tests/{ => Kql}/InterceptorTest.php | 26 ++--- tests/Kql/KqlTest.php | 158 ++++++++++++++++++++++++++++ tests/bootstrap.php | 19 +--- 7 files changed, 213 insertions(+), 51 deletions(-) rename tests/{ => Kql}/InterceptorTest.php (86%) create mode 100644 tests/Kql/KqlTest.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 5dc5e7a..20b1ade 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -1,19 +1,19 @@ exclude('dependencies') - ->exclude('panel/node_modules') + ->exclude('node_modules') ->in(__DIR__); $config = new PhpCsFixer\Config(); return $config ->setRules([ - '@PSR12' => true, + '@PSR1' => true, + '@PSR2' => true, 'align_multiline_comment' => ['comment_type' => 'phpdocs_like'], 'array_indentation' => true, 'array_syntax' => ['syntax' => 'short'], 'cast_spaces' => ['space' => 'none'], - // 'class_keyword_remove' => true, // replaces static::class with 'static' (won't work) + 'class_keyword_remove' => true, 'combine_consecutive_issets' => true, 'combine_consecutive_unsets' => true, 'combine_nested_dirname' => true, @@ -44,7 +44,7 @@ 'no_unused_imports' => true, 'no_useless_return' => true, 'ordered_imports' => ['sort_algorithm' => 'alpha'], - // 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], // adds params in the wrong order + 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], 'phpdoc_align' => ['align' => 'left'], 'phpdoc_indent' => true, 'phpdoc_scalar' => true, diff --git a/composer.json b/composer.json index 24f5edf..60032ae 100755 --- a/composer.json +++ b/composer.json @@ -44,7 +44,16 @@ "kirby-cms-path": false }, "scripts": { + "analyze": [ + "@analyze:composer" + ], + "analyze:composer": "composer validate --strict --no-check-version --no-check-all", + "ci": [ + "@fix", + "@analyze", + "@test" + ], "fix": "php-cs-fixer fix", - "test": "phpunit" + "test": "phpunit --stderr --coverage-html=tests/coverage" } } diff --git a/index.php b/index.php index abbbf88..6c612e3 100644 --- a/index.php +++ b/index.php @@ -1,5 +1,8 @@ [ 'routes' => function ($kirby) { return [ diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c7d5502..a0cdeb0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,13 +1,24 @@ - - - - - ./src - - - - - ./tests/ - - + + + + + + ./src + + + + + + ./tests/ + + diff --git a/tests/InterceptorTest.php b/tests/Kql/InterceptorTest.php similarity index 86% rename from tests/InterceptorTest.php rename to tests/Kql/InterceptorTest.php index 8bb146e..7a39aaa 100644 --- a/tests/InterceptorTest.php +++ b/tests/Kql/InterceptorTest.php @@ -52,7 +52,7 @@ public function objectProvider() ], [ new Blueprint([ - 'model' => new Page([ + 'model' => $page = new Page([ 'slug' => 'test' ]), 'name' => 'test', @@ -68,40 +68,34 @@ public function objectProvider() 'Kirby\\Kql\\Interceptors\\Cms\\Field' ], [ - new File(['filename' => 'test.jpg']), + $file = new File(['filename' => 'test.jpg', 'parent' => $page]), 'Kirby\\Kql\\Interceptors\\Cms\\File' ], [ new FileBlueprint([ - 'model' => new File([ - 'filename' => 'test.jpg' - ]), + 'model' => $file, 'name' => 'test', ]), 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint' ], [ - new FileExtended(['filename' => 'test.jpg']), + new FileExtended(['filename' => 'test.jpg', 'parent' => $page]), 'Kirby\\Kql\\Interceptors\\Cms\\File' ], [ new FileVersion([ - 'original' => new File([ - 'filename' => 'test.jpg', - ]), + 'original' => $file, 'url' => '/test.jpg' ]), 'Kirby\\Kql\\Interceptors\\Cms\\FileVersion' ], [ - new Page(['slug' => 'test']), + $page, 'Kirby\\Kql\\Interceptors\\Cms\\Page' ], [ new PageBlueprint([ - 'model' => new Page([ - 'slug' => 'test' - ]), + 'model' => $page, 'name' => 'test', ]), 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint' @@ -130,12 +124,12 @@ public function objectProvider() 'Kirby\\Kql\\Interceptors\\Cms\\Site' ], [ - new User(['email' => 'test@getkirby.com']), + $user = new User(['email' => 'test@getkirby.com']), 'Kirby\\Kql\\Interceptors\\Cms\\User' ], [ new UserBlueprint([ - 'model' => new User(['email' => 'test@getkirby.com']), + 'model' => $user, 'name' => 'test', ]), 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint' @@ -149,6 +143,8 @@ public function objectProvider() /** * @dataProvider objectProvider + * @param mixed $object + * @param mixed $inspector */ public function testReplace($object, $inspector) { diff --git a/tests/Kql/KqlTest.php b/tests/Kql/KqlTest.php new file mode 100644 index 0000000..ed7daab --- /dev/null +++ b/tests/Kql/KqlTest.php @@ -0,0 +1,158 @@ +app = new App([ + 'roots' => [ + 'index' => '/dev/null' + ], + 'site' => [ + 'children' => [ + [ + 'slug' => 'projects' + ], + [ + 'slug' => 'about' + ], + [ + 'slug' => 'contact' + ] + ], + 'content' => [ + 'title' => 'Test Site' + ], + ] + ]); + } + + public function testForbiddenMethod() + { + $this->expectException("Kirby\Exception\PermissionException"); + $this->expectExceptionMessage('The method "Kirby\Cms\Page::delete()" is not allowed in the API context'); + $result = Kql::run('site.children.first.delete'); + } + + public function testQuery() + { + $result = Kql::run([ + 'query' => 'site.children', + 'select' => 'slug' + ]); + + $expected = [ + [ + 'slug' => 'projects', + ], + [ + 'slug' => 'about', + ], + [ + 'slug' => 'contact', + ] + ]; + + $this->assertSame($expected, $result); + } + + public function testRun() + { + $result = Kql::run('site.title'); + $expected = 'Test Site'; + + $this->assertSame($expected, $result); + } + + public function testSelectWithAlias() + { + $result = Kql::run([ + 'select' => [ + 'myTitle' => 'site.title' + ] + ]); + + $expected = [ + 'myTitle' => 'Test Site', + ]; + + $this->assertSame($expected, $result); + } + + public function testSelectWithArray() + { + $result = Kql::run([ + 'select' => ['title', 'url'] + ]); + + $expected = [ + 'title' => 'Test Site', + 'url' => '/' + ]; + + $this->assertSame($expected, $result); + } + + public function testSelectWithBoolean() + { + $result = Kql::run([ + 'select' => [ + 'title' => true + ] + ]); + + $expected = [ + 'title' => 'Test Site' + ]; + + $this->assertSame($expected, $result); + } + + public function testSelectWithQuery() + { + $result = Kql::run([ + 'select' => [ + 'children' => [ + 'query' => 'site.children', + 'select' => 'slug' + ] + ] + ]); + + $expected = [ + 'children' => [ + [ + 'slug' => 'projects', + ], + [ + 'slug' => 'about', + ], + [ + 'slug' => 'contact', + ] + ] + ]; + + $this->assertSame($expected, $result); + } + + public function testSelectWithString() + { + $result = Kql::run([ + 'select' => [ + 'title' => 'site.title.upper' + ] + ]); + + $expected = [ + 'title' => 'TEST SITE' + ]; + + $this->assertSame($expected, $result); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 518b666..f9b4c49 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -6,20 +6,5 @@ ini_set('display_errors', 'on'); ini_set('display_startup_errors', 'on'); -require_once dirname(__DIR__, 1) . '/vendor/autoload.php'; - -// regular setup -$bootstrapper = dirname(__DIR__, 4) . '/kirby/bootstrap.php'; - -if (is_file($bootstrapper)) { - require_once $bootstrapper; -} - -// sandbox -$bootstrapper = dirname(__DIR__, 5) . '/kirby/bootstrap.php'; - -if (is_file($bootstrapper)) { - require_once $bootstrapper; -} - -kirby(); +require_once __DIR__ . '/../vendor/autoload.php'; +require_once __DIR__ . '/../index.php'; From 75c2849d7b27f75ba05ab40b57c6b3edef8ccb10 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sun, 20 Nov 2022 14:14:53 +0100 Subject: [PATCH 04/28] Clean up classes --- index.php | 1 + src/Kql/Help.php | 85 +++++---- src/Kql/Interceptor.php | 219 +++++++++++++++++++++-- src/Kql/Interceptors/Cms/App.php | 2 +- src/Kql/Interceptors/Cms/Blueprint.php | 2 +- src/Kql/Interceptors/Cms/Collection.php | 2 +- src/Kql/Interceptors/Cms/Content.php | 2 +- src/Kql/Interceptors/Cms/Field.php | 2 +- src/Kql/Interceptors/Cms/Model.php | 2 +- src/Kql/Interceptors/Cms/Role.php | 2 +- src/Kql/Interceptors/Cms/Translation.php | 2 +- src/Kql/Interceptors/Interceptor.php | 174 ------------------ src/Kql/Interceptors/Panel/Model.php | 2 +- src/Kql/Interceptors/Toolkit/Obj.php | 2 +- src/Kql/Kql.php | 104 ++++++----- src/Kql/Query.php | 4 + 16 files changed, 333 insertions(+), 274 deletions(-) delete mode 100644 src/Kql/Interceptors/Interceptor.php diff --git a/index.php b/index.php index 6c612e3..2145df9 100644 --- a/index.php +++ b/index.php @@ -6,6 +6,7 @@ @include_once __DIR__ . '/vendor/autoload.php'; class_alias('Kirby\Kql\Kql', 'Kql'); +class_alias('Kirby\Kql\Interceptor', 'Kirby\Kql\Interceptors\Interceptor'); function kql($input, $model = null) { diff --git a/src/Kql/Help.php b/src/Kql/Help.php index eda82cf..c2fcde8 100644 --- a/src/Kql/Help.php +++ b/src/Kql/Help.php @@ -4,25 +4,43 @@ use ReflectionMethod; +/** + * Providing help information about + * queried objects, methods, arrays... + * + * @package Kirby KQL + * @author Bastian Allgeier + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://getkirby.com/license + */ class Help { - public static function for($object) + + /** + * Provides information about passed value + * depending on its type + */ + public static function for($value): array { - if (is_array($object) === true) { - return static::forArray($object); + if (is_array($value) === true) { + return static::forArray($value); } - if (is_object($object) === true) { - return static::forObject($object); + if (is_object($value) === true) { + return static::forObject($value); } return [ - 'type' => gettype($object), - 'value' => $object + 'type' => gettype($value), + 'value' => $value ]; } - public static function forArray(array $array) + /** + * @internal + */ + public static function forArray(array $array): array { return [ 'type' => 'array', @@ -30,42 +48,40 @@ public static function forArray(array $array) ]; } - public static function forMethod($object, $method) + /** + * @internal + */ + public static function forMethod(object $object, string $method): array { $reflection = new ReflectionMethod($object, $method); - $returns = null; + $returns = $reflection->getReturnType()?->getName(); $params = []; - if ($returnType = $reflection->getReturnType()) { - $returns = $returnType->getName(); - } - foreach ($reflection->getParameters() as $param) { - $p = [ - 'name' => $param->getName(), - 'required' => $param->isOptional() === false, - 'type' => $param->hasType() ? $param->getType()->getName() : null, - ]; + $name = $param->getName(); + $required = $param->isOptional() === false; + $type = $param->hasType() ? $param->getType()->getName() : null; + $default = null; if ($param->isDefaultValueAvailable()) { - $p['default'] = $param->getDefaultValue(); + $default = $param->getDefaultValue(); } - $call = null; + $call = ''; - if ($p['type'] !== null) { - $call = $p['type'] . ' '; + if ($type !== null) { + $call = $type . ' '; } - $call .= '$' . $p['name']; + $call .= '$' . $name; - if ($p['required'] === false && isset($p['default']) === true) { - $call .= ' = ' . var_export($p['default'], true); + if ($required === false && $default === true) { + $call .= ' = ' . var_export($default, true); } $p['call'] = $call; - $params[$p['name']] = $p; + $params[$name] = compact('name', 'type', 'required', 'default', 'call'); } $call = '.' . $method; @@ -82,7 +98,10 @@ public static function forMethod($object, $method) ]; } - public static function forMethods($object, $methods) + /** + * @internal + */ + public static function forMethods(object $object, array $methods): array { $methods = array_unique($methods); $reflection = []; @@ -100,11 +119,11 @@ public static function forMethods($object, $methods) return $reflection; } - public static function forObject($object) + /** + * @internal + */ + public static function forObject(object $object): array { - $original = $object; - $object = Interceptor::replace($original); - - return $object->__debugInfo(); + return Interceptor::replace($object)->__debugInfo(); } } diff --git a/src/Kql/Interceptor.php b/src/Kql/Interceptor.php index ee14959..4db85d0 100644 --- a/src/Kql/Interceptor.php +++ b/src/Kql/Interceptor.php @@ -2,44 +2,215 @@ namespace Kirby\Kql; +use Closure; use Exception; +use Kirby\Cms\App; use Kirby\Exception\PermissionException; +use Kirby\Kql\Help; +use Kirby\Kql\Kql; +use Kirby\Toolkit\Str; +use ReflectionFunction; +use ReflectionMethod; +use Throwable; -class Interceptor +/** + * Base class for proxying core classes to + * intercept method calls that are not allowed + * on the related core class + * + * @package Kirby KQL + * @author Bastian Allgeier + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://getkirby.com/license + */ +abstract class Interceptor { + public const CLASS_ALIAS = null; + + protected $toArray = []; + + public function __construct(protected $object) + {} + + public function __call($method, array $args = []) + { + if ($this->isAllowedMethod($method) === true) { + return $this->object->$method(...$args); + } + + $this->forbiddenMethod($method); + } + + public function allowedMethods(): array + { + return []; + } + + protected static function class(string $class): string + { + return str_replace('Kirby\\', 'Kirby\\Kql\\Interceptors\\', $class); + } + + protected function forbiddenMethod(string $method) + { + $name = get_class($this->object) . '::' . $method . '()'; + throw new PermissionException('The method "' . $name . '" is not allowed in the API context'); + } + + /** + * Returns a registered method by name, either from + * the current class or from a parent class ordered by + * inheritance order (top to bottom) + */ + protected function getMethod(string $method): Closure|null + { + if (isset($this->object::$methods[$method]) === true) { + return $this->object::$methods[$method]; + } + + foreach (class_parents($this->object) as $parent) { + if (isset($parent::$methods[$method]) === true) { + return $parent::$methods[$method]; + } + } + + return null; + } + + protected function isAllowedCallable($method): bool + { + try { + $ref = match(true) { + $method instanceof Closure + => new ReflectionFunction($method), + is_string($method) === true + => new ReflectionMethod($this->object, $method), + default + => throw new Exception('Invalid method') + + }; + + if ($comment = $ref->getDocComment()) { + if (Str::contains($comment, '@kql-allowed') === true) { + return true; + } + } + } catch (Throwable) {} + + return false; + } + + protected function isAllowedMethod($method) + { + $kirby = App::instance(); + $name = strtolower(get_class($this->object) . '::' . $method); + + // get list of blocked methods from config + $blocked = $kirby->option('kql.methods.blocked', []); + $blocked = array_map('strtolower', $blocked); + + // check in the block list from the config + if (in_array($name, $blocked) === true) { + return false; + } + + // check in class allow list + if (in_array($method, $this->allowedMethods()) === true) { + return true; + } + + // get list of explicitly allowed methods from config + $allowed = $kirby->option('kql.methods.allowed', []); + $allowed = array_map('strtolower', $allowed); + + // check in the allow list from the config + if (in_array($name, $allowed) === true) { + return true; + } + + // support for model methods with docblock comment + if ($this->isAllowedCallable($method) === true) { + return true; + } + + // support for custom methods with docblock comment + if ($this->isAllowedCustomMethod($method) === true) { + return true; + } + + return false; + } + + protected function isAllowedCustomMethod(string $method): bool + { + // has no custom methods + if (property_exists($this->object, 'methods') === false) { + return false; + } + + // does not have that method + if (!$call = $this->getMethod($method)) { + return false; + } + + // check for a docblock comment + if ($this->isAllowedCallable($call) === true) { + return true; + } + + return false; + } + + public function __debugInfo(): array + { + $help = Help::forMethods($this->object, $this->allowedMethods()); + + return [ + 'type' => $this::CLASS_ALIAS, + 'methods' => $help, + 'value' => $this->toArray() + ]; + } + public static function replace($object) { if (is_object($object) === false) { throw new Exception('Unsupported value: ' . gettype($object)); } - $className = get_class($object); - $fullName = strtolower($className); - $blocked = array_map('strtolower', option('kql.classes.blocked', [])); + $kirby = App::instance(); + $class = get_class($object); + $name = strtolower($class); + + // get list of blocked classes from config + $blocked = $kirby->option('kql.classes.blocked', []); + $blocked = array_map('strtolower', $blocked); // check in the block list from the config - if (in_array($fullName, $blocked) === true) { - throw new PermissionException('Access to the class "' . $className . '" is blocked'); + if (in_array($name, $blocked) === true) { + throw new PermissionException('Access to the class "' . $class . '" is blocked'); } // directly return interceptor objects - if (is_a($object, 'Kirby\\Kql\\Interceptors\\Interceptor') === true) { + if ($object instanceof Interceptor) { return $object; } // check for an interceptor class - $interceptors = array_change_key_case(option('kql.interceptors', []), CASE_LOWER); + $interceptors = $kirby->option('kql.interceptors', []); + $interceptors = array_change_key_case($interceptors, CASE_LOWER); // load an interceptor from config if it exists and otherwise fall back to a built-in interceptor - $interceptor = $interceptors[$fullName] ?? str_replace('Kirby\\', 'Kirby\\Kql\\Interceptors\\', $className); + $interceptor = $interceptors[$name] ?? static::class($class); // check for a valid interceptor class - if ($className !== $interceptor && class_exists($interceptor) === true) { + if ($class !== $interceptor && class_exists($interceptor) === true) { return new $interceptor($object); } // go through parents of the current object to use their interceptors as fallback foreach (class_parents($object) as $parent) { - $interceptor = str_replace('Kirby\\', 'Kirby\\Kql\\Interceptors\\', $parent); + $interceptor = static::class($parent); if (class_exists($interceptor) === true) { return new $interceptor($object); @@ -47,13 +218,33 @@ public static function replace($object) } // check for a class in the allow list - $allowed = array_map('strtolower', option('kql.classes.allowed', [])); + $allowed = $kirby->option('kql.classes.allowed', []); + $allowed = array_map('strtolower', $allowed); // return the plain object if it is allowed - if (in_array($fullName, $allowed) === true) { + if (in_array($name, $allowed) === true) { return $object; } - throw new PermissionException('Access to the class "' . $className . '" is not supported'); + throw new PermissionException('Access to the class "' . $class . '" is not supported'); + } + + public function toArray(): array|null + { + $toArray = []; + + // filter methods which cannot be called + foreach ($this->toArray as $method) { + if ($this->isAllowedMethod($method) === true) { + $toArray[] = $method; + } + } + + return Kql::select($this, $toArray); + } + + public function toResponse() + { + return $this->toArray(); } } diff --git a/src/Kql/Interceptors/Cms/App.php b/src/Kql/Interceptors/Cms/App.php index 33acb65..e984dbb 100644 --- a/src/Kql/Interceptors/Cms/App.php +++ b/src/Kql/Interceptors/Cms/App.php @@ -2,7 +2,7 @@ namespace Kirby\Kql\Interceptors\Cms; -use Kirby\Kql\Interceptors\Interceptor; +use Kirby\Kql\Interceptor; class App extends Interceptor { diff --git a/src/Kql/Interceptors/Cms/Blueprint.php b/src/Kql/Interceptors/Cms/Blueprint.php index 016a6a1..d05aea0 100644 --- a/src/Kql/Interceptors/Cms/Blueprint.php +++ b/src/Kql/Interceptors/Cms/Blueprint.php @@ -2,7 +2,7 @@ namespace Kirby\Kql\Interceptors\Cms; -use Kirby\Kql\Interceptors\Interceptor; +use Kirby\Kql\Interceptor; class Blueprint extends Interceptor { diff --git a/src/Kql/Interceptors/Cms/Collection.php b/src/Kql/Interceptors/Cms/Collection.php index 5d7dec6..0608365 100644 --- a/src/Kql/Interceptors/Cms/Collection.php +++ b/src/Kql/Interceptors/Cms/Collection.php @@ -2,7 +2,7 @@ namespace Kirby\Kql\Interceptors\Cms; -use Kirby\Kql\Interceptors\Interceptor; +use Kirby\Kql\Interceptor; class Collection extends Interceptor { diff --git a/src/Kql/Interceptors/Cms/Content.php b/src/Kql/Interceptors/Cms/Content.php index 35e2309..86ff6e0 100644 --- a/src/Kql/Interceptors/Cms/Content.php +++ b/src/Kql/Interceptors/Cms/Content.php @@ -2,7 +2,7 @@ namespace Kirby\Kql\Interceptors\Cms; -use Kirby\Kql\Interceptors\Interceptor; +use Kirby\Kql\Interceptor; class Content extends Interceptor { diff --git a/src/Kql/Interceptors/Cms/Field.php b/src/Kql/Interceptors/Cms/Field.php index 757ade9..e0e8dce 100644 --- a/src/Kql/Interceptors/Cms/Field.php +++ b/src/Kql/Interceptors/Cms/Field.php @@ -2,7 +2,7 @@ namespace Kirby\Kql\Interceptors\Cms; -use Kirby\Kql\Interceptors\Interceptor; +use Kirby\Kql\Interceptor; class Field extends Interceptor { diff --git a/src/Kql/Interceptors/Cms/Model.php b/src/Kql/Interceptors/Cms/Model.php index 7e23ca4..79e51ab 100644 --- a/src/Kql/Interceptors/Cms/Model.php +++ b/src/Kql/Interceptors/Cms/Model.php @@ -2,7 +2,7 @@ namespace Kirby\Kql\Interceptors\Cms; -use Kirby\Kql\Interceptors\Interceptor; +use Kirby\Kql\Interceptor; class Model extends Interceptor { diff --git a/src/Kql/Interceptors/Cms/Role.php b/src/Kql/Interceptors/Cms/Role.php index 8446550..ba52e3a 100644 --- a/src/Kql/Interceptors/Cms/Role.php +++ b/src/Kql/Interceptors/Cms/Role.php @@ -2,7 +2,7 @@ namespace Kirby\Kql\Interceptors\Cms; -use Kirby\Kql\Interceptors\Interceptor; +use Kirby\Kql\Interceptor; class Role extends Interceptor { diff --git a/src/Kql/Interceptors/Cms/Translation.php b/src/Kql/Interceptors/Cms/Translation.php index 03a89a2..5c0d75a 100755 --- a/src/Kql/Interceptors/Cms/Translation.php +++ b/src/Kql/Interceptors/Cms/Translation.php @@ -2,7 +2,7 @@ namespace Kirby\Kql\Interceptors\Cms; -use Kirby\Kql\Interceptors\Interceptor; +use Kirby\Kql\Interceptor; class Translation extends Interceptor { diff --git a/src/Kql/Interceptors/Interceptor.php b/src/Kql/Interceptors/Interceptor.php deleted file mode 100644 index c48e952..0000000 --- a/src/Kql/Interceptors/Interceptor.php +++ /dev/null @@ -1,174 +0,0 @@ -object = $object; - } - - public function __call($method, array $args = []) - { - if ($this->isAllowedMethod($method) === true) { - return $this->object->$method(...$args); - } - - $this->forbiddenMethod($method); - } - - public function allowedMethods(): array - { - return []; - } - - protected function forbiddenMethod(string $method) - { - $className = get_class($this->object); - throw new PermissionException('The method "' . $className . '::' . $method . '()" is not allowed in the API context'); - } - - /** - * Returns a registered method by name, either from - * the current class or from a parent class ordered by - * inheritance order (top to bottom) - * - * @param string $method - * @return \Closure|null - */ - protected function getMethod(string $method) - { - if (isset($this->object::$methods[$method]) === true) { - return $this->object::$methods[$method]; - } - - foreach (class_parents($this->object) as $parent) { - if (isset($parent::$methods[$method]) === true) { - return $parent::$methods[$method]; - } - } - - return null; - } - - protected function isAllowedCallable($method): bool - { - try { - if (is_a($method, 'Closure') === true) { - $ref = new ReflectionFunction($method); - } elseif (is_string($method) === true) { - $ref = new ReflectionMethod($this->object, $method); - } else { - throw new Exception('Invalid method'); - } - - if ($comment = $ref->getDocComment()) { - if (Str::contains($comment, '@kql-allowed') === true) { - return true; - } - } - } catch (Throwable $e) { - return false; - } - - return false; - } - - protected function isAllowedMethod($method) - { - $fullName = strtolower(get_class($this->object) . '::' . $method); - $blocked = array_map('strtolower', option('kql.methods.blocked', [])); - - // check in the block list from the config - if (in_array($fullName, $blocked) === true) { - return false; - } - - // check in class allow list - if (in_array($method, $this->allowedMethods()) === true) { - return true; - } - - $allowed = array_map('strtolower', option('kql.methods.allowed', [])); - - // check in the allow list from the config - if (in_array($fullName, $allowed) === true) { - return true; - } - - // support for model methods with docblock comment - if ($this->isAllowedCallable($method) === true) { - return true; - } - - // support for custom methods with docblock comment - if ($this->isAllowedCustomMethod($method) === true) { - return true; - } - - return false; - } - - protected function isAllowedCustomMethod(string $method): bool - { - // has no custom methods - if (property_exists($this->object, 'methods') === false) { - return false; - } - - // does not have that method - if (!$call = $this->getMethod($method)) { - return false; - } - - // check for a docblock comment - if ($this->isAllowedCallable($call) === true) { - return true; - } - - return false; - } - - public function __debugInfo(): array - { - return [ - 'type' => $this::CLASS_ALIAS, - 'methods' => Help::forMethods($this->object, $this->allowedMethods()), - 'value' => $this->toArray() - ]; - } - - public function toArray(): ?array - { - $toArray = []; - - // filter methods which cannot be called - foreach ($this->toArray as $method) { - if ($this->isAllowedMethod($method) === true) { - $toArray[] = $method; - } - } - - return Kql::select($this, $toArray); - } - - public function toResponse() - { - return $this->toArray(); - } -} diff --git a/src/Kql/Interceptors/Panel/Model.php b/src/Kql/Interceptors/Panel/Model.php index 6c0bbda..006d47f 100755 --- a/src/Kql/Interceptors/Panel/Model.php +++ b/src/Kql/Interceptors/Panel/Model.php @@ -2,7 +2,7 @@ namespace Kirby\Kql\Interceptors\Panel; -use Kirby\Kql\Interceptors\Interceptor; +use Kirby\Kql\Interceptor; class Model extends Interceptor { diff --git a/src/Kql/Interceptors/Toolkit/Obj.php b/src/Kql/Interceptors/Toolkit/Obj.php index ba131f0..0ac2861 100755 --- a/src/Kql/Interceptors/Toolkit/Obj.php +++ b/src/Kql/Interceptors/Toolkit/Obj.php @@ -2,7 +2,7 @@ namespace Kirby\Kql\Interceptors\Toolkit; -use Kirby\Kql\Interceptors\Interceptor; +use Kirby\Kql\Interceptor; class Obj extends Interceptor { diff --git a/src/Kql/Kql.php b/src/Kql/Kql.php index 9630046..caa7bcd 100644 --- a/src/Kql/Kql.php +++ b/src/Kql/Kql.php @@ -3,51 +3,26 @@ namespace Kirby\Kql; use Exception; +use Kirby\Cms\App; use Kirby\Cms\Collection; use Kirby\Toolkit\Str; +/** + * ... + * + * @package Kirby KQL + * @author Bastian Allgeier + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://getkirby.com/license + */ class Kql { - public static function help($object) + public static function help($object): array { return Help::for($object); } - public static function run($input, $model = null) - { - // string queries - if (is_string($input) === true) { - $result = static::query($input, $model); - return static::render($result); - } - - // multiple queries - if (isset($input['queries']) === true) { - $result = []; - - foreach ($input['queries'] as $name => $query) { - $result[$name] = static::run($query); - } - - return $result; - } - - $query = $input['query'] ?? 'site'; - $select = $input['select'] ?? null; - $options = [ - 'pagination' => $input['pagination'] ?? null, - ]; - - // check for invalid queries - if (is_string($query) === false) { - throw new Exception('The query must be a string'); - } - - $result = static::query($query, $model); - - return static::select($result, $select, $options); - } - public static function fetch($model, $key, $selection) { // simple key/value @@ -56,8 +31,15 @@ public static function fetch($model, $key, $selection) } // selection without additional query - if (is_array($selection) === true && empty($selection['query']) === true) { - return static::select($model->$key(), $selection['select'] ?? null, $selection['options'] ?? []); + if ( + is_array($selection) === true && + empty($selection['query']) === true + ) { + return static::select( + $model->$key(), + $selection['select'] ?? null, + $selection['options'] ?? [] + ); } // nested queries @@ -66,16 +48,16 @@ public static function fetch($model, $key, $selection) public static function query(string $query, $model = null) { - $kirby = kirby(); - $site = $kirby->site(); - $model = $model ?? $site; + $model ??= App::instance()->site(); + $data = [$model::CLASS_ALIAS => $model]; - return Query::factory($query)->resolve([$model::CLASS_ALIAS => $model]); + return Query::factory($query)->resolve($data); } public static function render($value) { if (is_object($value) === true) { + // replace actual object with intercepting proxy class $object = Interceptor::replace($value); if (method_exists($object, 'toResponse') === true) { @@ -92,6 +74,41 @@ public static function render($value) return $value; } + public static function run($input, $model = null) + { + // string queries + if (is_string($input) === true) { + $result = static::query($input, $model); + return static::render($result); + } + + // multiple queries + if (isset($input['queries']) === true) { + $result = []; + + foreach ($input['queries'] as $name => $query) { + $result[$name] = static::run($query); + } + + return $result; + } + + $query = $input['query'] ?? 'site'; + $select = $input['select'] ?? null; + $options = [ + 'pagination' => $input['pagination'] ?? null, + ]; + + // check for invalid queries + if (is_string($query) === false) { + throw new Exception('The query must be a string'); + } + + $result = static::query($query, $model); + + return static::select($result, $select, $options); + } + public static function select($data, $select, array $options = []) { if ($select === null) { @@ -102,7 +119,7 @@ public static function select($data, $select, array $options = []) return static::help($data); } - if (is_a($data, 'Kirby\Cms\Collection') === true) { + if ($data instanceof Collection) { return static::selectFromCollection($data, $select, $options); } @@ -165,6 +182,7 @@ public static function selectFromCollection(Collection $collection, $select, arr public static function selectFromObject($object, $select, array $options = []) { + // replace actual object with intercepting proxy class $object = Interceptor::replace($object); $result = []; diff --git a/src/Kql/Query.php b/src/Kql/Query.php index 0dc6385..2fdc36b 100644 --- a/src/Kql/Query.php +++ b/src/Kql/Query.php @@ -18,6 +18,10 @@ class Query extends BaseQuery { public function intercept(mixed $result): mixed { + if (is_object($result) === false) { + return $result; + } + return Interceptor::replace($result); } } From 5341b6b93af3c7ddfb1973452ea93ad45b21d5c7 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sun, 20 Nov 2022 14:58:17 +0100 Subject: [PATCH 05/28] Move extensions to their own file --- extensions/aliases.php | 4 ++++ extensions/api.php | 25 +++++++++++++++++++++++++ extensions/helpers.php | 8 ++++++++ index.php | 31 +++---------------------------- 4 files changed, 40 insertions(+), 28 deletions(-) create mode 100644 extensions/aliases.php create mode 100644 extensions/api.php create mode 100644 extensions/helpers.php diff --git a/extensions/aliases.php b/extensions/aliases.php new file mode 100644 index 0000000..ff60603 --- /dev/null +++ b/extensions/aliases.php @@ -0,0 +1,4 @@ + function ($kirby) { + return [ + [ + 'pattern' => 'query', + 'method' => 'POST|GET', + 'auth' => $kirby->option('kql.auth') === false ? false : true, + 'action' => function () use ($kirby) { + $input = $kirby->request()->get(); + $result = Kql::run($input); + + return [ + 'code' => 200, + 'result' => $result, + 'status' => 'ok', + ]; + } + ] + ]; + } +]; diff --git a/extensions/helpers.php b/extensions/helpers.php new file mode 100644 index 0000000..2bbe75f --- /dev/null +++ b/extensions/helpers.php @@ -0,0 +1,8 @@ + [ - 'routes' => function ($kirby) { - return [ - [ - 'pattern' => 'query', - 'method' => 'POST|GET', - 'auth' => $kirby->option('kql.auth') === false ? false : true, - 'action' => function () { - $result = Kql::run(get()); - - return [ - 'code' => 200, - 'result' => $result, - 'status' => 'ok', - ]; - } - ] - ]; - } - ] + 'api' => require_once 'extensions/api.php' ]); From ee83af8abd5ea5e789af75354cd467bc9f58a44c Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sun, 20 Nov 2022 15:25:19 +0100 Subject: [PATCH 06/28] Unit tests for `Help` class --- composer.json | 5 +- phpunit.xml.dist | 6 +- src/Kql/Help.php | 19 ++++- src/Kql/Interceptor.php | 28 ++++--- tests/Kql/HelpTest.php | 142 ++++++++++++++++++++++++++++++++++ tests/Kql/InterceptorTest.php | 20 +++-- tests/Kql/KqlTest.php | 61 ++++++++------- tests/Kql/TestCase.php | 37 +++++++++ 8 files changed, 271 insertions(+), 47 deletions(-) create mode 100644 tests/Kql/HelpTest.php create mode 100644 tests/Kql/TestCase.php diff --git a/composer.json b/composer.json index 60032ae..66d3690 100755 --- a/composer.json +++ b/composer.json @@ -36,7 +36,10 @@ }, "autoload": { "psr-4": { - "Kirby\\": "src/" + "Kirby\\": [ + "src/", + "tests/" + ] } }, "extra": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a0cdeb0..62ddef2 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -18,7 +18,11 @@ - ./tests/ + ./tests + + + + diff --git a/src/Kql/Help.php b/src/Kql/Help.php index c2fcde8..5a6f466 100644 --- a/src/Kql/Help.php +++ b/src/Kql/Help.php @@ -2,6 +2,8 @@ namespace Kirby\Kql; +use Kirby\Toolkit\A; +use ReflectionClass; use ReflectionMethod; /** @@ -124,6 +126,21 @@ public static function forMethods(object $object, array $methods): array */ public static function forObject(object $object): array { - return Interceptor::replace($object)->__debugInfo(); + // get interceptor object to only return info on allowed methods + $interceptor = Interceptor::replace($object); + + if ($interceptor instanceof Interceptor) { + return $interceptor->__debugInfo(); + } + + // for original classes, use reflection + $class = new ReflectionClass($object); + $methods = $class->getMethods(); + $methods = A::map($methods, fn ($method) => static::forMethod($object, $method->getName())); + + return [ + 'type' => get_class($object), + 'methods' => $methods + ]; } } diff --git a/src/Kql/Interceptor.php b/src/Kql/Interceptor.php index 4db85d0..7753dc1 100644 --- a/src/Kql/Interceptor.php +++ b/src/Kql/Interceptor.php @@ -42,6 +42,17 @@ public function __call($method, array $args = []) $this->forbiddenMethod($method); } + public function __debugInfo(): array + { + $help = Help::forMethods($this->object, $this->allowedMethods()); + + return [ + 'type' => $this::CLASS_ALIAS, + 'methods' => $help, + 'value' => $this->toArray() + ]; + } + public function allowedMethods(): array { return []; @@ -162,17 +173,6 @@ protected function isAllowedCustomMethod(string $method): bool return false; } - public function __debugInfo(): array - { - $help = Help::forMethods($this->object, $this->allowedMethods()); - - return [ - 'type' => $this::CLASS_ALIAS, - 'methods' => $help, - 'value' => $this->toArray() - ]; - } - public static function replace($object) { if (is_object($object) === false) { @@ -183,6 +183,7 @@ public static function replace($object) $class = get_class($object); $name = strtolower($class); + // 1. Is $object class explicitly blocked? // get list of blocked classes from config $blocked = $kirby->option('kql.classes.blocked', []); $blocked = array_map('strtolower', $blocked); @@ -192,11 +193,13 @@ public static function replace($object) throw new PermissionException('Access to the class "' . $class . '" is blocked'); } + // 2. Is $object already an interceptor? // directly return interceptor objects if ($object instanceof Interceptor) { return $object; } + // 3. Does an interceptor class for $object exist? // check for an interceptor class $interceptors = $kirby->option('kql.interceptors', []); $interceptors = array_change_key_case($interceptors, CASE_LOWER); @@ -208,6 +211,7 @@ public static function replace($object) return new $interceptor($object); } + // 4. Also check for parent classes of $object // go through parents of the current object to use their interceptors as fallback foreach (class_parents($object) as $parent) { $interceptor = static::class($parent); @@ -217,6 +221,7 @@ public static function replace($object) } } + // 5. $object has no interceptor but is explicitly allowed? // check for a class in the allow list $allowed = $kirby->option('kql.classes.allowed', []); $allowed = array_map('strtolower', $allowed); @@ -226,6 +231,7 @@ public static function replace($object) return $object; } + // 6. None of the above? Block class. throw new PermissionException('Access to the class "' . $class . '" is not supported'); } diff --git a/tests/Kql/HelpTest.php b/tests/Kql/HelpTest.php new file mode 100644 index 0000000..b63ecc7 --- /dev/null +++ b/tests/Kql/HelpTest.php @@ -0,0 +1,142 @@ +assertSame(['type' => 'string', 'value' => 'foo'], $result); + + $result = Help::for(3); + $this->assertSame(['type' => 'integer', 'value' => 3], $result); + + $result = Help::for(3.0); + $this->assertSame(['type' => 'double', 'value' => 3.0], $result); + + $result = Help::for(false); + $this->assertSame(['type' => 'boolean', 'value' => false], $result); + } + + /** + * @covers ::for + * @covers ::forArray + */ + public function testForArray() + { + $result = Help::for(['foo' => 'bar', 'kirby' => 'cms']); + $this->assertSame(['type' => 'array', 'keys' => ['foo', 'kirby']], $result); + } + + /** + * @covers ::forMethod + */ + public function testForMethod() + { + $object = new TestObject(); + $result = Help::forMethod($object, 'foo'); + $this->assertSame([ + 'call' => '.foo(string $bar)', + 'name' => 'foo', + 'params' => [ + 'bar' => [ + 'name' => 'bar', + 'type' => 'string', + 'required' => false, + 'default' => 'hello', + 'call' => 'string $bar' + ] + ], + 'returns' => 'array' + ], $result); + } + + /** + * @covers ::forMethods + */ + public function testForMethods() + { + $object = new TestObject(); + $result = Help::forMethods($object, ['more', 'foo', 'more']); + $this->assertSame([ + 'foo' => [ + 'call' => '.foo(string $bar)', + 'name' => 'foo', + 'params' => [ + 'bar' => [ + 'name' => 'bar', + 'type' => 'string', + 'required' => false, + 'default' => 'hello', + 'call' => 'string $bar' + ] + ], + 'returns' => 'array' + ], + 'more' => [ + 'call' => '.more', + 'name' => 'more', + 'params' => [], + 'returns' => 'string' + ] + ], $result); + } + + /** + * @covers ::for + * @covers ::forObject + */ + public function testForObjectWithInterceptedObject() + { + $object = new Page(['slug' => 'test']); + $result = Help::forObject($object); + + + + $this->assertSame('page', $result['type']); + $this->assertArrayHasKey('methods', $result); + $this->assertArrayHasKey('value', $result); + } + + /** + * @covers ::for + * @covers ::forObject + */ + public function testForObjectWithOriginalObject() + { + $app = $this->app->clone([ + 'options' => [ + 'kql' => ['classes' => ['allowed' => ['Kirby\Kql\TestObject']]] + ] + ]); + + $object = new TestObject(); + $result = Help::forObject($object); + + $this->assertSame('Kirby\Kql\TestObject', $result['type']); + $this->assertCount(2, $result['methods']); + $this->assertSame('.foo(string $bar)', $result['methods'][0]['call']); + } +} diff --git a/tests/Kql/InterceptorTest.php b/tests/Kql/InterceptorTest.php index 7a39aaa..a9342bd 100644 --- a/tests/Kql/InterceptorTest.php +++ b/tests/Kql/InterceptorTest.php @@ -2,6 +2,7 @@ namespace Kirby\Kql; +use Exception; use Kirby\Cms\App; use Kirby\Cms\Blueprint; use Kirby\Cms\Content; @@ -16,7 +17,7 @@ use Kirby\Cms\SiteBlueprint; use Kirby\Cms\User; use Kirby\Cms\UserBlueprint; -use PHPUnit\Framework\TestCase; +use Kirby\Exception\PermissionException; class AppExtended extends App { @@ -37,6 +38,10 @@ class UserExtended extends User { } + +/** + * @coversDefaultClass \Kirby\Kql\Interceptor + */ class InterceptorTest extends TestCase { public function objectProvider() @@ -142,9 +147,8 @@ public function objectProvider() } /** + * @covers ::replace * @dataProvider objectProvider - * @param mixed $object - * @param mixed $inspector */ public function testReplace($object, $inspector) { @@ -152,17 +156,23 @@ public function testReplace($object, $inspector) $this->assertInstanceOf($inspector, $result); } + /** + * @covers ::replace + */ public function testReplaceNonObject() { - $this->expectException('Exception'); + $this->expectException(Exception::class); $this->expectExceptionMessage('Unsupported value: string'); $result = Interceptor::replace('hello'); } + /** + * @covers ::replace + */ public function testReplaceUnknownObject() { - $this->expectException('Kirby\Exception\PermissionException'); + $this->expectException(PermissionException::class); $this->expectExceptionMessage('Access to the class "stdClass" is not supported'); $object = new \stdClass(); diff --git a/tests/Kql/KqlTest.php b/tests/Kql/KqlTest.php index ed7daab..9709446 100644 --- a/tests/Kql/KqlTest.php +++ b/tests/Kql/KqlTest.php @@ -2,43 +2,27 @@ namespace Kirby\Kql; -use Kirby\Cms\App; -use PHPUnit\Framework\TestCase; +use Kirby\Exception\PermissionException; +/** + * @coversDefaultClass \Kirby\Kql\Kql + */ class KqlTest extends TestCase { - public function setUp(): void - { - $this->app = new App([ - 'roots' => [ - 'index' => '/dev/null' - ], - 'site' => [ - 'children' => [ - [ - 'slug' => 'projects' - ], - [ - 'slug' => 'about' - ], - [ - 'slug' => 'contact' - ] - ], - 'content' => [ - 'title' => 'Test Site' - ], - ] - ]); - } + /** + * @covers ::forbiddenMethod + */ public function testForbiddenMethod() { - $this->expectException("Kirby\Exception\PermissionException"); + $this->expectException(PermissionException::class); $this->expectExceptionMessage('The method "Kirby\Cms\Page::delete()" is not allowed in the API context'); - $result = Kql::run('site.children.first.delete'); + Kql::run('site.children.first.delete'); } + /** + * @covers ::query + */ public function testQuery() { $result = Kql::run([ @@ -61,6 +45,9 @@ public function testQuery() $this->assertSame($expected, $result); } + /** + * @covers ::run + */ public function testRun() { $result = Kql::run('site.title'); @@ -69,6 +56,9 @@ public function testRun() $this->assertSame($expected, $result); } + /** + * @covers ::select + */ public function testSelectWithAlias() { $result = Kql::run([ @@ -84,6 +74,10 @@ public function testSelectWithAlias() $this->assertSame($expected, $result); } + /** + * @covers ::select + * @covers ::selectFromArray + */ public function testSelectWithArray() { $result = Kql::run([ @@ -98,6 +92,9 @@ public function testSelectWithArray() $this->assertSame($expected, $result); } + /** + * @covers ::select + */ public function testSelectWithBoolean() { $result = Kql::run([ @@ -113,6 +110,11 @@ public function testSelectWithBoolean() $this->assertSame($expected, $result); } + /** + * @covers ::select + * @covers ::selectFromCollection + * @covers ::selectFromObject + */ public function testSelectWithQuery() { $result = Kql::run([ @@ -141,6 +143,9 @@ public function testSelectWithQuery() $this->assertSame($expected, $result); } + /** + * @covers ::select + */ public function testSelectWithString() { $result = Kql::run([ diff --git a/tests/Kql/TestCase.php b/tests/Kql/TestCase.php new file mode 100644 index 0000000..6236006 --- /dev/null +++ b/tests/Kql/TestCase.php @@ -0,0 +1,37 @@ +app = new App([ + 'roots' => [ + 'index' => '/dev/null' + ], + 'site' => [ + 'children' => [ + [ + 'slug' => 'projects' + ], + [ + 'slug' => 'about' + ], + [ + 'slug' => 'contact' + ] + ], + 'content' => [ + 'title' => 'Test Site' + ], + ] + ]); + } +} From 2ea143afc846497cff3ef1c988d14b01de5a5b20 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sun, 20 Nov 2022 15:34:10 +0100 Subject: [PATCH 07/28] Fix PHP CS fixer config --- .php-cs-fixer.dist.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 20b1ade..e126d67 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -13,7 +13,7 @@ 'array_indentation' => true, 'array_syntax' => ['syntax' => 'short'], 'cast_spaces' => ['space' => 'none'], - 'class_keyword_remove' => true, + 'class_keyword_remove' => false, 'combine_consecutive_issets' => true, 'combine_consecutive_unsets' => true, 'combine_nested_dirname' => true, @@ -44,7 +44,6 @@ 'no_unused_imports' => true, 'no_useless_return' => true, 'ordered_imports' => ['sort_algorithm' => 'alpha'], - 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], 'phpdoc_align' => ['align' => 'left'], 'phpdoc_indent' => true, 'phpdoc_scalar' => true, From 023f96f2f69505ad14124d0320ad514d6c2fbba3 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sun, 20 Nov 2022 15:48:51 +0100 Subject: [PATCH 08/28] Unit tests for `Query` class --- src/Kql/Help.php | 1 - src/Kql/Query.php | 6 ++++++ tests/Kql/HelpTest.php | 15 +-------------- tests/Kql/QueryTest.php | 28 ++++++++++++++++++++++++++++ tests/Kql/TestCase.php | 14 +++++++++++++- 5 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 tests/Kql/QueryTest.php diff --git a/src/Kql/Help.php b/src/Kql/Help.php index 5a6f466..1e10835 100644 --- a/src/Kql/Help.php +++ b/src/Kql/Help.php @@ -18,7 +18,6 @@ */ class Help { - /** * Provides information about passed value * depending on its type diff --git a/src/Kql/Query.php b/src/Kql/Query.php index 2fdc36b..98f92ba 100644 --- a/src/Kql/Query.php +++ b/src/Kql/Query.php @@ -16,6 +16,12 @@ */ class Query extends BaseQuery { + /** + * Intercepts the chain of segments called + * on each other by replacing objects with + * their corresponding Interceptor which + * handles blocking calls to restricted methods + */ public function intercept(mixed $result): mixed { if (is_object($result) === false) { diff --git a/tests/Kql/HelpTest.php b/tests/Kql/HelpTest.php index b63ecc7..a0678b6 100644 --- a/tests/Kql/HelpTest.php +++ b/tests/Kql/HelpTest.php @@ -4,24 +4,11 @@ use Kirby\Cms\Page; -class TestObject { - public function foo(string $bar = 'hello'): array - { - return [$bar]; - } - - public function more(): string - { - return 'no'; - } -} - /** * @coversDefaultClass \Kirby\Kql\Help */ class HelpTest extends TestCase { - /** * @covers ::for */ @@ -126,7 +113,7 @@ public function testForObjectWithInterceptedObject() */ public function testForObjectWithOriginalObject() { - $app = $this->app->clone([ + $this->app->clone([ 'options' => [ 'kql' => ['classes' => ['allowed' => ['Kirby\Kql\TestObject']]] ] diff --git a/tests/Kql/QueryTest.php b/tests/Kql/QueryTest.php new file mode 100644 index 0000000..0d98d23 --- /dev/null +++ b/tests/Kql/QueryTest.php @@ -0,0 +1,28 @@ +intercept('test'); + $this->assertSame('test', $result); + + // object + $object = new TestObject(); + $this->expectException(PermissionException::class); + $this->expectExceptionMessage('Access to the class "Kirby\Kql\TestObject" is not supported'); + $query->intercept($object); + } +} diff --git a/tests/Kql/TestCase.php b/tests/Kql/TestCase.php index 6236006..6452cc9 100644 --- a/tests/Kql/TestCase.php +++ b/tests/Kql/TestCase.php @@ -5,9 +5,21 @@ use Kirby\Cms\App; use PHPUnit\Framework\TestCase as BaseTestCase; -class TestCase extends BaseTestCase +class TestObject { + public function foo(string $bar = 'hello'): array + { + return [$bar]; + } + public function more(): string + { + return 'no'; + } +} + +class TestCase extends BaseTestCase +{ protected App $app; public function setUp(): void From 16900125fdf9a56c2033e5d082d1224969873d65 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sun, 20 Nov 2022 15:51:32 +0100 Subject: [PATCH 09/28] Exclude Interceptors from code coverage --- phpunit.xml.dist | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 62ddef2..91730d3 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,6 +14,10 @@ ./src + + + ./src/Kql/Interceptors + From 3917bcbbba604fdf8e2665ea7575caf2011c93e8 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sun, 20 Nov 2022 16:16:21 +0100 Subject: [PATCH 10/28] More unit tests and cleaning up --- src/Kql/Help.php | 13 ++++- src/Kql/Interceptor.php | 97 +++++++++++++++++++++----------- src/Kql/Kql.php | 38 ++++++++----- src/Kql/Query.php | 6 +- tests/Kql/HelpTest.php | 2 - tests/Kql/InterceptorTest.php | 101 +++++++++++++++++++++++++++------- tests/Kql/KqlTest.php | 50 ++++++++++------- 7 files changed, 209 insertions(+), 98 deletions(-) diff --git a/src/Kql/Help.php b/src/Kql/Help.php index 1e10835..cafb25a 100644 --- a/src/Kql/Help.php +++ b/src/Kql/Help.php @@ -50,6 +50,8 @@ public static function forArray(array $array): array } /** + * Gathers information for method about + * name, parameters, return type etc. * @internal */ public static function forMethod(object $object, string $method): array @@ -100,6 +102,7 @@ public static function forMethod(object $object, string $method): array } /** + * Gathers informations for each unique method * @internal */ public static function forMethods(object $object, array $methods): array @@ -121,6 +124,8 @@ public static function forMethods(object $object, array $methods): array } /** + * Retrieves info for objects either from Interceptor (to + * only list allowed methods) or via reflection * @internal */ public static function forObject(object $object): array @@ -134,11 +139,13 @@ public static function forObject(object $object): array // for original classes, use reflection $class = new ReflectionClass($object); - $methods = $class->getMethods(); - $methods = A::map($methods, fn ($method) => static::forMethod($object, $method->getName())); + $methods = A::map( + $class->getMethods(), + fn ($method) => static::forMethod($object, $method->getName()) + ); return [ - 'type' => get_class($object), + 'type' => $class->getName(), 'methods' => $methods ]; } diff --git a/src/Kql/Interceptor.php b/src/Kql/Interceptor.php index 7753dc1..0cb90df 100644 --- a/src/Kql/Interceptor.php +++ b/src/Kql/Interceptor.php @@ -3,11 +3,9 @@ namespace Kirby\Kql; use Closure; -use Exception; use Kirby\Cms\App; +use Kirby\Exception\InvalidArgumentException; use Kirby\Exception\PermissionException; -use Kirby\Kql\Help; -use Kirby\Kql\Kql; use Kirby\Toolkit\Str; use ReflectionFunction; use ReflectionMethod; @@ -31,9 +29,14 @@ abstract class Interceptor protected $toArray = []; public function __construct(protected $object) - {} + { + } - public function __call($method, array $args = []) + /** + * Magic caller that prevents access + * to restricted methods + */ + public function __call(string $method, array $args = []) { if ($this->isAllowedMethod($method) === true) { return $this->object->$method(...$args); @@ -42,6 +45,10 @@ public function __call($method, array $args = []) $this->forbiddenMethod($method); } + /** + * Return information about corresponding object + * incl. information about allowed methods + */ public function __debugInfo(): array { $help = Help::forMethods($this->object, $this->allowedMethods()); @@ -53,53 +60,45 @@ public function __debugInfo(): array ]; } + /** + * Returns list of allowed classes. Specific list + * to be implemented in specific interceptor child classes. + */ public function allowedMethods(): array { return []; } - protected static function class(string $class): string + /** + * Returns class name for Interceptor that responds + * to passed name string of a Kirby core class + * @internal + */ + public static function class(string $class): string { return str_replace('Kirby\\', 'Kirby\\Kql\\Interceptors\\', $class); } - protected function forbiddenMethod(string $method) - { - $name = get_class($this->object) . '::' . $method . '()'; - throw new PermissionException('The method "' . $name . '" is not allowed in the API context'); - } - /** - * Returns a registered method by name, either from - * the current class or from a parent class ordered by - * inheritance order (top to bottom) + * Throws exception for accessing a restricted method + * @throws \Kirby\Exception\PermissionException */ - protected function getMethod(string $method): Closure|null + protected function forbiddenMethod(string $method) { - if (isset($this->object::$methods[$method]) === true) { - return $this->object::$methods[$method]; - } - - foreach (class_parents($this->object) as $parent) { - if (isset($parent::$methods[$method]) === true) { - return $parent::$methods[$method]; - } - } - - return null; + $name = get_class($this->object) . '::' . $method . '()'; + throw new PermissionException('The method "' . $name . '" is not allowed in the API context'); } protected function isAllowedCallable($method): bool { try { - $ref = match(true) { + $ref = match (true) { $method instanceof Closure => new ReflectionFunction($method), is_string($method) === true => new ReflectionMethod($this->object, $method), default - => throw new Exception('Invalid method') - + => throw new InvalidArgumentException('Invalid method') }; if ($comment = $ref->getDocComment()) { @@ -107,7 +106,8 @@ protected function isAllowedCallable($method): bool return true; } } - } catch (Throwable) {} + } catch (Throwable) { + } return false; } @@ -161,7 +161,7 @@ protected function isAllowedCustomMethod(string $method): bool } // does not have that method - if (!$call = $this->getMethod($method)) { + if (!$call = $this->method($method)) { return false; } @@ -173,10 +173,36 @@ protected function isAllowedCustomMethod(string $method): bool return false; } + /** + * Returns a registered method by name, either from + * the current class or from a parent class ordered by + * inheritance order (top to bottom) + */ + protected function method(string $method): Closure|null + { + if (isset($this->object::$methods[$method]) === true) { + return $this->object::$methods[$method]; + } + + foreach (class_parents($this->object) as $parent) { + if (isset($parent::$methods[$method]) === true) { + return $parent::$methods[$method]; + } + } + + return null; + } + + /** + * Tries to replace a Kirby core object with the + * corresponding interceptor. + * @throws \Kirby\Exception\InvalidArgumentException for non-objects + * @throws \Kirby\Exception\PermissionException when accessing blocked class + */ public static function replace($object) { if (is_object($object) === false) { - throw new Exception('Unsupported value: ' . gettype($object)); + throw new InvalidArgumentException('Unsupported value: ' . gettype($object)); } $kirby = App::instance(); @@ -249,6 +275,11 @@ public function toArray(): array|null return Kql::select($this, $toArray); } + /** + * Mirrors by default ::toArray but can be + * implemented differently by specifc interceptor. + * KQL will prefer ::toResponse over ::toArray + */ public function toResponse() { return $this->toArray(); diff --git a/src/Kql/Kql.php b/src/Kql/Kql.php index caa7bcd..38a9b3e 100644 --- a/src/Kql/Kql.php +++ b/src/Kql/Kql.php @@ -18,11 +18,6 @@ */ class Kql { - public static function help($object): array - { - return Help::for($object); - } - public static function fetch($model, $key, $selection) { // simple key/value @@ -46,6 +41,15 @@ public static function fetch($model, $key, $selection) return static::run($selection, $model); } + /** + * Returns helpful information about the object + * type as well as, if available, values and methods + */ + public static function help($object): array + { + return Help::for($object); + } + public static function query(string $query, $model = null) { $model ??= App::instance()->site(); @@ -93,11 +97,9 @@ public static function run($input, $model = null) return $result; } - $query = $input['query'] ?? 'site'; + $query = $input['query'] ?? 'site'; $select = $input['select'] ?? null; - $options = [ - 'pagination' => $input['pagination'] ?? null, - ]; + $options = ['pagination' => $input['pagination'] ?? null]; // check for invalid queries if (is_string($query) === false) { @@ -105,7 +107,6 @@ public static function run($input, $model = null) } $result = static::query($query, $model); - return static::select($result, $select, $options); } @@ -124,15 +125,18 @@ public static function select($data, $select, array $options = []) } if (is_object($data) === true) { - return static::selectFromObject($data, $select, $options); + return static::selectFromObject($data, $select); } if (is_array($data) === true) { - return static::selectFromArray($data, $select, $options); + return static::selectFromArray($data, $select); } } - public static function selectFromArray($array, $select, array $options = []) + /** + * @internal + */ + public static function selectFromArray($array, $select) { $result = []; @@ -152,6 +156,9 @@ public static function selectFromArray($array, $select, array $options = []) return $result; } + /** + * @internal + */ public static function selectFromCollection(Collection $collection, $select, array $options = []) { if ($options['pagination'] ?? false) { @@ -180,7 +187,10 @@ public static function selectFromCollection(Collection $collection, $select, arr return $data; } - public static function selectFromObject($object, $select, array $options = []) + /** + * @internal + */ + public static function selectFromObject($object, $select) { // replace actual object with intercepting proxy class $object = Interceptor::replace($object); diff --git a/src/Kql/Query.php b/src/Kql/Query.php index 98f92ba..e4eebcb 100644 --- a/src/Kql/Query.php +++ b/src/Kql/Query.php @@ -24,10 +24,6 @@ class Query extends BaseQuery */ public function intercept(mixed $result): mixed { - if (is_object($result) === false) { - return $result; - } - - return Interceptor::replace($result); + return is_object($result) ? Interceptor::replace($result): $result; } } diff --git a/tests/Kql/HelpTest.php b/tests/Kql/HelpTest.php index a0678b6..e80af89 100644 --- a/tests/Kql/HelpTest.php +++ b/tests/Kql/HelpTest.php @@ -100,8 +100,6 @@ public function testForObjectWithInterceptedObject() $object = new Page(['slug' => 'test']); $result = Help::forObject($object); - - $this->assertSame('page', $result['type']); $this->assertArrayHasKey('methods', $result); $this->assertArrayHasKey('value', $result); diff --git a/tests/Kql/InterceptorTest.php b/tests/Kql/InterceptorTest.php index a9342bd..adec4b2 100644 --- a/tests/Kql/InterceptorTest.php +++ b/tests/Kql/InterceptorTest.php @@ -2,7 +2,6 @@ namespace Kirby\Kql; -use Exception; use Kirby\Cms\App; use Kirby\Cms\Blueprint; use Kirby\Cms\Content; @@ -17,6 +16,7 @@ use Kirby\Cms\SiteBlueprint; use Kirby\Cms\User; use Kirby\Cms\UserBlueprint; +use Kirby\Exception\InvalidArgumentException; use Kirby\Exception\PermissionException; class AppExtended extends App @@ -38,22 +38,81 @@ class UserExtended extends User { } +class TestInterceptor extends Interceptor +{ + public const CLASS_ALIAS = 'test'; + + public function allowedMethods(): array + { + return [ + 'more' + ]; + } +} + /** * @coversDefaultClass \Kirby\Kql\Interceptor */ class InterceptorTest extends TestCase { + /** + * @covers ::__construct + * @covers ::__call + */ + public function testCall() + { + $object = new TestObject(); + $interceptor = new TestInterceptor($object); + $this->assertSame('no', $interceptor->more()); + + $this->expectException(PermissionException::class); + $this->expectExceptionMessage('The method "Kirby\Kql\TestObject::foo()" is not allowed in the API context'); + $interceptor->foo('test'); + } + + /** + * @covers ::__debugInfo + */ + public function testDebugInfo() + { + $object = new TestObject(); + $interceptor = new TestInterceptor($object); + $result = $interceptor->__debugInfo(); + + $this->assertSame('test', $result['type']); + $this->assertArrayHasKey('methods', $result); + $this->assertArrayHasKey('value', $result); + } + + /** + * @covers ::allowedMethods + */ + public function testAllowedMethods() + { + $object = new TestObject(); + $interceptor = new TestInterceptor($object); + $this->assertSame(['more'], $interceptor->allowedMethods()); + } + + /** + * @covers ::class + */ + public function testClass() + { + $this->assertSame('Kirby\Kql\Interceptors\Kql\Test', Interceptor::class('Kirby\Kql\Test')); + } + public function objectProvider() { return [ [ new App(), - 'Kirby\\Kql\\Interceptors\\Cms\\App' + 'Kirby\Kql\Interceptors\Cms\App' ], [ new AppExtended(), - 'Kirby\\Kql\\Interceptors\\Cms\\App' + 'Kirby\Kql\Interceptors\Cms\App' ], [ new Blueprint([ @@ -62,86 +121,86 @@ public function objectProvider() ]), 'name' => 'test', ]), - 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint' + 'Kirby\Kql\Interceptors\Cms\Blueprint' ], [ new Content(), - 'Kirby\\Kql\\Interceptors\\Cms\\Content' + 'Kirby\Kql\Interceptors\Cms\Content' ], [ new Field(null, 'key', 'value'), - 'Kirby\\Kql\\Interceptors\\Cms\\Field' + 'Kirby\Kql\Interceptors\Cms\Field' ], [ $file = new File(['filename' => 'test.jpg', 'parent' => $page]), - 'Kirby\\Kql\\Interceptors\\Cms\\File' + 'Kirby\Kql\Interceptors\Cms\File' ], [ new FileBlueprint([ 'model' => $file, 'name' => 'test', ]), - 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint' + 'Kirby\Kql\Interceptors\Cms\Blueprint' ], [ new FileExtended(['filename' => 'test.jpg', 'parent' => $page]), - 'Kirby\\Kql\\Interceptors\\Cms\\File' + 'Kirby\Kql\Interceptors\Cms\File' ], [ new FileVersion([ 'original' => $file, 'url' => '/test.jpg' ]), - 'Kirby\\Kql\\Interceptors\\Cms\\FileVersion' + 'Kirby\Kql\Interceptors\Cms\FileVersion' ], [ $page, - 'Kirby\\Kql\\Interceptors\\Cms\\Page' + 'Kirby\Kql\Interceptors\Cms\Page' ], [ new PageBlueprint([ 'model' => $page, 'name' => 'test', ]), - 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint' + 'Kirby\Kql\Interceptors\Cms\Blueprint' ], [ new PageExtended(['slug' => 'test']), - 'Kirby\\Kql\\Interceptors\\Cms\\Page' + 'Kirby\Kql\Interceptors\Cms\Page' ], [ new Role(['name' => 'admin']), - 'Kirby\\Kql\\Interceptors\\Cms\\Role' + 'Kirby\Kql\Interceptors\Cms\Role' ], [ new Site(), - 'Kirby\\Kql\\Interceptors\\Cms\\Site' + 'Kirby\Kql\Interceptors\Cms\Site' ], [ new SiteBlueprint([ 'model' => new Site(), 'name' => 'test', ]), - 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint' + 'Kirby\Kql\Interceptors\Cms\Blueprint' ], [ new SiteExtended(), - 'Kirby\\Kql\\Interceptors\\Cms\\Site' + 'Kirby\Kql\Interceptors\Cms\Site' ], [ $user = new User(['email' => 'test@getkirby.com']), - 'Kirby\\Kql\\Interceptors\\Cms\\User' + 'Kirby\Kql\Interceptors\Cms\User' ], [ new UserBlueprint([ 'model' => $user, 'name' => 'test', ]), - 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint' + 'Kirby\Kql\Interceptors\Cms\Blueprint' ], [ new UserExtended(['email' => 'test@getkirby.com']), - 'Kirby\\Kql\\Interceptors\\Cms\\User' + 'Kirby\Kql\Interceptors\Cms\User' ] ]; } @@ -161,7 +220,7 @@ public function testReplace($object, $inspector) */ public function testReplaceNonObject() { - $this->expectException(Exception::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Unsupported value: string'); $result = Interceptor::replace('hello'); diff --git a/tests/Kql/KqlTest.php b/tests/Kql/KqlTest.php index 9709446..33cae16 100644 --- a/tests/Kql/KqlTest.php +++ b/tests/Kql/KqlTest.php @@ -9,15 +9,13 @@ */ class KqlTest extends TestCase { - /** - * @covers ::forbiddenMethod + * @covers ::help */ - public function testForbiddenMethod() + public function testHelp() { - $this->expectException(PermissionException::class); - $this->expectExceptionMessage('The method "Kirby\Cms\Page::delete()" is not allowed in the API context'); - Kql::run('site.children.first.delete'); + $result = Kql::help('foo'); + $this->assertSame(['type' => 'string', 'value' => 'foo'], $result); } /** @@ -31,15 +29,9 @@ public function testQuery() ]); $expected = [ - [ - 'slug' => 'projects', - ], - [ - 'slug' => 'about', - ], - [ - 'slug' => 'contact', - ] + ['slug' => 'projects'], + ['slug' => 'about'], + ['slug' => 'contact'] ]; $this->assertSame($expected, $result); @@ -56,6 +48,28 @@ public function testRun() $this->assertSame($expected, $result); } + /** + * @covers ::render + */ + public function testRender() + { + // non-object: returns value directly + $result = Kql::render('foo'); + $this->assertSame('foo', $result); + + // TODO: intercepted object + } + + /** + * @covers ::run + */ + public function testRunForbiddenMethod() + { + $this->expectException(PermissionException::class); + $this->expectExceptionMessage('The method "Kirby\Cms\Page::delete()" is not allowed in the API context'); + Kql::run('site.children.first.delete'); + } + /** * @covers ::select */ @@ -67,11 +81,7 @@ public function testSelectWithAlias() ] ]); - $expected = [ - 'myTitle' => 'Test Site', - ]; - - $this->assertSame($expected, $result); + $this->assertSame(['myTitle' => 'Test Site'], $result); } /** From 7919fb76d0bb9fb12df99451eb39ff24b32a69b6 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Mon, 21 Nov 2022 09:36:53 +0100 Subject: [PATCH 11/28] First attempt at CI --- .github/workflows/ci.yml | 230 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..51a97f5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,230 @@ +name: CI +on: + push: + paths: + - "**" + - "!.github/**" + - ".github/workflows/ci.yml" + - "!.vscode/**" + - "!**.md" + pull_request: + paths: + - "**" + - "!.github/**" + - ".github/workflows/ci.yml" + - "!.vscode/**" + - "!**.md" + +jobs: + tests: + name: "Unit tests - PHP ${{ matrix.php }}" + + # if on pull request, only run if from a fork + # (our own repo is covered by the push event) + if: > + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name != github.repository + + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + matrix: + php: ["8.0", "8.1"] + env: + extensions: mbstring, ctype, curl, gd, apcu, memcached + ini: apc.enabled=1, apc.enable_cli=1, pcov.directory=., "pcov.exclude=\"~(vendor|tests)~\"" + + steps: + - name: Checkout + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # pin@v3 + with: + fetch-depth: 2 + + - name: Preparations + run: mkdir sarif + + - name: Install system locales + run: sudo apt-get update && sudo apt-get install -y locales-all + + - name: Setup PHP cache environment + id: ext-cache + uses: shivammathur/cache-extensions@fc01a9cdc93341e96c2078d848f2e96240d83c17 # pin@v1 + with: + php-version: ${{ matrix.php }} + extensions: ${{ env.extensions }} + key: php-v1 + + - name: Cache PHP extensions + uses: actions/cache@c3f1317a9e7b1ef106c153ac8c0f00fed3ddbc0d # pin@v3 + with: + path: ${{ steps.ext-cache.outputs.dir }} + key: ${{ steps.ext-cache.outputs.key }} + restore-keys: ${{ steps.ext-cache.outputs.key }} + + - name: Setup PHP environment + uses: shivammathur/setup-php@3eda58347216592f618bb1dff277810b6698e4ca # pin@v2 + with: + php-version: ${{ matrix.php }} + extensions: ${{ env.extensions }} + ini-values: ${{ env.ini }} + coverage: pcov + tools: phpunit:9.5.22, psalm:4.26.0 + + - name: Setup problem matchers + run: | + echo "::add-matcher::${{ runner.tool_cache }}/php.json" + echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Cache analysis data + id: finishPrepare + uses: actions/cache@c3f1317a9e7b1ef106c153ac8c0f00fed3ddbc0d # pin@v3 + with: + path: ~/.cache/psalm + key: backend-analysis-${{ matrix.php }} + + - name: Check Composer platform requirements + if: always() && steps.finishPrepare.outcome == 'success' + run: composer check-platform-reqs + + - name: Run tests + if: always() && steps.finishPrepare.outcome == 'success' + run: phpunit --coverage-clover ${{ github.workspace }}/clover.xml + + - name: Statically analyze using Psalm + if: always() && steps.finishPrepare.outcome == 'success' + run: psalm --output-format=github --php-version=${{ matrix.php }} --report=sarif/psalm.sarif --report-show-info=false + + # - name: Upload coverage results to Codecov + # env: + # token: ${{ secrets.CODECOV_TOKEN }} + # PHP: ${{ matrix.php }} + # if: env.token != '' + # uses: codecov/codecov-action@66b3de25f6f91f65eb92c514d31d6b6f13d5ab18 # pin@v3 + # with: + # token: ${{ secrets.CODECOV_TOKEN }} # for better reliability if the GitHub API is down + # fail_ci_if_error: true + # files: ${{ github.workspace }}/clover.xml + # flags: backend + # env_vars: PHP + + - name: Upload code scanning results to GitHub + if: always() && steps.finishPrepare.outcome == 'success' + uses: github/codeql-action/upload-sarif@7e72857c42532e0c5757d954def4bf7cb35249fa # pin@v2 + with: + sarif_file: sarif + + analysis: + name: "Code Quality" + + # if on pull request, only run if from a fork + # (our own repo is covered by the push event) + if: > + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name != github.repository + + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + php: "8.0" + extensions: mbstring, ctype, curl, gd, apcu, memcached + + steps: + - name: Checkout + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # pin@v3 + + - name: Preparations + run: mkdir sarif + + - name: Setup PHP cache environment + id: ext-cache + uses: shivammathur/cache-extensions@fc01a9cdc93341e96c2078d848f2e96240d83c17 # pin@v1 + with: + php-version: ${{ env.php }} + extensions: ${{ env.extensions }} + key: php-analysis-v1 + + - name: Cache PHP extensions + uses: actions/cache@c3f1317a9e7b1ef106c153ac8c0f00fed3ddbc0d # pin@v3 + with: + path: ${{ steps.ext-cache.outputs.dir }} + key: ${{ steps.ext-cache.outputs.key }} + restore-keys: ${{ steps.ext-cache.outputs.key }} + + - name: Setup PHP environment + id: finishPrepare + uses: shivammathur/setup-php@3eda58347216592f618bb1dff277810b6698e4ca # pin@v2 + with: + php-version: ${{ env.php }} + extensions: ${{ env.extensions }} + coverage: none + tools: | + composer:2.3.8, composer-normalize:2.28.3, composer-require-checker:4.1.0, + composer-unused:0.7.12, phpcpd:6.0.3, phpmd:2.12.0 + + - name: Validate composer.json/composer.lock + if: always() && steps.finishPrepare.outcome == 'success' + run: composer validate --strict --no-check-version --no-check-all + + - name: Ensure that composer.json is normalized + if: always() && steps.finishPrepare.outcome == 'success' + run: composer-normalize --dry-run + + - name: Check for unused Composer dependencies + if: always() && steps.finishPrepare.outcome == 'success' + run: composer-unused --no-progress + + - name: Check for duplicated code + if: always() && steps.finishPrepare.outcome == 'success' + run: phpcpd --fuzzy --exclude tests --exclude vendor . + + - name: Statically analyze using PHPMD + if: always() && steps.finishPrepare.outcome == 'success' + run: phpmd . github phpmd.xml.dist --exclude 'dependencies/*,tests/*,vendor/*' --reportfile-sarif sarif/phpmd.sarif + + - name: Upload code scanning results to GitHub + if: always() && steps.finishPrepare.outcome == 'success' + uses: github/codeql-action/upload-sarif@7e72857c42532e0c5757d954def4bf7cb35249fa # pin@v2 + with: + sarif_file: sarif + + coding-style: + name: "Coding Style" + + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + php: "8.0" + + steps: + - name: Checkout + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # pin@v3 + + - name: Setup PHP environment + uses: shivammathur/setup-php@3eda58347216592f618bb1dff277810b6698e4ca # pin@v2 + with: + php-version: ${{ env.php }} + coverage: none + tools: php-cs-fixer:3.10.0 + + - name: Cache analysis data + id: finishPrepare + uses: actions/cache@c3f1317a9e7b1ef106c153ac8c0f00fed3ddbc0d # pin@v3 + with: + path: ~/.php-cs-fixer + key: coding-style + + - name: Check for PHP coding style violations + if: always() && steps.finishPrepare.outcome == 'success' + # Use the --dry-run flag in push builds to get a failed CI status + run: > + php-cs-fixer fix --diff + ${{ github.event_name != 'pull_request' && '--dry-run' || '' }} + + - name: Create code suggestions from the coding style changes (on PR only) + if: > + always() && steps.finishPrepare.outcome == 'success' && + github.event_name == 'pull_request' + uses: reviewdog/action-suggester@ab82daa6ea9b84fe43db7747bb10fa087f34e1ab # pin@v1 + with: + tool_name: PHP-CS-Fixer + fail_on_error: "true" From c7456e6b6b5717f92061876c24056224ef26fc5b Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Mon, 21 Nov 2022 10:04:51 +0100 Subject: [PATCH 12/28] Add phpmd and psalm config --- composer.json | 8 ++++++- phpmd.xml.dist | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++ psalm.xml.dist | 16 ++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 phpmd.xml.dist create mode 100644 psalm.xml.dist diff --git a/composer.json b/composer.json index 66d3690..87c955d 100755 --- a/composer.json +++ b/composer.json @@ -48,9 +48,15 @@ }, "scripts": { "analyze": [ - "@analyze:composer" + "@analyze:composer", + "@analyze:psalm", + "@analyze:phpcpd", + "@analyze:phpmd" ], "analyze:composer": "composer validate --strict --no-check-version --no-check-all", + "analyze:phpcpd": "phpcpd --fuzzy --exclude tests --exclude vendor .", + "analyze:phpmd": "phpmd . ansi phpmd.xml.dist --exclude 'dependencies/*,tests/*,vendor/*'", + "analyze:psalm": "psalm", "ci": [ "@fix", "@analyze", diff --git a/phpmd.xml.dist b/phpmd.xml.dist new file mode 100644 index 0000000..6f0f581 --- /dev/null +++ b/phpmd.xml.dist @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/psalm.xml.dist b/psalm.xml.dist new file mode 100644 index 0000000..d0b5fe8 --- /dev/null +++ b/psalm.xml.dist @@ -0,0 +1,16 @@ + + + + + + + + + + From 508ce8bf87715ca49d7e876e0c0f5b5889b94692 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Mon, 21 Nov 2022 10:10:54 +0100 Subject: [PATCH 13/28] Normalize composer.json file --- .github/workflows/ci.yml | 2 +- composer.json | 16 ++++++------ extensions/helpers.php | 9 ++++--- src/Kql/Interceptor.php | 53 +++++++++++++++++++++++----------------- 4 files changed, 45 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51a97f5..4a64e85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -179,7 +179,7 @@ jobs: - name: Statically analyze using PHPMD if: always() && steps.finishPrepare.outcome == 'success' - run: phpmd . github phpmd.xml.dist --exclude 'dependencies/*,tests/*,vendor/*' --reportfile-sarif sarif/phpmd.sarif + run: phpmd . github phpmd.xml.dist --exclude 'tests/*,vendor/*' --reportfile-sarif sarif/phpmd.sarif - name: Upload code scanning results to GitHub if: always() && steps.finishPrepare.outcome == 'success' diff --git a/composer.json b/composer.json index 87c955d..fcc4a1d 100755 --- a/composer.json +++ b/composer.json @@ -2,6 +2,7 @@ "name": "getkirby/kql", "description": "Kirby Query Language", "license": "MIT", + "type": "kirby-plugin", "version": "1.2.0", "keywords": [ "kirby", @@ -11,8 +12,6 @@ "query", "headless" ], - "homepage": "https://getkirby.com", - "type": "kirby-plugin", "authors": [ { "name": "Bastian Allgeier", @@ -23,17 +22,12 @@ "email": "nico@getkirby.com" } ], + "homepage": "https://getkirby.com", "require": { "php": ">=8.0.0 <8.2.0", "getkirby/cms": ">=3.8.2", "getkirby/composer-installer": "^1.2.1" }, - "config": { - "optimize-autoloader": true, - "allow-plugins": { - "getkirby/composer-installer": true - } - }, "autoload": { "psr-4": { "Kirby\\": [ @@ -42,6 +36,12 @@ ] } }, + "config": { + "allow-plugins": { + "getkirby/composer-installer": true + }, + "optimize-autoloader": true + }, "extra": { "installer-name": "versions", "kirby-cms-path": false diff --git a/extensions/helpers.php b/extensions/helpers.php index 2bbe75f..99a6178 100644 --- a/extensions/helpers.php +++ b/extensions/helpers.php @@ -1,8 +1,11 @@ new ReflectionFunction($method), - is_string($method) === true - => new ReflectionMethod($this->object, $method), - default - => throw new InvalidArgumentException('Invalid method') - }; - - if ($comment = $ref->getDocComment()) { - if (Str::contains($comment, '@kql-allowed') === true) { - return true; - } - } - } catch (Throwable) { - } - - return false; - } - + /** + * Checks if method is allowed to call + */ protected function isAllowedMethod($method) { $kirby = App::instance(); @@ -153,6 +133,33 @@ protected function isAllowedMethod($method) return false; } + /** + * Checks if closure or object method is allowed + */ + protected function isAllowedCallable($method): bool + { + try { + $ref = match (true) { + $method instanceof Closure + => new ReflectionFunction($method), + is_string($method) === true + => new ReflectionMethod($this->object, $method), + default + => throw new InvalidArgumentException('Invalid method') + }; + + if ($comment = $ref->getDocComment()) { + if (Str::contains($comment, '@kql-allowed') === true) { + return true; + } + } + } catch (Throwable) { + return false; + } + + return false; + } + protected function isAllowedCustomMethod(string $method): bool { // has no custom methods From bb7271b597a978221212a95e7a0f22e5aedfa1ef Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Mon, 21 Nov 2022 10:26:35 +0100 Subject: [PATCH 14/28] Try to fix CI --- .github/workflows/ci.yml | 135 ++++++++++++++++----------------------- 1 file changed, 55 insertions(+), 80 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a64e85..e38685d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,29 +1,9 @@ name: CI -on: - push: - paths: - - "**" - - "!.github/**" - - ".github/workflows/ci.yml" - - "!.vscode/**" - - "!**.md" - pull_request: - paths: - - "**" - - "!.github/**" - - ".github/workflows/ci.yml" - - "!.vscode/**" - - "!**.md" +on: [push, pull_request] jobs: tests: - name: "Unit tests - PHP ${{ matrix.php }}" - - # if on pull request, only run if from a fork - # (our own repo is covered by the push event) - if: > - github.event_name != 'pull_request' || - github.event.pull_request.head.repo.full_name != github.repository + name: PHP ${{ matrix.php }} runs-on: ubuntu-latest timeout-minutes: 5 @@ -31,20 +11,12 @@ jobs: matrix: php: ["8.0", "8.1"] env: - extensions: mbstring, ctype, curl, gd, apcu, memcached - ini: apc.enabled=1, apc.enable_cli=1, pcov.directory=., "pcov.exclude=\"~(vendor|tests)~\"" + extensions: mbstring, pcov + ini: pcov.directory=., "pcov.exclude=\"~(vendor|tests)~\"" steps: - name: Checkout uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # pin@v3 - with: - fetch-depth: 2 - - - name: Preparations - run: mkdir sarif - - - name: Install system locales - run: sudo apt-get update && sudo apt-get install -y locales-all - name: Setup PHP cache environment id: ext-cache @@ -68,23 +40,33 @@ jobs: extensions: ${{ env.extensions }} ini-values: ${{ env.ini }} coverage: pcov - tools: phpunit:9.5.22, psalm:4.26.0 + tools: phpunit:9.5.13, psalm:4.11.2 - name: Setup problem matchers run: | echo "::add-matcher::${{ runner.tool_cache }}/php.json" echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Get Composer cache directory + id: composerCache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@c3f1317a9e7b1ef106c153ac8c0f00fed3ddbc0d # pin@v3 + with: + path: ${{ steps.composerCache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist + - name: Cache analysis data id: finishPrepare uses: actions/cache@c3f1317a9e7b1ef106c153ac8c0f00fed3ddbc0d # pin@v3 with: path: ~/.cache/psalm - key: backend-analysis-${{ matrix.php }} - - - name: Check Composer platform requirements - if: always() && steps.finishPrepare.outcome == 'success' - run: composer check-platform-reqs + key: backend-analysis-${{ matrix.php }}-v2 - name: Run tests if: always() && steps.finishPrepare.outcome == 'success' @@ -92,56 +74,37 @@ jobs: - name: Statically analyze using Psalm if: always() && steps.finishPrepare.outcome == 'success' - run: psalm --output-format=github --php-version=${{ matrix.php }} --report=sarif/psalm.sarif --report-show-info=false + run: psalm --output-format=github --php-version=${{ matrix.php }} # - name: Upload coverage results to Codecov - # env: - # token: ${{ secrets.CODECOV_TOKEN }} - # PHP: ${{ matrix.php }} - # if: env.token != '' # uses: codecov/codecov-action@66b3de25f6f91f65eb92c514d31d6b6f13d5ab18 # pin@v3 # with: - # token: ${{ secrets.CODECOV_TOKEN }} # for better reliability if the GitHub API is down - # fail_ci_if_error: true - # files: ${{ github.workspace }}/clover.xml + # file: ${{ github.workspace }}/clover.xml # flags: backend # env_vars: PHP - - - name: Upload code scanning results to GitHub - if: always() && steps.finishPrepare.outcome == 'success' - uses: github/codeql-action/upload-sarif@7e72857c42532e0c5757d954def4bf7cb35249fa # pin@v2 - with: - sarif_file: sarif + # env: + # PHP: ${{ matrix.php }} analysis: - name: "Code Quality" - - # if on pull request, only run if from a fork - # (our own repo is covered by the push event) - if: > - github.event_name != 'pull_request' || - github.event.pull_request.head.repo.full_name != github.repository + name: Analysis runs-on: ubuntu-latest timeout-minutes: 5 env: php: "8.0" - extensions: mbstring, ctype, curl, gd, apcu, memcached + extensions: mbstring steps: - name: Checkout uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # pin@v3 - - name: Preparations - run: mkdir sarif - - name: Setup PHP cache environment id: ext-cache uses: shivammathur/cache-extensions@fc01a9cdc93341e96c2078d848f2e96240d83c17 # pin@v1 with: php-version: ${{ env.php }} extensions: ${{ env.extensions }} - key: php-analysis-v1 + key: php-v1 - name: Cache PHP extensions uses: actions/cache@c3f1317a9e7b1ef106c153ac8c0f00fed3ddbc0d # pin@v3 @@ -158,7 +121,7 @@ jobs: extensions: ${{ env.extensions }} coverage: none tools: | - composer:2.3.8, composer-normalize:2.28.3, composer-require-checker:4.1.0, + composer:2.3.7, composer-normalize:2.28.0, composer-unused:0.7.12, phpcpd:6.0.3, phpmd:2.12.0 - name: Validate composer.json/composer.lock @@ -169,31 +132,42 @@ jobs: if: always() && steps.finishPrepare.outcome == 'success' run: composer-normalize --dry-run - - name: Check for unused Composer dependencies + - name: Get Composer cache directory + id: composerCache1 if: always() && steps.finishPrepare.outcome == 'success' + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + id: composerCache2 + if: always() && steps.composerCache1.outcome == 'success' + uses: actions/cache@c3f1317a9e7b1ef106c153ac8c0f00fed3ddbc0d # pin@v3 + with: + path: ${{ steps.composerCache1.outputs.dir }} + key: ${{ runner.os }}-composer-locked-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer-locked- + + - name: Install dependencies + id: composerInstall + if: always() && steps.composerCache2.outcome == 'success' + run: composer install --prefer-dist + + - name: Check for unused Composer dependencies + if: always() && steps.composerInstall.outcome == 'success' run: composer-unused --no-progress - name: Check for duplicated code - if: always() && steps.finishPrepare.outcome == 'success' + if: always() && steps.composerInstall.outcome == 'success' run: phpcpd --fuzzy --exclude tests --exclude vendor . - name: Statically analyze using PHPMD - if: always() && steps.finishPrepare.outcome == 'success' - run: phpmd . github phpmd.xml.dist --exclude 'tests/*,vendor/*' --reportfile-sarif sarif/phpmd.sarif - - - name: Upload code scanning results to GitHub - if: always() && steps.finishPrepare.outcome == 'success' - uses: github/codeql-action/upload-sarif@7e72857c42532e0c5757d954def4bf7cb35249fa # pin@v2 - with: - sarif_file: sarif + if: always() && steps.composerInstall.outcome == 'success' + run: phpmd . github phpmd.xml.dist --exclude 'tests/*,vendor/*' coding-style: - name: "Coding Style" + name: Coding Style runs-on: ubuntu-latest timeout-minutes: 5 - env: - php: "8.0" steps: - name: Checkout @@ -202,9 +176,8 @@ jobs: - name: Setup PHP environment uses: shivammathur/setup-php@3eda58347216592f618bb1dff277810b6698e4ca # pin@v2 with: - php-version: ${{ env.php }} coverage: none - tools: php-cs-fixer:3.10.0 + tools: php-cs-fixer:3.8.0 - name: Cache analysis data id: finishPrepare @@ -215,6 +188,8 @@ jobs: - name: Check for PHP coding style violations if: always() && steps.finishPrepare.outcome == 'success' + env: + PHP_CS_FIXER_IGNORE_ENV: 1 # Use the --dry-run flag in push builds to get a failed CI status run: > php-cs-fixer fix --diff From 044d3194df40af049051786da61111646c10d3fa Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Mon, 21 Nov 2022 19:24:00 +0100 Subject: [PATCH 15/28] Unit tests for `Interceptor` class --- phpunit.xml.dist | 2 +- src/Kql/Help.php | 2 +- src/Kql/Interceptor.php | 5 +- tests/Kql/HelpTest.php | 18 ++-- tests/Kql/InterceptorTest.php | 158 ++++++++++++++++++++++++++++++++-- tests/Kql/TestCase.php | 16 ++++ 6 files changed, 183 insertions(+), 18 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 91730d3..a64a205 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -28,5 +28,5 @@ - + diff --git a/src/Kql/Help.php b/src/Kql/Help.php index cafb25a..faa6f21 100644 --- a/src/Kql/Help.php +++ b/src/Kql/Help.php @@ -78,7 +78,7 @@ public static function forMethod(object $object, string $method): array $call .= '$' . $name; - if ($required === false && $default === true) { + if ($required === false && $default !== null) { $call .= ' = ' . var_export($default, true); } diff --git a/src/Kql/Interceptor.php b/src/Kql/Interceptor.php index 1736610..7f6e774 100644 --- a/src/Kql/Interceptor.php +++ b/src/Kql/Interceptor.php @@ -63,6 +63,7 @@ public function __debugInfo(): array /** * Returns list of allowed classes. Specific list * to be implemented in specific interceptor child classes. + * @codeCoverageIgnore */ public function allowedMethods(): array { @@ -92,7 +93,7 @@ protected function forbiddenMethod(string $method) /** * Checks if method is allowed to call */ - protected function isAllowedMethod($method) + public function isAllowedMethod($method) { $kirby = App::instance(); $name = strtolower(get_class($this->object) . '::' . $method); @@ -185,7 +186,7 @@ protected function isAllowedCustomMethod(string $method): bool * the current class or from a parent class ordered by * inheritance order (top to bottom) */ - protected function method(string $method): Closure|null + protected function method(string $method) { if (isset($this->object::$methods[$method]) === true) { return $this->object::$methods[$method]; diff --git a/tests/Kql/HelpTest.php b/tests/Kql/HelpTest.php index e80af89..97c4d64 100644 --- a/tests/Kql/HelpTest.php +++ b/tests/Kql/HelpTest.php @@ -45,7 +45,7 @@ public function testForMethod() $object = new TestObject(); $result = Help::forMethod($object, 'foo'); $this->assertSame([ - 'call' => '.foo(string $bar)', + 'call' => '.foo(string $bar = \'hello\')', 'name' => 'foo', 'params' => [ 'bar' => [ @@ -53,7 +53,7 @@ public function testForMethod() 'type' => 'string', 'required' => false, 'default' => 'hello', - 'call' => 'string $bar' + 'call' => 'string $bar = \'hello\'' ] ], 'returns' => 'array' @@ -66,10 +66,10 @@ public function testForMethod() public function testForMethods() { $object = new TestObject(); - $result = Help::forMethods($object, ['more', 'foo', 'more']); + $result = Help::forMethods($object, ['more', 'foo', 'more', '404']); $this->assertSame([ 'foo' => [ - 'call' => '.foo(string $bar)', + 'call' => '.foo(string $bar = \'hello\')', 'name' => 'foo', 'params' => [ 'bar' => [ @@ -77,7 +77,7 @@ public function testForMethods() 'type' => 'string', 'required' => false, 'default' => 'hello', - 'call' => 'string $bar' + 'call' => 'string $bar = \'hello\'' ] ], 'returns' => 'array' @@ -98,7 +98,7 @@ public function testForMethods() public function testForObjectWithInterceptedObject() { $object = new Page(['slug' => 'test']); - $result = Help::forObject($object); + $result = Help::for($object); $this->assertSame('page', $result['type']); $this->assertArrayHasKey('methods', $result); @@ -118,10 +118,10 @@ public function testForObjectWithOriginalObject() ]); $object = new TestObject(); - $result = Help::forObject($object); + $result = Help::for($object); $this->assertSame('Kirby\Kql\TestObject', $result['type']); - $this->assertCount(2, $result['methods']); - $this->assertSame('.foo(string $bar)', $result['methods'][0]['call']); + $this->assertCount(3, $result['methods']); + $this->assertSame('.foo(string $bar = \'hello\')', $result['methods'][0]['call']); } } diff --git a/tests/Kql/InterceptorTest.php b/tests/Kql/InterceptorTest.php index adec4b2..6593f6e 100644 --- a/tests/Kql/InterceptorTest.php +++ b/tests/Kql/InterceptorTest.php @@ -42,6 +42,8 @@ class TestInterceptor extends Interceptor { public const CLASS_ALIAS = 'test'; + protected $toArray = ['more', 'foo']; + public function allowedMethods(): array { return [ @@ -59,6 +61,7 @@ class InterceptorTest extends TestCase /** * @covers ::__construct * @covers ::__call + * @covers ::forbiddenMethod */ public function testCall() { @@ -103,6 +106,99 @@ public function testClass() $this->assertSame('Kirby\Kql\Interceptors\Kql\Test', Interceptor::class('Kirby\Kql\Test')); } + /** + * @covers ::isAllowedMethod + */ + public function testIsAllowedMethod() + { + $object = new TestObject(); + $interceptor = new TestInterceptor($object); + $this->assertTrue($interceptor->isAllowedMethod('more')); + $this->assertFalse($interceptor->isAllowedMethod('foo')); + } + + /** + * @covers ::isAllowedMethod + */ + public function testIsAllowedMethodWithBlockedConfig() + { + $this->app->clone([ + 'options' => [ + 'kql' => ['methods' => ['blocked' => ['Kirby\Kql\TestObject::more']]] + ] + ]); + + $object = new TestObject(); + $interceptor = new TestInterceptor($object); + $this->assertFalse($interceptor->isAllowedMethod('more')); + } + + /** + * @covers ::isAllowedMethod + */ + public function testIsAllowedMethodWithAllowedConfig() + { + $this->app->clone([ + 'options' => [ + 'kql' => ['methods' => ['allowed' => ['Kirby\Kql\TestObject::foo']]] + ] + ]); + + $object = new TestObject(); + $interceptor = new TestInterceptor($object); + $this->assertTrue($interceptor->isAllowedMethod('foo')); + } + + /** + * @covers ::isAllowedMethod + * @covers ::isAllowedCallable + */ + public function testIsAllowedCallable() + { + $object = new TestObject(); + $interceptor = new TestInterceptor($object); + $this->assertTrue($interceptor->isAllowedMethod('homer')); + } + + /** + * @covers ::isAllowedMethod + * @covers ::isAllowedCustomMethod + * @covers ::isAllowedCallable + * @covers ::method + */ + public function testIsAllowedCustomMethod() + { + $object = new TestObject(); + $interceptor = new TestInterceptor($object); + $this->assertFalse($interceptor->isAllowedMethod('simple')); + + $object = new TestObjectWithMethods(); + $interceptor = new TestInterceptor($object); + $this->assertFalse($interceptor->isAllowedMethod('simple')); + + TestObjectWithMethods::$methods = ['simple' => fn () => false]; + $this->assertFalse($interceptor->isAllowedMethod('simple')); + + $object = new TestObjectWithMethodsAsChild(); + $interceptor = new TestInterceptor($object); + $this->assertFalse($interceptor->isAllowedMethod('simple')); + + TestObjectWithMethods::$methods = ['closure' => function () { return; }]; + $this->assertFalse($interceptor->isAllowedMethod('closure')); + + TestObjectWithMethods::$methods = [ + /** + * @kql-allowed + */ + 'closure' => function () { return; } + ]; + $this->assertTrue($interceptor->isAllowedMethod('closure')); + + TestObjectWithMethods::$methods = ['invalid' => 5]; + $this->assertFalse($interceptor->isAllowedMethod('invalid')); + } + + public function objectProvider() { return [ @@ -209,10 +305,10 @@ public function objectProvider() * @covers ::replace * @dataProvider objectProvider */ - public function testReplace($object, $inspector) + public function testReplace($object, $interceptor) { $result = Interceptor::replace($object); - $this->assertInstanceOf($inspector, $result); + $this->assertInstanceOf($interceptor, $result); } /** @@ -222,8 +318,48 @@ public function testReplaceNonObject() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Unsupported value: string'); + Interceptor::replace('hello'); + } + + /** + * @covers ::replace + */ + public function testReplaceBlockedOptions() + { + $this->app->clone([ + 'options' => [ + 'kql' => ['classes' => ['blocked' => ['Kirby\Kql\TestOBJect']]] + ] + ]); + + $this->expectException(PermissionException::class); + $this->expectExceptionMessage('Access to the class "Kirby\Kql\TestObject" is blocked'); + Interceptor::replace(new TestObject()); + } + + /** + * @covers ::replace + */ + public function testReplaceObjectIsInterceptor() + { + $object = new TestObject(); + $interceptor = new TestInterceptor($object); + $this->assertSame($interceptor, Interceptor::replace($interceptor)); + } + + /** + * @covers ::replace + */ + public function testReplaceAllowedOptions() + { + $this->app->clone([ + 'options' => [ + 'kql' => ['classes' => ['allowed' => ['stdClass']]] + ] + ]); - $result = Interceptor::replace('hello'); + $object = new \stdClass(); + $this->assertSame($object, Interceptor::replace($object)); } /** @@ -233,8 +369,20 @@ public function testReplaceUnknownObject() { $this->expectException(PermissionException::class); $this->expectExceptionMessage('Access to the class "stdClass" is not supported'); - $object = new \stdClass(); - $result = Interceptor::replace($object); + Interceptor::replace($object); + } + + /** + * @covers ::toArray + * @covers ::toResponse + */ + public function testToArray() + { + $object = new TestObject(); + $interceptor = new TestInterceptor($object); + $expected = ['more' => 'no']; + $this->assertSame($expected, $interceptor->toArray()); + $this->assertSame($expected, $interceptor->toResponse()); } } diff --git a/tests/Kql/TestCase.php b/tests/Kql/TestCase.php index 6452cc9..8be9b6d 100644 --- a/tests/Kql/TestCase.php +++ b/tests/Kql/TestCase.php @@ -16,6 +16,22 @@ public function more(): string { return 'no'; } + + /** + * @kql-allowed + */ + public function homer() + {} +} + +class TestObjectWithMethods extends TestObject +{ + public static array $methods = []; +} + +class TestObjectWithMethodsAsChild extends TestObjectWithMethods +{ + public static array $methods = []; } class TestCase extends BaseTestCase From 8fe3f9a0ae6b479f7ceff706954a71933dddd2ae Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Mon, 21 Nov 2022 19:50:41 +0100 Subject: [PATCH 16/28] Unit tests for `Kql` class --- src/Kql/Kql.php | 6 +- tests/Kql/KqlTest.php | 143 +++++++++++++++++++++++++++++++++++++---- tests/Kql/TestCase.php | 5 ++ 3 files changed, 137 insertions(+), 17 deletions(-) diff --git a/src/Kql/Kql.php b/src/Kql/Kql.php index 38a9b3e..c48e732 100644 --- a/src/Kql/Kql.php +++ b/src/Kql/Kql.php @@ -136,7 +136,7 @@ public static function select($data, $select, array $options = []) /** * @internal */ - public static function selectFromArray($array, $select) + public static function selectFromArray(array $array, array $select): array { $result = []; @@ -159,7 +159,7 @@ public static function selectFromArray($array, $select) /** * @internal */ - public static function selectFromCollection(Collection $collection, $select, array $options = []) + public static function selectFromCollection(Collection $collection, array|string $select, array $options = []) { if ($options['pagination'] ?? false) { $collection = $collection->paginate($options['pagination']); @@ -190,7 +190,7 @@ public static function selectFromCollection(Collection $collection, $select, arr /** * @internal */ - public static function selectFromObject($object, $select) + public static function selectFromObject(object $object, array|string $select) { // replace actual object with intercepting proxy class $object = Interceptor::replace($object); diff --git a/tests/Kql/KqlTest.php b/tests/Kql/KqlTest.php index 33cae16..2c2715a 100644 --- a/tests/Kql/KqlTest.php +++ b/tests/Kql/KqlTest.php @@ -2,6 +2,8 @@ namespace Kirby\Kql; +use Exception; +use Kirby\Cms\Page; use Kirby\Exception\PermissionException; /** @@ -9,6 +11,24 @@ */ class KqlTest extends TestCase { + /** + * @covers ::fetch + */ + public function testFetch() + { + $object = new TestObject(); + $result = Kql::fetch($object, 'more', true); + $this->assertSame('no', $result); + + $object = new Page(['slug' => 'test']); + $result = Kql::fetch($object, 'slug', []); + $this->assertSame('test', $result); + + $object = new Page(['slug' => 'test']); + $result = Kql::fetch($object, null, ['query' => 'page.slug']); + $this->assertSame('test', $result); + } + /** * @covers ::help */ @@ -37,6 +57,45 @@ public function testQuery() $this->assertSame($expected, $result); } + /** + * @covers ::render + */ + public function testRender() + { + // non-object: returns value directly + $result = Kql::render('foo'); + $this->assertSame('foo', $result); + + // intercepted object + $object = new Page(['slug' => 'test']); + $result = Kql::render($object); + $this->assertIsArray($result); + } + + /** + * @covers ::render + */ + public function testRenderOriginalObject() + { + $this->app->clone([ + 'options' => [ + 'kql' => ['classes' => ['allowed' => [ + 'Kirby\Kql\TestObject', + 'Kirby\Kql\TestObjectWithMethods' + ]]] + ] + ]); + + $object = new TestObjectWithMethods(); + $result = Kql::render($object); + $this->assertIsArray($result); + + $object = new TestObject(); + $this->expectException(Exception::class); + $this->expectExceptionMessage('The object "Kirby\Kql\TestObject" cannot be rendered. Try querying one of its methods instead.'); + Kql::render($object); + } + /** * @covers ::run */ @@ -44,20 +103,23 @@ public function testRun() { $result = Kql::run('site.title'); $expected = 'Test Site'; - $this->assertSame($expected, $result); + + $result = Kql::run(['queries' => ['site.title']]); + $this->assertSame([$expected], $result); + + $result = Kql::run(['query' => 'site', 'select' => 'title']); + $this->assertSame(['title' => $expected], $result); } /** - * @covers ::render + * @covers ::run */ - public function testRender() + public function testRunInvalidQuery() { - // non-object: returns value directly - $result = Kql::render('foo'); - $this->assertSame('foo', $result); - - // TODO: intercepted object + $this->expectException(Exception::class); + $this->expectExceptionMessage('The query must be a string'); + Kql::run(['query' => false]); } /** @@ -70,6 +132,20 @@ public function testRunForbiddenMethod() Kql::run('site.children.first.delete'); } + /** + * @covers ::select + */ + public function testSelect() + { + // no select, returns data via ::render + $result = Kql::select('foo', null); + $this->assertSame('foo', $result); + + // help + $result = Kql::select('foo', '?'); + $this->assertSame(['type' => 'string', 'value' => 'foo'], $result); + } + /** * @covers ::select */ @@ -88,18 +164,57 @@ public function testSelectWithAlias() * @covers ::select * @covers ::selectFromArray */ - public function testSelectWithArray() + public function testSelectFromArray() { - $result = Kql::run([ - 'select' => ['title', 'url'] - ]); - $expected = [ + $data = [ 'title' => 'Test Site', 'url' => '/' ]; - $this->assertSame($expected, $result); + $result = Kql::select($data, ['title' => true, 'url' => false]); + $this->assertSame(['title' => 'Test Site'], $result); + + $result = Kql::select($data, ['title']); + $this->assertSame(['title' => 'Test Site'], $result); + } + + /** + * @covers ::select + * @covers ::selectFromCollection + */ + public function testSelectFromCollection() + { + $result = Kql::run([ + 'select' => [ + 'children' => [ + 'query' => 'site.children', + 'select' => 'slug', + 'pagination' => ['limit' => 2] + ] + ] + ]); + + $this->assertCount(2, $result['children']['data']); + $this->assertSame(2, $result['children']['pagination']['limit']); + } + + /** + * @covers ::select + * @covers ::selectFromObject + */ + public function testSelectFromObject() + { + $result = Kql::run([ + 'select' => [ + 'test' => [ + 'query' => 'site.page("about")', + 'select' => ['url' => true, 'slug' => false], + ] + ] + ]); + + $this->assertSame('/about', $result['test']['url']); } /** diff --git a/tests/Kql/TestCase.php b/tests/Kql/TestCase.php index 8be9b6d..12b3ac2 100644 --- a/tests/Kql/TestCase.php +++ b/tests/Kql/TestCase.php @@ -27,6 +27,11 @@ public function homer() class TestObjectWithMethods extends TestObject { public static array $methods = []; + + public function toArray() + { + return []; + } } class TestObjectWithMethodsAsChild extends TestObjectWithMethods From e735ce8fb37e760fadb8e488e1c9966bb9e0a348 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Mon, 21 Nov 2022 19:51:33 +0100 Subject: [PATCH 17/28] cs fixes --- tests/Kql/InterceptorTest.php | 8 ++++++-- tests/Kql/KqlTest.php | 1 - tests/Kql/TestCase.php | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/Kql/InterceptorTest.php b/tests/Kql/InterceptorTest.php index 6593f6e..d958cec 100644 --- a/tests/Kql/InterceptorTest.php +++ b/tests/Kql/InterceptorTest.php @@ -183,14 +183,18 @@ public function testIsAllowedCustomMethod() $interceptor = new TestInterceptor($object); $this->assertFalse($interceptor->isAllowedMethod('simple')); - TestObjectWithMethods::$methods = ['closure' => function () { return; }]; + TestObjectWithMethods::$methods = ['closure' => function () { + + }]; $this->assertFalse($interceptor->isAllowedMethod('closure')); TestObjectWithMethods::$methods = [ /** * @kql-allowed */ - 'closure' => function () { return; } + 'closure' => function () { + + } ]; $this->assertTrue($interceptor->isAllowedMethod('closure')); diff --git a/tests/Kql/KqlTest.php b/tests/Kql/KqlTest.php index 2c2715a..70bbfe9 100644 --- a/tests/Kql/KqlTest.php +++ b/tests/Kql/KqlTest.php @@ -166,7 +166,6 @@ public function testSelectWithAlias() */ public function testSelectFromArray() { - $data = [ 'title' => 'Test Site', 'url' => '/' diff --git a/tests/Kql/TestCase.php b/tests/Kql/TestCase.php index 12b3ac2..eff510f 100644 --- a/tests/Kql/TestCase.php +++ b/tests/Kql/TestCase.php @@ -21,7 +21,8 @@ public function more(): string * @kql-allowed */ public function homer() - {} + { + } } class TestObjectWithMethods extends TestObject From c6a4b88c6b3ac365c9bc8596a476c713790d18e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Hoffmann=20=20=E0=B7=B4?= Date: Mon, 21 Nov 2022 19:53:18 +0100 Subject: [PATCH 18/28] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- tests/Kql/InterceptorTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Kql/InterceptorTest.php b/tests/Kql/InterceptorTest.php index d958cec..9f122eb 100644 --- a/tests/Kql/InterceptorTest.php +++ b/tests/Kql/InterceptorTest.php @@ -184,7 +184,6 @@ public function testIsAllowedCustomMethod() $this->assertFalse($interceptor->isAllowedMethod('simple')); TestObjectWithMethods::$methods = ['closure' => function () { - }]; $this->assertFalse($interceptor->isAllowedMethod('closure')); @@ -193,7 +192,6 @@ public function testIsAllowedCustomMethod() * @kql-allowed */ 'closure' => function () { - } ]; $this->assertTrue($interceptor->isAllowedMethod('closure')); From a624a0165bc26250c11a34745325d59567c5f1b8 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Mon, 21 Nov 2022 19:57:43 +0100 Subject: [PATCH 19/28] Code clean-up --- src/Kql/Kql.php | 20 ++++++++++++++------ tests/Kql/KqlTest.php | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Kql/Kql.php b/src/Kql/Kql.php index c48e732..55d1016 100644 --- a/src/Kql/Kql.php +++ b/src/Kql/Kql.php @@ -110,8 +110,11 @@ public static function run($input, $model = null) return static::select($result, $select, $options); } - public static function select($data, $select, array $options = []) - { + public static function select( + $data, + array|string|null $select = null, + array $options = [] + ) { if ($select === null) { return static::render($data); } @@ -159,8 +162,11 @@ public static function selectFromArray(array $array, array $select): array /** * @internal */ - public static function selectFromCollection(Collection $collection, array|string $select, array $options = []) - { + public static function selectFromCollection( + Collection $collection, + array|string $select, + array $options = [] + ): array { if ($options['pagination'] ?? false) { $collection = $collection->paginate($options['pagination']); } @@ -190,8 +196,10 @@ public static function selectFromCollection(Collection $collection, array|string /** * @internal */ - public static function selectFromObject(object $object, array|string $select) - { + public static function selectFromObject( + object $object, + array|string $select + ): array { // replace actual object with intercepting proxy class $object = Interceptor::replace($object); $result = []; diff --git a/tests/Kql/KqlTest.php b/tests/Kql/KqlTest.php index 70bbfe9..ef6bf34 100644 --- a/tests/Kql/KqlTest.php +++ b/tests/Kql/KqlTest.php @@ -138,7 +138,7 @@ public function testRunForbiddenMethod() public function testSelect() { // no select, returns data via ::render - $result = Kql::select('foo', null); + $result = Kql::select('foo'); $this->assertSame('foo', $result); // help From dfe2ea6e7bdb4a70821fa8b161d5618a7b355164 Mon Sep 17 00:00:00 2001 From: Bastian Allgeier Date: Wed, 23 Nov 2022 11:51:56 +0100 Subject: [PATCH 20/28] Add support to composer.json --- composer.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/composer.json b/composer.json index fcc4a1d..43322fd 100755 --- a/composer.json +++ b/composer.json @@ -23,6 +23,12 @@ } ], "homepage": "https://getkirby.com", + "support": { + "email": "support@getkirby.com", + "issues": "https://github.com/getkirby/kql/issues", + "forum": "https://forum.getkirby.com", + "source": "https://github.com/getkirby/kql" + }, "require": { "php": ">=8.0.0 <8.2.0", "getkirby/cms": ">=3.8.2", From d5ec4d4867c96eb21a01286b4005fa2ca4288330 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Thu, 24 Nov 2022 09:49:12 +0100 Subject: [PATCH 21/28] Update version number 1.3.0-rc.1 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 43322fd..ee17f9c 100755 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Kirby Query Language", "license": "MIT", "type": "kirby-plugin", - "version": "1.2.0", + "version": "1.3.0-rc.1", "keywords": [ "kirby", "cms", From d5b12d72631271ac5ce2b5b6e6499f7b7cbd9b9a Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Mon, 5 Dec 2022 19:01:41 +0100 Subject: [PATCH 22/28] Upgrade version number --- composer.json | 2 +- composer.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index ee17f9c..ab6991e 100755 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Kirby Query Language", "license": "MIT", "type": "kirby-plugin", - "version": "1.3.0-rc.1", + "version": "1.3.0", "keywords": [ "kirby", "cms", diff --git a/composer.lock b/composer.lock index 4337daf..efee249 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5803be65e48773b737ab5f06e1f54d28", + "content-hash": "6e55fbcba999cb4cdf22b5817fee3d0a", "packages": [ { "name": "claviska/simpleimage", From d0d53a8e1be5df5e0b51a7899aa649106a3d77bf Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Mon, 5 Dec 2022 19:09:14 +0100 Subject: [PATCH 23/28] Prepare support for PHP 8.2 --- composer.json | 2 +- composer.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index ab6991e..06618fe 100755 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "source": "https://github.com/getkirby/kql" }, "require": { - "php": ">=8.0.0 <8.2.0", + "php": ">=8.0.0 <8.3.0", "getkirby/cms": ">=3.8.2", "getkirby/composer-installer": "^1.2.1" }, diff --git a/composer.lock b/composer.lock index efee249..ffac42c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6e55fbcba999cb4cdf22b5817fee3d0a", + "content-hash": "84d0ec0719df6a10e4805065986abf63", "packages": [ { "name": "claviska/simpleimage", @@ -918,7 +918,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.0.0 <8.2.0" + "php": ">=8.0.0 <8.3.0" }, "platform-dev": [], "plugin-api-version": "2.3.0" From 4d8d7438c86b7d8240aabd605e5eceea93a989f3 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Tue, 6 Dec 2022 21:12:46 +0100 Subject: [PATCH 24/28] Update version number to 2.0.0 --- composer.json | 2 +- composer.lock | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/composer.json b/composer.json index 06618fe..2d8a76a 100755 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Kirby Query Language", "license": "MIT", "type": "kirby-plugin", - "version": "1.3.0", + "version": "2.0.0", "keywords": [ "kirby", "cms", diff --git a/composer.lock b/composer.lock index ffac42c..6f10f77 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "84d0ec0719df6a10e4805065986abf63", + "content-hash": "8f2cd74600df8ed221feb7e6e2902171", "packages": [ { "name": "claviska/simpleimage", @@ -138,16 +138,16 @@ }, { "name": "filp/whoops", - "version": "2.14.5", + "version": "2.14.6", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "a63e5e8f26ebbebf8ed3c5c691637325512eb0dc" + "reference": "f7948baaa0330277c729714910336383286305da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/a63e5e8f26ebbebf8ed3c5c691637325512eb0dc", - "reference": "a63e5e8f26ebbebf8ed3c5c691637325512eb0dc", + "url": "https://api.github.com/repos/filp/whoops/zipball/f7948baaa0330277c729714910336383286305da", + "reference": "f7948baaa0330277c729714910336383286305da", "shasum": "" }, "require": { @@ -197,7 +197,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.14.5" + "source": "https://github.com/filp/whoops/tree/2.14.6" }, "funding": [ { @@ -205,20 +205,20 @@ "type": "github" } ], - "time": "2022-01-07T12:00:00+00:00" + "time": "2022-11-02T16:23:29+00:00" }, { "name": "getkirby/cms", - "version": "3.8.2", + "version": "3.8.3", "source": { "type": "git", "url": "https://github.com/getkirby/kirby.git", - "reference": "f16b0b41db19ab5dbcf22e27f4aa2c35cb2c490c" + "reference": "41719bd54310dfc2e321a75a8549da98ccf5cd1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getkirby/kirby/zipball/f16b0b41db19ab5dbcf22e27f4aa2c35cb2c490c", - "reference": "f16b0b41db19ab5dbcf22e27f4aa2c35cb2c490c", + "url": "https://api.github.com/repos/getkirby/kirby/zipball/41719bd54310dfc2e321a75a8549da98ccf5cd1d", + "reference": "41719bd54310dfc2e321a75a8549da98ccf5cd1d", "shasum": "" }, "require": { @@ -235,7 +235,7 @@ "ext-mbstring": "*", "ext-openssl": "*", "ext-simplexml": "*", - "filp/whoops": "2.14.5", + "filp/whoops": "2.14.6", "getkirby/composer-installer": "^1.2.1", "laminas/laminas-escaper": "2.12.0", "michelf/php-smartypants": "1.8.1", @@ -305,7 +305,7 @@ "type": "custom" } ], - "time": "2022-11-15T12:18:18+00:00" + "time": "2022-12-06T14:31:06+00:00" }, { "name": "getkirby/composer-installer", From 4d7b5fcea5b00ab67446ac2ddd4e1546cafc9720 Mon Sep 17 00:00:00 2001 From: Bastian Allgeier Date: Wed, 7 Dec 2022 14:26:17 +0100 Subject: [PATCH 25/28] Custom autoloader for Kql classes --- .gitignore | 2 +- composer.json | 3 +-- composer.lock | 2 +- extensions/autoload.php | 21 +++++++++++++++++++++ index.php | 10 +++++++--- tests/bootstrap.php | 2 ++ 6 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 extensions/autoload.php diff --git a/.gitignore b/.gitignore index aec3c3f..8322fd4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,4 @@ /.php-cs-fixer.cache /.phpunit.result.cache /tests/coverage -/tests/*/tmp \ No newline at end of file +/tests/*/tmp diff --git a/composer.json b/composer.json index 2d8a76a..ca79327 100755 --- a/composer.json +++ b/composer.json @@ -37,7 +37,6 @@ "autoload": { "psr-4": { "Kirby\\": [ - "src/", "tests/" ] } @@ -49,7 +48,7 @@ "optimize-autoloader": true }, "extra": { - "installer-name": "versions", + "installer-name": "kirby", "kirby-cms-path": false }, "scripts": { diff --git a/composer.lock b/composer.lock index 6f10f77..807addd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8f2cd74600df8ed221feb7e6e2902171", + "content-hash": "4e117f830d181ad338f44debeebf5eff", "packages": [ { "name": "claviska/simpleimage", diff --git a/extensions/autoload.php b/extensions/autoload.php new file mode 100644 index 0000000..54224fe --- /dev/null +++ b/extensions/autoload.php @@ -0,0 +1,21 @@ + require_once 'extensions/api.php' diff --git a/tests/bootstrap.php b/tests/bootstrap.php index f9b4c49..ccd64cf 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,5 +1,7 @@ Date: Wed, 7 Dec 2022 15:18:48 +0100 Subject: [PATCH 26/28] Fix installer name --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ca79327..3764358 100755 --- a/composer.json +++ b/composer.json @@ -48,7 +48,7 @@ "optimize-autoloader": true }, "extra": { - "installer-name": "kirby", + "installer-name": "kql", "kirby-cms-path": false }, "scripts": { From 6b24204c1e6d04197fed525d5d0a80c87406957f Mon Sep 17 00:00:00 2001 From: Bastian Allgeier Date: Wed, 7 Dec 2022 15:21:06 +0100 Subject: [PATCH 27/28] Normalize composer --- composer.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index 807addd..b522b2c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4e117f830d181ad338f44debeebf5eff", + "content-hash": "4f9434217019185bc7ab3807ed083bda", "packages": [ { "name": "claviska/simpleimage", From 7de44546fd2117759708e27fc5314507fa64a6eb Mon Sep 17 00:00:00 2001 From: Bastian Allgeier Date: Wed, 7 Dec 2022 15:24:03 +0100 Subject: [PATCH 28/28] Avoid directory traversal in class loader --- extensions/autoload.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/autoload.php b/extensions/autoload.php index 54224fe..274e1f0 100644 --- a/extensions/autoload.php +++ b/extensions/autoload.php @@ -7,7 +7,7 @@ function autoload(string $namespace, string $dir) { spl_autoload_register(function ($class) use ($namespace, $dir) { - if (str_starts_with($class, $namespace) === false) { + if (str_contains($class, '.') === true || str_starts_with($class, $namespace) === false) { return; }