diff --git a/public/site/plugins/kql/.editorconfig b/public/site/plugins/kql/.editorconfig
new file mode 100644
index 0000000..fa9ba60
--- /dev/null
+++ b/public/site/plugins/kql/.editorconfig
@@ -0,0 +1,24 @@
+# This file is for unifying the coding style for different editors and IDEs
+# editorconfig.org
+
+# PHP PSR-12 Coding Standards
+# https://www.php-fig.org/psr/psr-12/
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = tab
+indent_size = 2
+trim_trailing_whitespace = true
+
+[*.php]
+indent_size = 4
+insert_final_newline = true
+
+[*.yml]
+indent_style = space
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/public/site/plugins/kql/.github/FUNDING.yml b/public/site/plugins/kql/.github/FUNDING.yml
new file mode 100644
index 0000000..b481582
--- /dev/null
+++ b/public/site/plugins/kql/.github/FUNDING.yml
@@ -0,0 +1 @@
+custom: ['https://getkirby.com/buy']
diff --git a/public/site/plugins/kql/.gitignore b/public/site/plugins/kql/.gitignore
new file mode 100644
index 0000000..954190f
--- /dev/null
+++ b/public/site/plugins/kql/.gitignore
@@ -0,0 +1,29 @@
+# OS files
+.DS_Store
+
+# npm modules
+/node_modules
+
+# 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
+/.php-cs-fixer.cache
diff --git a/public/site/plugins/kql/.php-cs-fixer.dist.php b/public/site/plugins/kql/.php-cs-fixer.dist.php
new file mode 100644
index 0000000..5dc5e7a
--- /dev/null
+++ b/public/site/plugins/kql/.php-cs-fixer.dist.php
@@ -0,0 +1,60 @@
+exclude('dependencies')
+ ->exclude('panel/node_modules')
+ ->in(__DIR__);
+
+$config = new PhpCsFixer\Config();
+return $config
+ ->setRules([
+ '@PSR12' => 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)
+ 'combine_consecutive_issets' => true,
+ 'combine_consecutive_unsets' => true,
+ 'combine_nested_dirname' => true,
+ 'concat_space' => ['spacing' => 'one'],
+ 'declare_equal_normalize' => ['space' => 'single'],
+ 'dir_constant' => true,
+ 'function_typehint_space' => true,
+ 'include' => true,
+ 'logical_operators' => true,
+ 'lowercase_cast' => true,
+ 'lowercase_static_reference' => true,
+ 'magic_constant_casing' => true,
+ 'magic_method_casing' => true,
+ 'method_chaining_indentation' => true,
+ 'modernize_types_casting' => true,
+ 'multiline_comment_opening_closing' => true,
+ 'native_function_casing' => true,
+ 'native_function_type_declaration_casing' => true,
+ 'new_with_braces' => true,
+ 'no_blank_lines_after_class_opening' => true,
+ 'no_blank_lines_after_phpdoc' => true,
+ 'no_empty_comment' => true,
+ 'no_empty_phpdoc' => true,
+ 'no_empty_statement' => true,
+ 'no_leading_namespace_whitespace' => true,
+ 'no_mixed_echo_print' => ['use' => 'echo'],
+ 'no_unneeded_control_parentheses' => true,
+ '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_align' => ['align' => 'left'],
+ 'phpdoc_indent' => true,
+ 'phpdoc_scalar' => true,
+ 'phpdoc_trim' => true,
+ 'short_scalar_cast' => true,
+ 'single_line_comment_style' => true,
+ 'single_quote' => true,
+ 'ternary_to_null_coalescing' => true,
+ 'whitespace_after_comma_in_array' => true
+ ])
+ ->setRiskyAllowed(true)
+ ->setIndent("\t")
+ ->setFinder($finder);
diff --git a/public/site/plugins/kql/README.md b/public/site/plugins/kql/README.md
index b85e616..6ad9ed9 100755
--- a/public/site/plugins/kql/README.md
+++ b/public/site/plugins/kql/README.md
@@ -16,21 +16,21 @@ Given a POST request to: `/api/query`
```json
{
- "query": "page('photography').children",
- "select": {
- "url": true,
- "title": true,
- "text": "page.text.markdown",
- "images": {
- "query": "page.moodboard",
- "select": {
- "url": true
- }
- }
- },
- "pagination": {
- "limit": 10
+ "query": "page('photography').children",
+ "select": {
+ "url": true,
+ "title": true,
+ "text": "page.text.markdown",
+ "images": {
+ "query": "page.images",
+ "select": {
+ "url": true
+ }
}
+ },
+ "pagination": {
+ "limit": 10
+ }
}
```
@@ -39,48 +39,48 @@ Given a POST request to: `/api/query`
```json
{
- "code": 200,
- "result": {
- "data": [
- {
- "url": "https://example.com/photography/trees",
- "title": "Trees",
- "text": "Lorem ipsum …",
- "images": [
- {
- "url": "https://example.com/media/pages/photography/trees/1353177920-1579007734/cheesy-autumn.jpg"
- },
- {
- "url": "https://example.com/media/pages/photography/trees/1940579124-1579007734/last-tree-standing.jpg"
- },
- {
- "url": "https://example.com/media/pages/photography/trees/3506294441-1579007734/monster-trees-in-the-fog.jpg"
- }
- ]
- },
- {
- "url": "https://example.com/photography/sky",
- "title": "Sky",
- "text": "
Dolor sit amet
…",
- "images": [
- {
- "url": "https://example.com/media/pages/photography/sky/183363500-1579007734/blood-moon.jpg"
- },
- {
- "url": "https://example.com/media/pages/photography/sky/3904851178-1579007734/coconut-milkyway.jpg"
- }
- ]
- }
- ],
- "pagination": {
- "page": 1,
- "pages": 1,
- "offset": 0,
- "limit": 10,
- "total": 2
- }
- },
- "status": "ok"
+ "code": 200,
+ "result": {
+ "data": [
+ {
+ "url": "https://example.com/photography/trees",
+ "title": "Trees",
+ "text": "Lorem ipsum …",
+ "images": [
+ {
+ "url": "https://example.com/media/pages/photography/trees/1353177920-1579007734/cheesy-autumn.jpg"
+ },
+ {
+ "url": "https://example.com/media/pages/photography/trees/1940579124-1579007734/last-tree-standing.jpg"
+ },
+ {
+ "url": "https://example.com/media/pages/photography/trees/3506294441-1579007734/monster-trees-in-the-fog.jpg"
+ }
+ ]
+ },
+ {
+ "url": "https://example.com/photography/sky",
+ "title": "Sky",
+ "text": "Dolor sit amet
…",
+ "images": [
+ {
+ "url": "https://example.com/media/pages/photography/sky/183363500-1579007734/blood-moon.jpg"
+ },
+ {
+ "url": "https://example.com/media/pages/photography/sky/3904851178-1579007734/coconut-milkyway.jpg"
+ }
+ ]
+ }
+ ],
+ "pagination": {
+ "page": 1,
+ "pages": 1,
+ "offset": 0,
+ "limit": 10,
+ "total": 2
+ }
+ },
+ "status": "ok"
}
```
@@ -116,35 +116,34 @@ return [
### Sending POST Requests
-You can use any HTTP request library in your language of choice to make regular POST requests to your `/api/query` endpoint. In this example, we are using [the `fetch` API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) and JavaScript to retrieve data from our Kirby installation.
+You can use any HTTP request library in your language of choice to make regular POST requests to your `/api/query` endpoint. In this example, we are using [ohmyfetch](https://github.com/unjs/ohmyfetch) (a better fetch API for Node and the browser) and JavaScript to retreive data from our Kirby installation.
```js
+import { $fetch } from "ohmyfetch";
+
const api = "https://yoursite.com/api/query";
const username = "apiuser";
const password = "strong-secret-api-password";
const headers = {
- Authorization:
- "Basic " + Buffer.from(`${username}:${password}`).toString("base64"),
- "Content-Type": "application/json",
- Accept: "application/json",
+ Authorization: Buffer.from(`${username}:${password}`).toString("base64"),
};
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "page('notes').children",
- select: {
- title: true,
- text: "page.text.kirbytext",
- slug: true,
- date: "page.date.toDate('d.m.Y')",
- },
- }),
- headers,
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "page('notes').children",
+ select: {
+ title: true,
+ text: "page.text.kirbytext",
+ slug: true,
+ date: "page.date.toDate('d.m.Y')",
+ },
+ },
+ headers,
});
-console.log(await response.json());
+console.log(response);
```
### `query`
@@ -158,15 +157,15 @@ When you don't pass the select option, Kirby will try to come up with the most u
##### Fetching the Site Title
```js
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "site.title",
- }),
- headers,
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "site.title",
+ },
+ headers,
});
-console.log(await response.json());
+console.log(response);
```
@@ -185,15 +184,15 @@ console.log(await response.json());
##### Fetching a List of Page IDs
```js
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "site.children",
- }),
- headers,
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "site.children",
+ },
+ headers,
});
-console.log(await response.json());
+console.log(response);
```
@@ -220,15 +219,15 @@ console.log(await response.json());
Queries can even execute field methods.
```js
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "site.title.upper",
- }),
- headers,
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "site.title.upper",
+ },
+ headers,
});
-console.log(await response.json());
+console.log(response);
```
@@ -253,16 +252,16 @@ KQL becomes really powerful by its flexible way to control the result set with t
To include a property or field in your results, list them as an array. Check out our [reference for available properties](https://getkirby.com/docs/reference) for pages, users, files, etc.
```js
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "site.children",
- select: ["title", "url"],
- }),
- headers,
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "site.children",
+ select: ["title", "url"],
+ },
+ headers,
});
-console.log(await response.json());
+console.log(response);
```
@@ -311,19 +310,19 @@ console.log(await response.json());
You can also use the object notation and pass true for each key/property you want to include.
```js
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "site.children",
- select: {
- title: true,
- url: true,
- },
- }),
- headers,
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "site.children",
+ select: {
+ title: true,
+ url: true,
+ },
+ },
+ headers,
});
-console.log(await response.json());
+console.log(response);
```
@@ -368,18 +367,18 @@ console.log(await response.json());
Instead of passing true, you can also pass a string query to specify what you want to return for each key in your select object.
```js
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "site.children",
- select: {
- title: "page.title",
- },
- }),
- headers,
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "site.children",
+ select: {
+ title: "page.title",
+ },
+ },
+ headers,
});
-console.log(await response.json());
+console.log(response);
```
@@ -409,18 +408,18 @@ console.log(await response.json());
#### Executing Field Methods
```js
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "site.children",
- select: {
- title: "page.title.upper",
- },
- }),
- headers,
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "site.children",
+ select: {
+ title: "page.title.upper",
+ },
+ },
+ headers,
});
-console.log(await response.json());
+console.log(response);
```
@@ -452,23 +451,21 @@ console.log(await response.json());
String queries are a perfect way to create aliases or return variations of the same field or property multiple times.
```js
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "page('notes').children",
- select: {
- title: "page.title",
- upperCaseTitle: "page.title.upper",
- lowerCaseTitle: "page.title.lower",
- guid: "page.id",
- date: "page.date.toDate('d.m.Y')",
- timestamp: "page.date.toTimestamp",
- },
- }),
- headers,
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "page('notes').children",
+ select: {
+ title: "page.title",
+ upperCaseTitle: "page.title.upper",
+ lowerCaseTitle: "page.title.lower",
+ guid: "page.id",
+ date: "page.date.toDate('d.m.Y')",
+ timestamp: "page.date.toTimestamp",
+ },
+ },
+ headers,
});
-
-console.log(await response.json());
```
@@ -504,19 +501,17 @@ console.log(await response.json());
With such string queries you can of course also include nested data
```js
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "page('photography').children",
- select: {
- title: "page.title",
- images: "page.moodboard",
- },
- }),
- headers,
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "page('photography').children",
+ select: {
+ title: "page.title",
+ images: "page.images",
+ },
+ },
+ headers,
});
-
-console.log(await response.json());
```
@@ -554,24 +549,22 @@ console.log(await response.json());
You can also pass an object with a `query` and a `select` option
```js
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "page('photography').children",
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "page('photography').children",
+ select: {
+ title: "page.title",
+ images: {
+ query: "page.images",
select: {
- title: "page.title",
- images: {
- query: "page.moodboard",
- select: {
- filename: true,
- },
- },
+ filename: true,
},
- }),
- headers,
+ },
+ },
+ },
+ headers,
});
-
-console.log(await response.json());
```
@@ -623,21 +616,19 @@ Whenever you query a collection (pages, files, users, roles, languages) you can
You can specify a custom limit with the limit option. The default limit for collections is 100 entries.
```js
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "page('notes').children",
- pagination: {
- limit: 5,
- },
- select: {
- title: "page.title",
- },
- }),
- headers,
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "page('notes').children",
+ pagination: {
+ limit: 5,
+ },
+ select: {
+ title: "page.title",
+ },
+ },
+ headers,
});
-
-console.log(await response.json());
```
@@ -683,22 +674,20 @@ console.log(await response.json());
You can jump to any page in the resultset with the `page` option.
```js
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "page('notes').children",
- pagination: {
- page: 2,
- limit: 5,
- },
- select: {
- title: "page.title",
- },
- }),
- headers,
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "page('notes').children",
+ pagination: {
+ page: 2,
+ limit: 5,
+ },
+ select: {
+ title: "page.title",
+ },
+ },
+ headers,
});
-
-console.log(await response.json());
```
@@ -735,28 +724,26 @@ console.log(await response.json());
Pagination settings also work for subqueries.
```js
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "page('photography').children",
- select: {
- title: "page.title",
- images: {
- query: "page.moodboard",
- pagination: {
- page: 2,
- limit: 5,
- },
- select: {
- filename: true,
- },
- },
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "page('photography').children",
+ select: {
+ title: "page.title",
+ images: {
+ query: "page.images",
+ pagination: {
+ page: 2,
+ limit: 5,
},
- }),
- headers,
+ select: {
+ filename: true,
+ },
+ },
+ },
+ },
+ headers,
});
-
-console.log(await response.json());
```
### Multiple Queries in a Single Call
@@ -764,45 +751,43 @@ console.log(await response.json());
With the power of selects and subqueries you can basically query the entire site in a single request
```js
-const response = await fetch(api, {
- method: "post",
- body: JSON.stringify({
- query: "site",
+const response = await $fetch(api, {
+ method: "post",
+ body: {
+ query: "site",
+ select: {
+ title: "site.title",
+ url: "site.url",
+ notes: {
+ query: "page('notes').children.listed",
select: {
- title: "site.title",
- url: "site.url",
- notes: {
- query: "page('notes').children.listed",
- select: {
- title: true,
- url: true,
- date: "page.date.toDate('d.m.Y')",
- text: "page.text.kirbytext",
- },
- },
- photography: {
- query: "page('photography').children.listed",
- select: {
- title: true,
- images: {
- query: "page.moodboard",
- select: {
- url: true,
- alt: true,
- caption: "file.caption.kirbytext",
- },
- },
- },
- },
- about: {
- text: "page.text.kirbytext",
- },
+ title: true,
+ url: true,
+ date: "page.date.toDate('d.m.Y')",
+ text: "page.text.kirbytext",
},
- }),
- headers,
+ },
+ photography: {
+ query: "page('photography').children.listed",
+ select: {
+ title: true,
+ images: {
+ query: "page.images",
+ select: {
+ url: true,
+ alt: true,
+ caption: "file.caption.kirbytext",
+ },
+ },
+ },
+ },
+ about: {
+ text: "page.text.kirbytext",
+ },
+ },
+ },
+ headers,
});
-
-console.log(await response.json());
```
### Allowing Methods
@@ -951,7 +936,7 @@ If you want to fully allow access to an entire class without putting an intercep
return [
'kql' => [
'classes' => [
- 'allowed' => [
+ 'allow' => [
'Kirby\Cms\System'
]
]
@@ -967,23 +952,8 @@ KQL only offers access to data in your site. It does not support any mutations.
## Plugins
-- [KQL + 11ty](https://github.com/getkirby/eleventykit)
-- [KQL + Nuxt](https://nuxt-kql.jhnn.dev)
-
-## What's Kirby?
-
-- **[getkirby.com](https://getkirby.com)** – Get to know the CMS.
-- **[Try it](https://getkirby.com/try)** – Take a test ride with our online demo. Or download one of our kits to get started.
-- **[Documentation](https://getkirby.com/docs/guide)** – Read the official guide, reference and cookbook recipes.
-- **[Issues](https://github.com/getkirby/kirby/issues)** – Report bugs and other problems.
-- **[Feedback](https://feedback.getkirby.com)** – You have an idea for Kirby? Share it.
-- **[Forum](https://forum.getkirby.com)** – Whenever you get stuck, don't hesitate to reach out for questions and support.
-- **[Discord](https://chat.getkirby.com)** – Hang out and meet the community.
-- **[Mastodon](https://mastodon.social/@getkirby)** – Spread the word.
-- **[Instagram](https://www.instagram.com/getkirby/)** – Share your creations: #madewithkirby.
-
----
+- [nuxt-kql](https://nuxt-kql.jhnn.dev): A Nuxt 3 module for KQL.
## License
-[MIT](./LICENSE) License © 2020-2023 [Bastian Allgeier](https://getkirby.com)
+[MIT](./LICENSE) License © 2020-2022 [Bastian Allgeier](https://getkirby.com)
diff --git a/public/site/plugins/kql/composer.json b/public/site/plugins/kql/composer.json
index 6939d91..a7172ae 100755
--- a/public/site/plugins/kql/composer.json
+++ b/public/site/plugins/kql/composer.json
@@ -2,8 +2,7 @@
"name": "getkirby/kql",
"description": "Kirby Query Language",
"license": "MIT",
- "type": "kirby-plugin",
- "version": "2.1.0",
+ "version": "1.2.0",
"keywords": [
"kirby",
"cms",
@@ -12,62 +11,29 @@
"query",
"headless"
],
+ "homepage": "https://getkirby.com",
+ "type": "kirby-plugin",
"authors": [
{
"name": "Bastian Allgeier",
"email": "bastian@getkirby.com"
- },
- {
- "name": "Nico Hoffmann",
- "email": "nico@getkirby.com"
}
],
- "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.3.0",
- "getkirby/cms": ">=3.8.2",
"getkirby/composer-installer": "^1.2.1"
},
+ "config": {
+ "optimize-autoloader": true,
+ "allow-plugins": {
+ "getkirby/composer-installer": false
+ }
+ },
"autoload": {
"psr-4": {
- "Kirby\\": [
- "tests/"
- ]
+ "Kirby\\": "src/"
}
},
- "config": {
- "allow-plugins": {
- "getkirby/composer-installer": true
- },
- "optimize-autoloader": true
- },
- "extra": {
- "installer-name": "kql",
- "kirby-cms-path": false
- },
"scripts": {
- "analyze": [
- "@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",
- "@test"
- ],
- "fix": "php-cs-fixer fix",
- "test": "phpunit --stderr --coverage-html=tests/coverage"
+ "fix": "php-cs-fixer fix"
}
}
diff --git a/public/site/plugins/kql/composer.lock b/public/site/plugins/kql/composer.lock
new file mode 100644
index 0000000..e5902db
--- /dev/null
+++ b/public/site/plugins/kql/composer.lock
@@ -0,0 +1,66 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "b3661c52f0da1ad6c43e66f6001abb79",
+ "packages": [
+ {
+ "name": "getkirby/composer-installer",
+ "version": "1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/getkirby/composer-installer.git",
+ "reference": "c98ece30bfba45be7ce457e1102d1b169d922f3d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/getkirby/composer-installer/zipball/c98ece30bfba45be7ce457e1102d1b169d922f3d",
+ "reference": "c98ece30bfba45be7ce457e1102d1b169d922f3d",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0 || ^2.0"
+ },
+ "require-dev": {
+ "composer/composer": "^1.8 || ^2.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Kirby\\ComposerInstaller\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "Kirby\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Kirby's custom Composer installer for the Kirby CMS and for Kirby plugins",
+ "homepage": "https://getkirby.com",
+ "support": {
+ "issues": "https://github.com/getkirby/composer-installer/issues",
+ "source": "https://github.com/getkirby/composer-installer/tree/1.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://getkirby.com/buy",
+ "type": "custom"
+ }
+ ],
+ "time": "2020-12-28T12:54:39+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "plugin-api-version": "2.3.0"
+}
diff --git a/public/site/plugins/kql/extensions/aliases.php b/public/site/plugins/kql/extensions/aliases.php
deleted file mode 100644
index e3f1330..0000000
--- a/public/site/plugins/kql/extensions/aliases.php
+++ /dev/null
@@ -1,8 +0,0 @@
- 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/public/site/plugins/kql/extensions/autoload.php b/public/site/plugins/kql/extensions/autoload.php
deleted file mode 100644
index 274e1f0..0000000
--- a/public/site/plugins/kql/extensions/autoload.php
+++ /dev/null
@@ -1,21 +0,0 @@
- [
+ 'routes' => function ($kirby) {
+ return [
+ [
+ 'pattern' => 'query',
+ 'method' => 'POST|GET',
+ 'auth' => $kirby->option('kql.auth') === false ? false : true,
+ 'action' => function () {
+ $result = Kql::run(get());
-require_once __DIR__ . '/extensions/aliases.php';
-require_once __DIR__ . '/extensions/helpers.php';
-
-App::plugin('getkirby/kql', [
- 'api' => require_once 'extensions/api.php'
+ return [
+ 'code' => 200,
+ 'result' => $result,
+ 'status' => 'ok',
+ ];
+ }
+ ]
+ ];
+ }
+ ]
]);
diff --git a/public/site/plugins/kql/phpunit.xml.dist b/public/site/plugins/kql/phpunit.xml.dist
new file mode 100644
index 0000000..c7d5502
--- /dev/null
+++ b/public/site/plugins/kql/phpunit.xml.dist
@@ -0,0 +1,13 @@
+
+
+
+
+ ./src
+
+
+
+
+ ./tests/
+
+
+
diff --git a/public/site/plugins/kql/src/Kql/Help.php b/public/site/plugins/kql/src/Kql/Help.php
index faa6f21..eda82cf 100644
--- a/public/site/plugins/kql/src/Kql/Help.php
+++ b/public/site/plugins/kql/src/Kql/Help.php
@@ -2,46 +2,27 @@
namespace Kirby\Kql;
-use Kirby\Toolkit\A;
-use ReflectionClass;
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
{
- /**
- * Provides information about passed value
- * depending on its type
- */
- public static function for($value): array
+ public static function for($object)
{
- if (is_array($value) === true) {
- return static::forArray($value);
+ if (is_array($object) === true) {
+ return static::forArray($object);
}
- if (is_object($value) === true) {
- return static::forObject($value);
+ if (is_object($object) === true) {
+ return static::forObject($object);
}
return [
- 'type' => gettype($value),
- 'value' => $value
+ 'type' => gettype($object),
+ 'value' => $object
];
}
- /**
- * @internal
- */
- public static function forArray(array $array): array
+ public static function forArray(array $array)
{
return [
'type' => 'array',
@@ -49,42 +30,42 @@ class Help
];
}
- /**
- * Gathers information for method about
- * name, parameters, return type etc.
- * @internal
- */
- public static function forMethod(object $object, string $method): array
+ public static function forMethod($object, $method)
{
$reflection = new ReflectionMethod($object, $method);
- $returns = $reflection->getReturnType()?->getName();
+ $returns = null;
$params = [];
+ if ($returnType = $reflection->getReturnType()) {
+ $returns = $returnType->getName();
+ }
+
foreach ($reflection->getParameters() as $param) {
- $name = $param->getName();
- $required = $param->isOptional() === false;
- $type = $param->hasType() ? $param->getType()->getName() : null;
- $default = null;
+ $p = [
+ 'name' => $param->getName(),
+ 'required' => $param->isOptional() === false,
+ 'type' => $param->hasType() ? $param->getType()->getName() : null,
+ ];
if ($param->isDefaultValueAvailable()) {
- $default = $param->getDefaultValue();
+ $p['default'] = $param->getDefaultValue();
}
- $call = '';
+ $call = null;
- if ($type !== null) {
- $call = $type . ' ';
+ if ($p['type'] !== null) {
+ $call = $p['type'] . ' ';
}
- $call .= '$' . $name;
+ $call .= '$' . $p['name'];
- if ($required === false && $default !== null) {
- $call .= ' = ' . var_export($default, true);
+ if ($p['required'] === false && isset($p['default']) === true) {
+ $call .= ' = ' . var_export($p['default'], true);
}
$p['call'] = $call;
- $params[$name] = compact('name', 'type', 'required', 'default', 'call');
+ $params[$p['name']] = $p;
}
$call = '.' . $method;
@@ -101,11 +82,7 @@ class Help
];
}
- /**
- * Gathers informations for each unique method
- * @internal
- */
- public static function forMethods(object $object, array $methods): array
+ public static function forMethods($object, $methods)
{
$methods = array_unique($methods);
$reflection = [];
@@ -123,30 +100,11 @@ class Help
return $reflection;
}
- /**
- * Retrieves info for objects either from Interceptor (to
- * only list allowed methods) or via reflection
- * @internal
- */
- public static function forObject(object $object): array
+ public static function forObject($object)
{
- // get interceptor object to only return info on allowed methods
- $interceptor = Interceptor::replace($object);
+ $original = $object;
+ $object = Interceptor::replace($original);
- if ($interceptor instanceof Interceptor) {
- return $interceptor->__debugInfo();
- }
-
- // for original classes, use reflection
- $class = new ReflectionClass($object);
- $methods = A::map(
- $class->getMethods(),
- fn ($method) => static::forMethod($object, $method->getName())
- );
-
- return [
- 'type' => $class->getName(),
- 'methods' => $methods
- ];
+ return $object->__debugInfo();
}
}
diff --git a/public/site/plugins/kql/src/Kql/Interceptor.php b/public/site/plugins/kql/src/Kql/Interceptor.php
index 7f6e774..ee14959 100644
--- a/public/site/plugins/kql/src/Kql/Interceptor.php
+++ b/public/site/plugins/kql/src/Kql/Interceptor.php
@@ -2,294 +2,58 @@
namespace Kirby\Kql;
-use Closure;
-use Kirby\Cms\App;
-use Kirby\Exception\InvalidArgumentException;
+use Exception;
use Kirby\Exception\PermissionException;
-use Kirby\Toolkit\Str;
-use ReflectionFunction;
-use ReflectionMethod;
-use Throwable;
-/**
- * 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
+class Interceptor
{
- public const CLASS_ALIAS = null;
-
- protected $toArray = [];
-
- public function __construct(protected $object)
- {
- }
-
- /**
- * 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);
- }
-
- $this->forbiddenMethod($method);
- }
-
- /**
- * Return information about corresponding object
- * incl. information about allowed methods
- */
- public function __debugInfo(): array
- {
- $help = Help::forMethods($this->object, $this->allowedMethods());
-
- return [
- 'type' => $this::CLASS_ALIAS,
- 'methods' => $help,
- 'value' => $this->toArray()
- ];
- }
-
- /**
- * Returns list of allowed classes. Specific list
- * to be implemented in specific interceptor child classes.
- * @codeCoverageIgnore
- */
- public function allowedMethods(): array
- {
- return [];
- }
-
- /**
- * 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);
- }
-
- /**
- * Throws exception for accessing a restricted method
- * @throws \Kirby\Exception\PermissionException
- */
- protected function forbiddenMethod(string $method)
- {
- $name = get_class($this->object) . '::' . $method . '()';
- throw new PermissionException('The method "' . $name . '" is not allowed in the API context');
- }
-
- /**
- * Checks if method is allowed to call
- */
- public 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;
- }
-
- /**
- * 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
- if (property_exists($this->object, 'methods') === false) {
- return false;
- }
-
- // does not have that method
- if (!$call = $this->method($method)) {
- return false;
- }
-
- // check for a docblock comment
- if ($this->isAllowedCallable($call) === true) {
- return true;
- }
-
- 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)
- {
- 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 InvalidArgumentException('Unsupported value: ' . gettype($object));
+ throw new Exception('Unsupported value: ' . gettype($object));
}
- $kirby = App::instance();
- $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);
+ $className = get_class($object);
+ $fullName = strtolower($className);
+ $blocked = array_map('strtolower', option('kql.classes.blocked', []));
// check in the block list from the config
- if (in_array($name, $blocked) === true) {
- throw new PermissionException('Access to the class "' . $class . '" is blocked');
+ if (in_array($fullName, $blocked) === true) {
+ throw new PermissionException('Access to the class "' . $className . '" is blocked');
}
- // 2. Is $object already an interceptor?
// directly return interceptor objects
- if ($object instanceof Interceptor) {
+ if (is_a($object, 'Kirby\\Kql\\Interceptors\\Interceptor') === true) {
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);
+ $interceptors = array_change_key_case(option('kql.interceptors', []), CASE_LOWER);
// load an interceptor from config if it exists and otherwise fall back to a built-in interceptor
- $interceptor = $interceptors[$name] ?? static::class($class);
+ $interceptor = $interceptors[$fullName] ?? str_replace('Kirby\\', 'Kirby\\Kql\\Interceptors\\', $className);
// check for a valid interceptor class
- if ($class !== $interceptor && class_exists($interceptor) === true) {
+ if ($className !== $interceptor && class_exists($interceptor) === true) {
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);
+ $interceptor = str_replace('Kirby\\', 'Kirby\\Kql\\Interceptors\\', $parent);
if (class_exists($interceptor) === true) {
return new $interceptor($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);
+ $allowed = array_map('strtolower', option('kql.classes.allowed', []));
// return the plain object if it is allowed
- if (in_array($name, $allowed) === true) {
+ if (in_array($fullName, $allowed) === true) {
return $object;
}
- // 6. None of the above? Block class.
- 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);
- }
-
- /**
- * Mirrors by default ::toArray but can be
- * implemented differently by specifc interceptor.
- * KQL will prefer ::toResponse over ::toArray
- */
- public function toResponse()
- {
- return $this->toArray();
+ throw new PermissionException('Access to the class "' . $className . '" is not supported');
}
}
diff --git a/public/site/plugins/kql/src/Kql/Interceptors/Cms/App.php b/public/site/plugins/kql/src/Kql/Interceptors/Cms/App.php
index e984dbb..33acb65 100644
--- a/public/site/plugins/kql/src/Kql/Interceptors/Cms/App.php
+++ b/public/site/plugins/kql/src/Kql/Interceptors/Cms/App.php
@@ -2,7 +2,7 @@
namespace Kirby\Kql\Interceptors\Cms;
-use Kirby\Kql\Interceptor;
+use Kirby\Kql\Interceptors\Interceptor;
class App extends Interceptor
{
diff --git a/public/site/plugins/kql/src/Kql/Interceptors/Cms/Blueprint.php b/public/site/plugins/kql/src/Kql/Interceptors/Cms/Blueprint.php
index d05aea0..016a6a1 100644
--- a/public/site/plugins/kql/src/Kql/Interceptors/Cms/Blueprint.php
+++ b/public/site/plugins/kql/src/Kql/Interceptors/Cms/Blueprint.php
@@ -2,7 +2,7 @@
namespace Kirby\Kql\Interceptors\Cms;
-use Kirby\Kql\Interceptor;
+use Kirby\Kql\Interceptors\Interceptor;
class Blueprint extends Interceptor
{
diff --git a/public/site/plugins/kql/src/Kql/Interceptors/Cms/Collection.php b/public/site/plugins/kql/src/Kql/Interceptors/Cms/Collection.php
index 0608365..5d7dec6 100644
--- a/public/site/plugins/kql/src/Kql/Interceptors/Cms/Collection.php
+++ b/public/site/plugins/kql/src/Kql/Interceptors/Cms/Collection.php
@@ -2,7 +2,7 @@
namespace Kirby\Kql\Interceptors\Cms;
-use Kirby\Kql\Interceptor;
+use Kirby\Kql\Interceptors\Interceptor;
class Collection extends Interceptor
{
diff --git a/public/site/plugins/kql/src/Kql/Interceptors/Content/Content.php b/public/site/plugins/kql/src/Kql/Interceptors/Cms/Content.php
similarity index 88%
rename from public/site/plugins/kql/src/Kql/Interceptors/Content/Content.php
rename to public/site/plugins/kql/src/Kql/Interceptors/Cms/Content.php
index e1e75e4..35e2309 100644
--- a/public/site/plugins/kql/src/Kql/Interceptors/Content/Content.php
+++ b/public/site/plugins/kql/src/Kql/Interceptors/Cms/Content.php
@@ -1,8 +1,8 @@
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/public/site/plugins/kql/src/Kql/Interceptors/Panel/Model.php b/public/site/plugins/kql/src/Kql/Interceptors/Panel/Model.php
index 006d47f..6c0bbda 100755
--- a/public/site/plugins/kql/src/Kql/Interceptors/Panel/Model.php
+++ b/public/site/plugins/kql/src/Kql/Interceptors/Panel/Model.php
@@ -2,7 +2,7 @@
namespace Kirby\Kql\Interceptors\Panel;
-use Kirby\Kql\Interceptor;
+use Kirby\Kql\Interceptors\Interceptor;
class Model extends Interceptor
{
diff --git a/public/site/plugins/kql/src/Kql/Interceptors/Toolkit/Obj.php b/public/site/plugins/kql/src/Kql/Interceptors/Toolkit/Obj.php
index 0ac2861..ba131f0 100755
--- a/public/site/plugins/kql/src/Kql/Interceptors/Toolkit/Obj.php
+++ b/public/site/plugins/kql/src/Kql/Interceptors/Toolkit/Obj.php
@@ -2,7 +2,7 @@
namespace Kirby\Kql\Interceptors\Toolkit;
-use Kirby\Kql\Interceptor;
+use Kirby\Kql\Interceptors\Interceptor;
class Obj extends Interceptor
{
diff --git a/public/site/plugins/kql/src/Kql/Kql.php b/public/site/plugins/kql/src/Kql/Kql.php
index 55d1016..bbfa631 100644
--- a/public/site/plugins/kql/src/Kql/Kql.php
+++ b/public/site/plugins/kql/src/Kql/Kql.php
@@ -3,81 +3,16 @@
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 fetch($model, $key, $selection)
- {
- // simple key/value
- if ($selection === true) {
- return static::render($model->$key());
- }
-
- // selection without additional query
- if (
- is_array($selection) === true &&
- empty($selection['query']) === true
- ) {
- return static::select(
- $model->$key(),
- $selection['select'] ?? null,
- $selection['options'] ?? []
- );
- }
-
- // nested queries
- 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
+ public static function help($object)
{
return Help::for($object);
}
- public static function query(string $query, $model = null)
- {
- $model ??= App::instance()->site();
- $data = [$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) {
- return $object->toResponse();
- }
-
- if (method_exists($object, 'toArray') === true) {
- return $object->toArray();
- }
-
- throw new Exception('The object "' . get_class($object) . '" cannot be rendered. Try querying one of its methods instead.');
- }
-
- return $value;
- }
-
public static function run($input, $model = null)
{
// string queries
@@ -97,9 +32,11 @@ class Kql
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) {
@@ -107,14 +44,74 @@ class Kql
}
$result = static::query($query, $model);
+
return static::select($result, $select, $options);
}
- public static function select(
- $data,
- array|string|null $select = null,
- array $options = []
- ) {
+ public static function fetch($model, $key, $selection)
+ {
+ // simple key/value
+ if ($selection === true) {
+ return static::render($model->$key());
+ }
+
+ // selection without additional query
+ if (is_array($selection) === true && empty($selection['query']) === true) {
+ return static::select($model->$key(), $selection['select'] ?? null, $selection['options'] ?? []);
+ }
+
+ // nested queries
+ return static::run($selection, $model);
+ }
+
+ public static function query(string $query, $model = null)
+ {
+ $kirby = kirby();
+ $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();
+ }
+
+ public static function render($value)
+ {
+ if (is_object($value) === true) {
+ $object = Interceptor::replace($value);
+
+ if (method_exists($object, 'toResponse') === true) {
+ return $object->toResponse();
+ }
+
+ if (method_exists($object, 'toArray') === true) {
+ return $object->toArray();
+ }
+
+ throw new Exception('The object "' . get_class($object) . '" cannot be rendered. Try querying one of its methods instead.');
+ }
+
+ return $value;
+ }
+
+ public static function select($data, $select, array $options = [])
+ {
if ($select === null) {
return static::render($data);
}
@@ -123,23 +120,20 @@ class Kql
return static::help($data);
}
- if ($data instanceof Collection) {
+ if (is_a($data, 'Kirby\Cms\Collection') === true) {
return static::selectFromCollection($data, $select, $options);
}
if (is_object($data) === true) {
- return static::selectFromObject($data, $select);
+ return static::selectFromObject($data, $select, $options);
}
if (is_array($data) === true) {
- return static::selectFromArray($data, $select);
+ return static::selectFromArray($data, $select, $options);
}
}
- /**
- * @internal
- */
- public static function selectFromArray(array $array, array $select): array
+ public static function selectFromArray($array, $select, array $options = [])
{
$result = [];
@@ -159,14 +153,8 @@ class Kql
return $result;
}
- /**
- * @internal
- */
- public static function selectFromCollection(
- Collection $collection,
- array|string $select,
- array $options = []
- ): array {
+ public static function selectFromCollection(Collection $collection, $select, array $options = [])
+ {
if ($options['pagination'] ?? false) {
$collection = $collection->paginate($options['pagination']);
}
@@ -193,14 +181,8 @@ class Kql
return $data;
}
- /**
- * @internal
- */
- public static function selectFromObject(
- object $object,
- array|string $select
- ): array {
- // replace actual object with intercepting proxy class
+ public static function selectFromObject($object, $select, array $options = [])
+ {
$object = Interceptor::replace($object);
$result = [];
diff --git a/public/site/plugins/kql/src/Kql/Query.php b/public/site/plugins/kql/src/Kql/Query.php
index e4eebcb..7cd012b 100644
--- a/public/site/plugins/kql/src/Kql/Query.php
+++ b/public/site/plugins/kql/src/Kql/Query.php
@@ -2,28 +2,71 @@
namespace Kirby\Kql;
-use Kirby\Query\Query as BaseQuery;
+use Kirby\Toolkit\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
{
- /**
- * 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
+ protected function interceptor($object)
{
- return is_object($result) ? Interceptor::replace($result): $result;
+ 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)
+ {
+ // 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;
}
}
diff --git a/public/site/plugins/kql/tests/InterceptorTest.php b/public/site/plugins/kql/tests/InterceptorTest.php
new file mode 100644
index 0000000..8bb146e
--- /dev/null
+++ b/public/site/plugins/kql/tests/InterceptorTest.php
@@ -0,0 +1,175 @@
+ new Page([
+ 'slug' => 'test'
+ ]),
+ 'name' => 'test',
+ ]),
+ 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint'
+ ],
+ [
+ new Content(),
+ 'Kirby\\Kql\\Interceptors\\Cms\\Content'
+ ],
+ [
+ new Field(null, 'key', 'value'),
+ 'Kirby\\Kql\\Interceptors\\Cms\\Field'
+ ],
+ [
+ new File(['filename' => 'test.jpg']),
+ 'Kirby\\Kql\\Interceptors\\Cms\\File'
+ ],
+ [
+ new FileBlueprint([
+ 'model' => new File([
+ 'filename' => 'test.jpg'
+ ]),
+ 'name' => 'test',
+ ]),
+ 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint'
+ ],
+ [
+ new FileExtended(['filename' => 'test.jpg']),
+ 'Kirby\\Kql\\Interceptors\\Cms\\File'
+ ],
+ [
+ new FileVersion([
+ 'original' => new File([
+ 'filename' => 'test.jpg',
+ ]),
+ 'url' => '/test.jpg'
+ ]),
+ 'Kirby\\Kql\\Interceptors\\Cms\\FileVersion'
+ ],
+ [
+ new Page(['slug' => 'test']),
+ 'Kirby\\Kql\\Interceptors\\Cms\\Page'
+ ],
+ [
+ new PageBlueprint([
+ 'model' => new Page([
+ 'slug' => 'test'
+ ]),
+ 'name' => 'test',
+ ]),
+ 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint'
+ ],
+ [
+ new PageExtended(['slug' => 'test']),
+ 'Kirby\\Kql\\Interceptors\\Cms\\Page'
+ ],
+ [
+ new Role(['name' => 'admin']),
+ 'Kirby\\Kql\\Interceptors\\Cms\\Role'
+ ],
+ [
+ new Site(),
+ 'Kirby\\Kql\\Interceptors\\Cms\\Site'
+ ],
+ [
+ new SiteBlueprint([
+ 'model' => new Site(),
+ 'name' => 'test',
+ ]),
+ 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint'
+ ],
+ [
+ new SiteExtended(),
+ 'Kirby\\Kql\\Interceptors\\Cms\\Site'
+ ],
+ [
+ new User(['email' => 'test@getkirby.com']),
+ 'Kirby\\Kql\\Interceptors\\Cms\\User'
+ ],
+ [
+ new UserBlueprint([
+ 'model' => new User(['email' => 'test@getkirby.com']),
+ 'name' => 'test',
+ ]),
+ 'Kirby\\Kql\\Interceptors\\Cms\\Blueprint'
+ ],
+ [
+ new UserExtended(['email' => 'test@getkirby.com']),
+ 'Kirby\\Kql\\Interceptors\\Cms\\User'
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider objectProvider
+ */
+ public function testReplace($object, $inspector)
+ {
+ $result = Interceptor::replace($object);
+ $this->assertInstanceOf($inspector, $result);
+ }
+
+ public function testReplaceNonObject()
+ {
+ $this->expectException('Exception');
+ $this->expectExceptionMessage('Unsupported value: string');
+
+ $result = Interceptor::replace('hello');
+ }
+
+ public function testReplaceUnknownObject()
+ {
+ $this->expectException('Kirby\Exception\PermissionException');
+ $this->expectExceptionMessage('Access to the class "stdClass" is not supported');
+
+ $object = new \stdClass();
+ $result = Interceptor::replace($object);
+ }
+}
diff --git a/public/site/plugins/kql/tests/KqlTest.php b/public/site/plugins/kql/tests/KqlTest.php
new file mode 100644
index 0000000..f565938
--- /dev/null
+++ b/public/site/plugins/kql/tests/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 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/public/site/plugins/kql/tests/bootstrap.php b/public/site/plugins/kql/tests/bootstrap.php
new file mode 100644
index 0000000..518b666
--- /dev/null
+++ b/public/site/plugins/kql/tests/bootstrap.php
@@ -0,0 +1,25 @@
+