Initial commit
This commit is contained in:
commit
08a8a71c55
631 changed files with 139902 additions and 0 deletions
21
public/site/plugins/kql/LICENSE.md
Executable file
21
public/site/plugins/kql/LICENSE.md
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Bastian Allgeier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
987
public/site/plugins/kql/README.md
Executable file
987
public/site/plugins/kql/README.md
Executable file
|
|
@ -0,0 +1,987 @@
|
|||
# Kirby QL
|
||||
|
||||
Kirby's Query Language API combines the flexibility of Kirby's data structures, the power of GraphQL and the simplicity of REST.
|
||||
|
||||
The Kirby QL API takes POST requests with standard JSON objects and returns highly customized results that fit your application.
|
||||
|
||||
## Playground
|
||||
|
||||
You can play in our [KQL sandbox](https://kql.getkirby.com). The sandbox is based on the Kirby starterkit.
|
||||
|
||||
> ℹ️ Source code of the playground is [available on GitHub](https://github.com/getkirby/kql.getkirby.com).
|
||||
|
||||
## Example
|
||||
|
||||
Given a POST request to: `/api/query`
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "page('photography').children",
|
||||
"select": {
|
||||
"url": true,
|
||||
"title": true,
|
||||
"text": "page.text.markdown",
|
||||
"images": {
|
||||
"query": "page.images",
|
||||
"select": {
|
||||
"url": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"limit": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<details open>
|
||||
<summary>🆗 Response</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"result": {
|
||||
"data": [
|
||||
{
|
||||
"url": "https://example.com/photography/trees",
|
||||
"title": "Trees",
|
||||
"text": "Lorem <strong>ipsum</strong> …",
|
||||
"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": "<h1>Dolor sit amet</h1> …",
|
||||
"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"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Installation
|
||||
|
||||
### Manual
|
||||
|
||||
[Download](https://github.com/getkirby/kql/releases) and copy this repository to `/site/plugins/kql` of your Kirby installation.
|
||||
|
||||
### Composer
|
||||
|
||||
```bash
|
||||
composer require getkirby/kql
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### API Endpoint
|
||||
|
||||
KQL adds a new `query` API endpoint to your Kirby API (i.e. `yoursite.com/api/query`). This endpoint [requires authentication](https://getkirby.com/docs/guide/api/authentication).
|
||||
|
||||
You can switch off authentication in your config at your own risk:
|
||||
|
||||
```php
|
||||
return [
|
||||
'kql' => [
|
||||
'auth' => false
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```js
|
||||
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",
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
### `query`
|
||||
|
||||
With the query, you can fetch data from anywhere in your Kirby site. You can query fields, pages, files, users, languages, roles and more.
|
||||
|
||||
#### Queries Without Selects
|
||||
|
||||
When you don't pass the select option, Kirby will try to come up with the most useful result set for you. This is great for simple queries.
|
||||
|
||||
##### Fetching the Site Title
|
||||
|
||||
```js
|
||||
const response = await fetch(api, {
|
||||
method: "post",
|
||||
body: JSON.stringify({
|
||||
query: "site.title",
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🆗 Response</summary>
|
||||
|
||||
```js
|
||||
{
|
||||
code: 200,
|
||||
result: "Kirby Starterkit",
|
||||
status: "ok"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
##### Fetching a List of Page IDs
|
||||
|
||||
```js
|
||||
const response = await fetch(api, {
|
||||
method: "post",
|
||||
body: JSON.stringify({
|
||||
query: "site.children",
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🆗 Response</summary>
|
||||
|
||||
```js
|
||||
{
|
||||
code: 200,
|
||||
result: [
|
||||
"photography",
|
||||
"notes",
|
||||
"about",
|
||||
"error",
|
||||
"home"
|
||||
],
|
||||
status: "ok"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Running Field Methods
|
||||
|
||||
Queries can even execute field methods.
|
||||
|
||||
```js
|
||||
const response = await fetch(api, {
|
||||
method: "post",
|
||||
body: JSON.stringify({
|
||||
query: "site.title.upper",
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🆗 Response</summary>
|
||||
|
||||
```js
|
||||
{
|
||||
code: 200,
|
||||
result: "KIRBY STARTERKIT",
|
||||
status: "ok"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### `select`
|
||||
|
||||
KQL becomes really powerful by its flexible way to control the result set with the select option.
|
||||
|
||||
#### Select Single Properties and Fields
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🆗 Response</summary>
|
||||
|
||||
```js
|
||||
{
|
||||
code: 200,
|
||||
result: {
|
||||
data: [
|
||||
{
|
||||
title: "Photography",
|
||||
url: "/photography"
|
||||
},
|
||||
{
|
||||
title: "Notes",
|
||||
url: "/notes"
|
||||
},
|
||||
{
|
||||
title: "About us",
|
||||
url: "/about"
|
||||
},
|
||||
{
|
||||
title: "Error",
|
||||
url: "/error"
|
||||
},
|
||||
{
|
||||
title: "Home",
|
||||
url: "/"
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
page: 1,
|
||||
pages: 1,
|
||||
offset: 0,
|
||||
limit: 100,
|
||||
total: 5
|
||||
}
|
||||
},
|
||||
status: "ok"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🆗 Response</summary>
|
||||
|
||||
```js
|
||||
{
|
||||
code: 200,
|
||||
result: {
|
||||
data: [
|
||||
{
|
||||
title: "Photography",
|
||||
url: "/photography"
|
||||
},
|
||||
{
|
||||
title: "Notes",
|
||||
url: "/notes"
|
||||
},
|
||||
{
|
||||
title: "About us",
|
||||
url: "/about"
|
||||
},
|
||||
{
|
||||
title: "Error",
|
||||
url: "/error"
|
||||
},
|
||||
{
|
||||
title: "Home",
|
||||
url: "/"
|
||||
}
|
||||
],
|
||||
pagination: { ... }
|
||||
},
|
||||
status: "ok"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Using Queries for Properties and Fields
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🆗 Response</summary>
|
||||
|
||||
```js
|
||||
{
|
||||
code: 200,
|
||||
result: {
|
||||
data: [
|
||||
{
|
||||
title: "Photography",
|
||||
},
|
||||
{
|
||||
title: "Notes",
|
||||
},
|
||||
...
|
||||
],
|
||||
pagination: { ... }
|
||||
},
|
||||
status: "ok"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Executing Field Methods
|
||||
|
||||
```js
|
||||
const response = await fetch(api, {
|
||||
method: "post",
|
||||
body: JSON.stringify({
|
||||
query: "site.children",
|
||||
select: {
|
||||
title: "page.title.upper",
|
||||
},
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🆗 Response</summary>
|
||||
|
||||
```js
|
||||
{
|
||||
code: 200,
|
||||
result: {
|
||||
data: [
|
||||
{
|
||||
title: "PHOTOGRAPHY",
|
||||
},
|
||||
{
|
||||
title: "NOTES",
|
||||
},
|
||||
...
|
||||
],
|
||||
pagination: { ... }
|
||||
},
|
||||
status: "ok"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Creating Aliases
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🆗 Response</summary>
|
||||
|
||||
```js
|
||||
{
|
||||
code: 200,
|
||||
result: {
|
||||
data: [
|
||||
{
|
||||
title: "Explore the universe",
|
||||
upperCaseTitle: "EXPLORE THE UNIVERSE",
|
||||
lowerCaseTitle: "explore the universe",
|
||||
guid: "notes/explore-the-universe",
|
||||
date: "21.04.2018",
|
||||
timestamp: 1524316200
|
||||
},
|
||||
{ ... },
|
||||
{ ... },
|
||||
...
|
||||
],
|
||||
pagination: { ... }
|
||||
},
|
||||
status: "ok"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Subqueries
|
||||
|
||||
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.images",
|
||||
},
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🆗 Response</summary>
|
||||
|
||||
```js
|
||||
{
|
||||
code: 200,
|
||||
result: {
|
||||
data: [
|
||||
{
|
||||
title: "Trees",
|
||||
images: [
|
||||
"photography/trees/cheesy-autumn.jpg",
|
||||
"photography/trees/last-tree-standing.jpg",
|
||||
"photography/trees/monster-trees-in-the-fog.jpg",
|
||||
"photography/trees/sharewood-forest.jpg",
|
||||
"photography/trees/stay-in-the-car.jpg"
|
||||
]
|
||||
},
|
||||
{ ... },
|
||||
{ ... },
|
||||
...
|
||||
],
|
||||
pagination: { ... }
|
||||
},
|
||||
status: "ok"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Subqueries With Selects
|
||||
|
||||
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",
|
||||
select: {
|
||||
title: "page.title",
|
||||
images: {
|
||||
query: "page.images",
|
||||
select: {
|
||||
filename: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🆗 Response</summary>
|
||||
|
||||
```js
|
||||
{
|
||||
code: 200,
|
||||
result: {
|
||||
data: [
|
||||
{
|
||||
title: "Trees",
|
||||
images: {
|
||||
{
|
||||
filename: "cheesy-autumn.jpg"
|
||||
},
|
||||
{
|
||||
filename: "last-tree-standing.jpg"
|
||||
},
|
||||
{
|
||||
filename: "monster-trees-in-the-fog.jpg"
|
||||
},
|
||||
{
|
||||
filename: "sharewood-forest.jpg"
|
||||
},
|
||||
{
|
||||
filename: "stay-in-the-car.jpg"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ ... },
|
||||
{ ... },
|
||||
...
|
||||
],
|
||||
pagination: { ... }
|
||||
},
|
||||
status: "ok"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Pagination
|
||||
|
||||
Whenever you query a collection (pages, files, users, roles, languages) you can limit the resultset and also paginate through entries. You've probably already seen the pagination object in the results above. It is included in all results for collections, even if you didn't specify any pagination settings.
|
||||
|
||||
#### `limit`
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🆗 Response</summary>
|
||||
|
||||
```js
|
||||
{
|
||||
code: 200,
|
||||
result: {
|
||||
data: [
|
||||
{
|
||||
title: "Across the ocean"
|
||||
},
|
||||
{
|
||||
title: "A night in the forest"
|
||||
},
|
||||
{
|
||||
title: "In the jungle of Sumatra"
|
||||
},
|
||||
{
|
||||
title: "Through the desert"
|
||||
},
|
||||
{
|
||||
title: "Himalaya and back"
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
page: 1,
|
||||
pages: 2,
|
||||
offset: 0,
|
||||
limit: 5,
|
||||
total: 7
|
||||
}
|
||||
},
|
||||
status: "ok"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### `page`
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🆗 Response</summary>
|
||||
|
||||
```js
|
||||
{
|
||||
code: 200,
|
||||
result: {
|
||||
data: [
|
||||
{
|
||||
title: "Chasing waterfalls"
|
||||
},
|
||||
{
|
||||
title: "Exploring the universe"
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
page: 2,
|
||||
pages: 2,
|
||||
offset: 5,
|
||||
limit: 5,
|
||||
total: 7
|
||||
}
|
||||
},
|
||||
status: "ok"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Pagination in Subqueries
|
||||
|
||||
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.images",
|
||||
pagination: {
|
||||
page: 2,
|
||||
limit: 5,
|
||||
},
|
||||
select: {
|
||||
filename: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
### Multiple Queries in a Single Call
|
||||
|
||||
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",
|
||||
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.images",
|
||||
select: {
|
||||
url: true,
|
||||
alt: true,
|
||||
caption: "file.caption.kirbytext",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
about: {
|
||||
text: "page.text.kirbytext",
|
||||
},
|
||||
},
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
|
||||
console.log(await response.json());
|
||||
```
|
||||
|
||||
### Allowing Methods
|
||||
|
||||
KQL is very strict with allowed methods by default. Custom page methods, file methods or model methods are not allowed to make sure you don't miss an important security issue by accident. You can allow additional methods though.
|
||||
|
||||
#### Allow List
|
||||
|
||||
The most straight forward way is to define allowed methods in your config.
|
||||
|
||||
```php
|
||||
return [
|
||||
'kql' => [
|
||||
'methods' => [
|
||||
'allowed' => [
|
||||
'MyCustomPage::cover'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
#### DocBlock Comment
|
||||
|
||||
You can also add a comment to your methods' doc blocks to allow them:
|
||||
|
||||
```php
|
||||
class MyCustomPage extends Page
|
||||
{
|
||||
/**
|
||||
* @kql-allowed
|
||||
*/
|
||||
public function cover()
|
||||
{
|
||||
return $this->images()->findBy('name', 'cover') ?? $this->image();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This works for model methods as well as for custom page methods, file methods or other methods defined in plugins.
|
||||
|
||||
```php
|
||||
Kirby::plugin('your-name/your-plugin', [
|
||||
'pageMethods' => [
|
||||
/**
|
||||
* @kql-allowed
|
||||
*/
|
||||
'cover' => function () {
|
||||
return $this->images()->findBy('name', 'cover') ?? $this->image();
|
||||
}
|
||||
]
|
||||
]);
|
||||
```
|
||||
|
||||
### Blocking Methods
|
||||
|
||||
You can block individual class methods that would normally be accessible by listing them in your config:
|
||||
|
||||
```php
|
||||
return [
|
||||
'kql' => [
|
||||
'methods' => [
|
||||
'blocked' => [
|
||||
'Kirby\Cms\Page::url'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
### Blocking Classes
|
||||
|
||||
Sometimes you might want to reduce access to various parts of the system. This can be done by blocking individual methods (see above) or by blocking entire classes.
|
||||
|
||||
```php
|
||||
return [
|
||||
'kql' => [
|
||||
'classes' => [
|
||||
'blocked' => [
|
||||
'Kirby\Cms\User'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
Now, access to any user is blocked.
|
||||
|
||||
### Custom Classes and Interceptors
|
||||
|
||||
If you want to add support for a custom class or a class in Kirby's source that is not supported yet, you can list your own interceptors in your config
|
||||
|
||||
```php
|
||||
return [
|
||||
'kql' => [
|
||||
'interceptors' => [
|
||||
'Kirby\Cms\System' => 'SystemInterceptor'
|
||||
]
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
You can put the class for such a custom interceptor in a plugin for example.
|
||||
|
||||
```php
|
||||
class SystemInterceptor extends Kirby\Kql\Interceptors\Interceptor
|
||||
{
|
||||
public const CLASS_ALIAS = 'system';
|
||||
|
||||
protected $toArray = [
|
||||
'isInstallable',
|
||||
];
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return [
|
||||
'isInstallable',
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Interceptor classes are pretty straight forward. With the `CLASS_ALIAS` you can give objects with that class a short name for KQL queries. The `$toArray` property lists all methods that should be rendered if you don't run a subquery. I.e. in this case `kirby.system` would render an array with the `isInstallable` value.
|
||||
|
||||
The `allowedMethods` method must return an array of all methods that can be access for this object. In addition to that you can also create your own custom methods in an interceptor that will then become available in KQL.
|
||||
|
||||
```php
|
||||
class SystemInterceptor extends Kirby\Kql\Interceptors\Interceptor
|
||||
{
|
||||
...
|
||||
|
||||
public function isReady()
|
||||
{
|
||||
return 'yes it is!';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This custom method can now be used with `kirby.system.isReady` in KQL and will return `yes it is!`
|
||||
|
||||
### Unintercepted Classes
|
||||
|
||||
If you want to fully allow access to an entire class without putting an interceptor in between, you can add the class to the allow list in your config:
|
||||
|
||||
```php
|
||||
return [
|
||||
'kql' => [
|
||||
'classes' => [
|
||||
'allowed' => [
|
||||
'Kirby\Cms\System'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
This will introduce full access to all public class methods. This can be very risky though and you should avoid this if possible.
|
||||
|
||||
### No Mutations
|
||||
|
||||
KQL only offers access to data in your site. It does not support any mutations. All destructive methods are blocked and cannot be accessed in queries.
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE) License © 2020-2023 [Bastian Allgeier](https://getkirby.com)
|
||||
73
public/site/plugins/kql/composer.json
Executable file
73
public/site/plugins/kql/composer.json
Executable file
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"name": "getkirby/kql",
|
||||
"description": "Kirby Query Language",
|
||||
"license": "MIT",
|
||||
"type": "kirby-plugin",
|
||||
"version": "2.1.0",
|
||||
"keywords": [
|
||||
"kirby",
|
||||
"cms",
|
||||
"api",
|
||||
"json",
|
||||
"query",
|
||||
"headless"
|
||||
],
|
||||
"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"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Kirby\\": [
|
||||
"tests/"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
8
public/site/plugins/kql/extensions/aliases.php
Normal file
8
public/site/plugins/kql/extensions/aliases.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
class_alias(\Kirby\Kql\Kql::class, 'Kql');
|
||||
class_alias(\Kirby\Kql\Interceptor::class, 'Kirby\Kql\Interceptors\Interceptor');
|
||||
|
||||
// Provide backwards compatibility for Kirby 3 core classes
|
||||
class_alias(\Kirby\Kql\Interceptors\Content\Content::class, 'Kirby\Kql\Interceptors\Cms\Content');
|
||||
class_alias(\Kirby\Kql\Interceptors\Content\Field::class, 'Kirby\Kql\Interceptors\Cms\Field');
|
||||
25
public/site/plugins/kql/extensions/api.php
Normal file
25
public/site/plugins/kql/extensions/api.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Kql\Kql;
|
||||
|
||||
return [
|
||||
'routes' => 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',
|
||||
];
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
];
|
||||
21
public/site/plugins/kql/extensions/autoload.php
Normal file
21
public/site/plugins/kql/extensions/autoload.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql;
|
||||
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
function autoload(string $namespace, string $dir)
|
||||
{
|
||||
spl_autoload_register(function ($class) use ($namespace, $dir) {
|
||||
if (str_contains($class, '.') === true || str_starts_with($class, $namespace) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = Str::after($class, $namespace);
|
||||
$path = $dir . '/' . str_replace('\\', '/', $path) . '.php';
|
||||
|
||||
if (is_file($path) === true) {
|
||||
include $path;
|
||||
}
|
||||
});
|
||||
}
|
||||
11
public/site/plugins/kql/extensions/helpers.php
Normal file
11
public/site/plugins/kql/extensions/helpers.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\Helpers;
|
||||
use Kirby\Kql\Kql;
|
||||
|
||||
if (Helpers::hasOverride('kql') === false) {
|
||||
function kql($input, $model = null)
|
||||
{
|
||||
return Kql::run($input, $model);
|
||||
}
|
||||
}
|
||||
16
public/site/plugins/kql/index.php
Normal file
16
public/site/plugins/kql/index.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql;
|
||||
|
||||
use Kirby\Cms\App;
|
||||
|
||||
require_once __DIR__ . '/extensions/autoload.php';
|
||||
|
||||
autoload('Kirby\\', __DIR__ . '/src/');
|
||||
|
||||
require_once __DIR__ . '/extensions/aliases.php';
|
||||
require_once __DIR__ . '/extensions/helpers.php';
|
||||
|
||||
App::plugin('getkirby/kql', [
|
||||
'api' => require_once 'extensions/api.php'
|
||||
]);
|
||||
152
public/site/plugins/kql/src/Kql/Help.php
Normal file
152
public/site/plugins/kql/src/Kql/Help.php
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
|
||||
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 <bastian@getkirby.com>
|
||||
* @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
|
||||
{
|
||||
if (is_array($value) === true) {
|
||||
return static::forArray($value);
|
||||
}
|
||||
|
||||
if (is_object($value) === true) {
|
||||
return static::forObject($value);
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => gettype($value),
|
||||
'value' => $value
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function forArray(array $array): array
|
||||
{
|
||||
return [
|
||||
'type' => 'array',
|
||||
'keys' => array_keys($array),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers information for method about
|
||||
* name, parameters, return type etc.
|
||||
* @internal
|
||||
*/
|
||||
public static function forMethod(object $object, string $method): array
|
||||
{
|
||||
$reflection = new ReflectionMethod($object, $method);
|
||||
$returns = $reflection->getReturnType()?->getName();
|
||||
$params = [];
|
||||
|
||||
foreach ($reflection->getParameters() as $param) {
|
||||
$name = $param->getName();
|
||||
$required = $param->isOptional() === false;
|
||||
$type = $param->hasType() ? $param->getType()->getName() : null;
|
||||
$default = null;
|
||||
|
||||
if ($param->isDefaultValueAvailable()) {
|
||||
$default = $param->getDefaultValue();
|
||||
}
|
||||
|
||||
$call = '';
|
||||
|
||||
if ($type !== null) {
|
||||
$call = $type . ' ';
|
||||
}
|
||||
|
||||
$call .= '$' . $name;
|
||||
|
||||
if ($required === false && $default !== null) {
|
||||
$call .= ' = ' . var_export($default, true);
|
||||
}
|
||||
|
||||
$p['call'] = $call;
|
||||
|
||||
$params[$name] = compact('name', 'type', 'required', 'default', 'call');
|
||||
}
|
||||
|
||||
$call = '.' . $method;
|
||||
|
||||
if (empty($params) === false) {
|
||||
$call .= '(' . implode(', ', array_column($params, 'call')) . ')';
|
||||
}
|
||||
|
||||
return [
|
||||
'call' => $call,
|
||||
'name' => $method,
|
||||
'params' => $params,
|
||||
'returns' => $returns
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers informations for each unique method
|
||||
* @internal
|
||||
*/
|
||||
public static function forMethods(object $object, array $methods): array
|
||||
{
|
||||
$methods = array_unique($methods);
|
||||
$reflection = [];
|
||||
|
||||
sort($methods);
|
||||
|
||||
foreach ($methods as $methodName) {
|
||||
if (method_exists($object, $methodName) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reflection[$methodName] = static::forMethod($object, $methodName);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
// 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 = A::map(
|
||||
$class->getMethods(),
|
||||
fn ($method) => static::forMethod($object, $method->getName())
|
||||
);
|
||||
|
||||
return [
|
||||
'type' => $class->getName(),
|
||||
'methods' => $methods
|
||||
];
|
||||
}
|
||||
}
|
||||
295
public/site/plugins/kql/src/Kql/Interceptor.php
Normal file
295
public/site/plugins/kql/src/Kql/Interceptor.php
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
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 <bastian@getkirby.com>
|
||||
* @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)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
// check in the block list from the config
|
||||
if (in_array($name, $blocked) === true) {
|
||||
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);
|
||||
// load an interceptor from config if it exists and otherwise fall back to a built-in interceptor
|
||||
$interceptor = $interceptors[$name] ?? static::class($class);
|
||||
|
||||
// check for a valid interceptor class
|
||||
if ($class !== $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);
|
||||
|
||||
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);
|
||||
|
||||
// return the plain object if it is allowed
|
||||
if (in_array($name, $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();
|
||||
}
|
||||
}
|
||||
39
public/site/plugins/kql/src/Kql/Interceptors/Cms/App.php
Normal file
39
public/site/plugins/kql/src/Kql/Interceptors/Cms/App.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
use Kirby\Kql\Interceptor;
|
||||
|
||||
class App extends Interceptor
|
||||
{
|
||||
public const CLASS_ALIAS = 'kirby';
|
||||
|
||||
protected $toArray = [
|
||||
'site',
|
||||
'url'
|
||||
];
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return [
|
||||
'collection',
|
||||
'defaultLanguage',
|
||||
'detectedLanguage',
|
||||
'draft',
|
||||
'file',
|
||||
'language',
|
||||
'languageCode',
|
||||
'languages',
|
||||
'multilang',
|
||||
'page',
|
||||
'roles',
|
||||
'site',
|
||||
'translation',
|
||||
'translations',
|
||||
'url',
|
||||
'user',
|
||||
'users',
|
||||
'version'
|
||||
];
|
||||
}
|
||||
}
|
||||
34
public/site/plugins/kql/src/Kql/Interceptors/Cms/Block.php
Executable file
34
public/site/plugins/kql/src/Kql/Interceptors/Cms/Block.php
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class Block extends Model
|
||||
{
|
||||
public const CLASS_ALIAS = 'block';
|
||||
|
||||
protected $toArray = [
|
||||
'content',
|
||||
'id',
|
||||
'isEmpty',
|
||||
'isHidden',
|
||||
'type'
|
||||
];
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return array_merge(
|
||||
$this->allowedMethodsForSiblings(),
|
||||
[
|
||||
'content',
|
||||
'id',
|
||||
'isEmpty',
|
||||
'isHidden',
|
||||
'isNotEmpty',
|
||||
'toField',
|
||||
'toHtml',
|
||||
'parent',
|
||||
'type'
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
24
public/site/plugins/kql/src/Kql/Interceptors/Cms/Blocks.php
Executable file
24
public/site/plugins/kql/src/Kql/Interceptors/Cms/Blocks.php
Executable file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class Blocks extends Collection
|
||||
{
|
||||
public const CLASS_ALIAS = 'blocks';
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return array_merge(
|
||||
parent::allowedMethods(),
|
||||
[
|
||||
'excerpt',
|
||||
'toHtml'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->object->toArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
use Kirby\Kql\Interceptor;
|
||||
|
||||
class Blueprint extends Interceptor
|
||||
{
|
||||
public const CLASS_ALIAS = 'blueprint';
|
||||
|
||||
protected $toArray = [
|
||||
'description',
|
||||
'fields',
|
||||
'isDefault',
|
||||
'name',
|
||||
'sections',
|
||||
'options',
|
||||
'tabs',
|
||||
'title',
|
||||
];
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return [
|
||||
'description',
|
||||
'field',
|
||||
'fields',
|
||||
'isDefault',
|
||||
'name',
|
||||
'options',
|
||||
'section',
|
||||
'sections',
|
||||
'tab',
|
||||
'tabs',
|
||||
'title',
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return $this->object->fields();
|
||||
}
|
||||
|
||||
public function sections(): array
|
||||
{
|
||||
return array_keys($this->object->sections());
|
||||
}
|
||||
|
||||
public function tab(string $name): ?array
|
||||
{
|
||||
if ($tab = $this->object->tab($name)) {
|
||||
foreach ($tab['columns'] as $columnIndex => $column) {
|
||||
$tab['columns'][$columnIndex]['sections'] = array_keys($column['sections']);
|
||||
}
|
||||
|
||||
return $tab;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function tabs(): array
|
||||
{
|
||||
$tabs = [];
|
||||
|
||||
foreach ($this->object->tabs() as $tab) {
|
||||
$tabs[$tab['name']] = $this->tab($tab['name']);
|
||||
}
|
||||
|
||||
return $tabs;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
use Kirby\Kql\Interceptor;
|
||||
|
||||
class Collection extends Interceptor
|
||||
{
|
||||
public const CLASS_ALIAS = 'collection';
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return [
|
||||
'chunk',
|
||||
'count',
|
||||
'filterBy',
|
||||
'find',
|
||||
'findBy',
|
||||
'findByKey',
|
||||
'first',
|
||||
'flip',
|
||||
'groupBy',
|
||||
'has',
|
||||
'isEmpty',
|
||||
'isEven',
|
||||
'isNotEmpty',
|
||||
'isOdd',
|
||||
'keys',
|
||||
'last',
|
||||
'limit',
|
||||
'next',
|
||||
'not',
|
||||
'nth',
|
||||
'offset',
|
||||
'pagination',
|
||||
'pluck',
|
||||
'prev',
|
||||
'shuffle',
|
||||
'slice',
|
||||
'sortBy',
|
||||
'without',
|
||||
];
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->object->keys();
|
||||
}
|
||||
}
|
||||
71
public/site/plugins/kql/src/Kql/Interceptors/Cms/File.php
Normal file
71
public/site/plugins/kql/src/Kql/Interceptors/Cms/File.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class File extends Model
|
||||
{
|
||||
public const CLASS_ALIAS = 'file';
|
||||
|
||||
protected $toArray = [
|
||||
'extension',
|
||||
'filename',
|
||||
'height',
|
||||
'id',
|
||||
'mime',
|
||||
'niceSize',
|
||||
'template',
|
||||
'type',
|
||||
'url',
|
||||
'width'
|
||||
];
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return array_merge(
|
||||
$this->allowedMethodsForModels(),
|
||||
$this->allowedMethodsForParents(),
|
||||
$this->allowedMethodsForSiblings(),
|
||||
[
|
||||
'blur',
|
||||
'bw',
|
||||
'crop',
|
||||
'dataUri',
|
||||
'dimensions',
|
||||
'exif',
|
||||
'extension',
|
||||
'filename',
|
||||
'files',
|
||||
'grayscale',
|
||||
'greyscale',
|
||||
'height',
|
||||
'html',
|
||||
'isPortrait',
|
||||
'isLandscape',
|
||||
'isSquare',
|
||||
'mime',
|
||||
'name',
|
||||
'niceSize',
|
||||
'orientation',
|
||||
'ratio',
|
||||
'resize',
|
||||
'size',
|
||||
'srcset',
|
||||
'template',
|
||||
'templateSiblings',
|
||||
'thumb',
|
||||
'type',
|
||||
'width'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function dimensions(): array
|
||||
{
|
||||
return $this->object->dimensions()->toArray();
|
||||
}
|
||||
|
||||
public function exif(): array
|
||||
{
|
||||
return $this->object->exif()->toArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class FileVersion extends File
|
||||
{
|
||||
public const CLASS_ALIAS = 'file';
|
||||
}
|
||||
18
public/site/plugins/kql/src/Kql/Interceptors/Cms/Files.php
Normal file
18
public/site/plugins/kql/src/Kql/Interceptors/Cms/Files.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class Files extends Collection
|
||||
{
|
||||
public const CLASS_ALIAS = 'files';
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return array_merge(
|
||||
parent::allowedMethods(),
|
||||
[
|
||||
'template'
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
30
public/site/plugins/kql/src/Kql/Interceptors/Cms/Layout.php
Executable file
30
public/site/plugins/kql/src/Kql/Interceptors/Cms/Layout.php
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class Layout extends Model
|
||||
{
|
||||
public const CLASS_ALIAS = 'layout';
|
||||
|
||||
protected $toArray = [
|
||||
'attrs',
|
||||
'columns',
|
||||
'id',
|
||||
'isEmpty',
|
||||
];
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return array_merge(
|
||||
$this->allowedMethodsForSiblings(),
|
||||
[
|
||||
'attrs',
|
||||
'columns',
|
||||
'id',
|
||||
'isEmpty',
|
||||
'isNotEmpty',
|
||||
'parent'
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
30
public/site/plugins/kql/src/Kql/Interceptors/Cms/LayoutColumn.php
Executable file
30
public/site/plugins/kql/src/Kql/Interceptors/Cms/LayoutColumn.php
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class LayoutColumn extends Model
|
||||
{
|
||||
public const CLASS_ALIAS = 'layoutColumn';
|
||||
|
||||
protected $toArray = [
|
||||
'blocks',
|
||||
'id',
|
||||
'isEmpty',
|
||||
'width',
|
||||
];
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return array_merge(
|
||||
$this->allowedMethodsForSiblings(),
|
||||
[
|
||||
'blocks',
|
||||
'id',
|
||||
'isEmpty',
|
||||
'isNotEmpty',
|
||||
'span',
|
||||
'width'
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
13
public/site/plugins/kql/src/Kql/Interceptors/Cms/LayoutColumns.php
Executable file
13
public/site/plugins/kql/src/Kql/Interceptors/Cms/LayoutColumns.php
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class LayoutColumns extends Collection
|
||||
{
|
||||
public const CLASS_ALIAS = 'layoutColumns';
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->object->toArray();
|
||||
}
|
||||
}
|
||||
13
public/site/plugins/kql/src/Kql/Interceptors/Cms/Layouts.php
Executable file
13
public/site/plugins/kql/src/Kql/Interceptors/Cms/Layouts.php
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class Layouts extends Collection
|
||||
{
|
||||
public const CLASS_ALIAS = 'layouts';
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->object->toArray();
|
||||
}
|
||||
}
|
||||
113
public/site/plugins/kql/src/Kql/Interceptors/Cms/Model.php
Normal file
113
public/site/plugins/kql/src/Kql/Interceptors/Cms/Model.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
use Kirby\Kql\Interceptor;
|
||||
|
||||
class Model extends Interceptor
|
||||
{
|
||||
public const CLASS_ALIAS = 'model';
|
||||
|
||||
public function __call($method, array $args = [])
|
||||
{
|
||||
if ($this->isAllowedMethod($method) === true) {
|
||||
return $this->object->$method(...$args);
|
||||
}
|
||||
|
||||
if (method_exists($this->object, $method) === false) {
|
||||
return $this->object->content()->get($method);
|
||||
}
|
||||
|
||||
$this->forbiddenMethod($method);
|
||||
}
|
||||
|
||||
protected function allowedMethodsForChildren()
|
||||
{
|
||||
return [
|
||||
'children',
|
||||
'childrenAndDrafts',
|
||||
'draft',
|
||||
'drafts',
|
||||
'find',
|
||||
'findPageOrDraft',
|
||||
'grandChildren',
|
||||
'hasChildren',
|
||||
'hasDrafts',
|
||||
'hasListedChildren',
|
||||
'hasUnlistedChildren',
|
||||
'index',
|
||||
'search',
|
||||
];
|
||||
}
|
||||
|
||||
protected function allowedMethodsForFiles()
|
||||
{
|
||||
return [
|
||||
'audio',
|
||||
'code',
|
||||
'documents',
|
||||
'file',
|
||||
'files',
|
||||
'hasAudio',
|
||||
'hasCode',
|
||||
'hasDocuments',
|
||||
'hasFiles',
|
||||
'hasImages',
|
||||
'hasVideos',
|
||||
'image',
|
||||
'images',
|
||||
'videos'
|
||||
];
|
||||
}
|
||||
|
||||
protected function allowedMethodsForModels()
|
||||
{
|
||||
return [
|
||||
'apiUrl',
|
||||
'blueprint',
|
||||
'content',
|
||||
'dragText',
|
||||
'exists',
|
||||
'id',
|
||||
'mediaUrl',
|
||||
'modified',
|
||||
'permissions',
|
||||
'panel',
|
||||
'permalink',
|
||||
'previewUrl',
|
||||
'url',
|
||||
];
|
||||
}
|
||||
|
||||
protected function allowedMethodsForSiblings()
|
||||
{
|
||||
return [
|
||||
'indexOf',
|
||||
'next',
|
||||
'nextAll',
|
||||
'prev',
|
||||
'prevAll',
|
||||
'siblings',
|
||||
'hasNext',
|
||||
'hasPrev',
|
||||
'isFirst',
|
||||
'isLast',
|
||||
'isNth'
|
||||
];
|
||||
}
|
||||
|
||||
protected function allowedMethodsForParents()
|
||||
{
|
||||
return [
|
||||
'parent',
|
||||
'parentId',
|
||||
'parentModel',
|
||||
'site',
|
||||
];
|
||||
}
|
||||
|
||||
public function uuid(): string
|
||||
{
|
||||
return $this->object->uuid()->toString();
|
||||
}
|
||||
}
|
||||
68
public/site/plugins/kql/src/Kql/Interceptors/Cms/Page.php
Normal file
68
public/site/plugins/kql/src/Kql/Interceptors/Cms/Page.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class Page extends Model
|
||||
{
|
||||
public const CLASS_ALIAS = 'page';
|
||||
|
||||
protected $toArray = [
|
||||
'children',
|
||||
'content',
|
||||
'drafts',
|
||||
'files',
|
||||
'id',
|
||||
'intendedTemplate',
|
||||
'isHomePage',
|
||||
'isErrorPage',
|
||||
'num',
|
||||
'template',
|
||||
'title',
|
||||
'slug',
|
||||
'status',
|
||||
'uid',
|
||||
'url'
|
||||
];
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return array_merge(
|
||||
$this->allowedMethodsForChildren(),
|
||||
$this->allowedMethodsForFiles(),
|
||||
$this->allowedMethodsForModels(),
|
||||
$this->allowedMethodsForParents(),
|
||||
$this->allowedMethodsForSiblings(),
|
||||
[
|
||||
'blueprints',
|
||||
'depth',
|
||||
'hasTemplate',
|
||||
'intendedTemplate',
|
||||
'isDraft',
|
||||
'isErrorPage',
|
||||
'isHomePage',
|
||||
'isHomeOrErrorPage',
|
||||
'isListed',
|
||||
'isReadable',
|
||||
'isSortable',
|
||||
'isUnlisted',
|
||||
'num',
|
||||
'slug',
|
||||
'status',
|
||||
'template',
|
||||
'title',
|
||||
'uid',
|
||||
'uri',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function intendedTemplate(): string
|
||||
{
|
||||
return $this->object->intendedTemplate()->name();
|
||||
}
|
||||
|
||||
public function template(): string
|
||||
{
|
||||
return $this->object->template()->name();
|
||||
}
|
||||
}
|
||||
34
public/site/plugins/kql/src/Kql/Interceptors/Cms/Pages.php
Normal file
34
public/site/plugins/kql/src/Kql/Interceptors/Cms/Pages.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class Pages extends Collection
|
||||
{
|
||||
public const CLASS_ALIAS = 'pages';
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return array_merge(
|
||||
parent::allowedMethods(),
|
||||
[
|
||||
'audio',
|
||||
'children',
|
||||
'code',
|
||||
'documents',
|
||||
'drafts',
|
||||
'files',
|
||||
'findByUri',
|
||||
'images',
|
||||
'index',
|
||||
'listed',
|
||||
'notTemplate',
|
||||
'nums',
|
||||
'published',
|
||||
'search',
|
||||
'template',
|
||||
'unlisted',
|
||||
'videos',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
33
public/site/plugins/kql/src/Kql/Interceptors/Cms/Role.php
Normal file
33
public/site/plugins/kql/src/Kql/Interceptors/Cms/Role.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
use Kirby\Kql\Interceptor;
|
||||
|
||||
class Role extends Interceptor
|
||||
{
|
||||
public const CLASS_ALIAS = 'role';
|
||||
|
||||
protected $toArray = [
|
||||
'description',
|
||||
'id',
|
||||
'name',
|
||||
'title',
|
||||
];
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return [
|
||||
'description',
|
||||
'id',
|
||||
'name',
|
||||
'permissions',
|
||||
'title'
|
||||
];
|
||||
}
|
||||
|
||||
public function permissions(): array
|
||||
{
|
||||
return $this->object->permissions()->toArray();
|
||||
}
|
||||
}
|
||||
36
public/site/plugins/kql/src/Kql/Interceptors/Cms/Site.php
Normal file
36
public/site/plugins/kql/src/Kql/Interceptors/Cms/Site.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class Site extends Model
|
||||
{
|
||||
public const CLASS_ALIAS = 'site';
|
||||
|
||||
protected $toArray = [
|
||||
'children',
|
||||
'drafts',
|
||||
'files',
|
||||
'title',
|
||||
'url',
|
||||
];
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return array_merge(
|
||||
$this->allowedMethodsForChildren(),
|
||||
$this->allowedMethodsForFiles(),
|
||||
$this->allowedMethodsForModels(),
|
||||
[
|
||||
'blueprints',
|
||||
'breadcrumb',
|
||||
'errorPage',
|
||||
'errorPageId',
|
||||
'homePage',
|
||||
'homePageId',
|
||||
'page',
|
||||
'pages',
|
||||
'title',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class Structure extends Collection
|
||||
{
|
||||
public const CLASS_ALIAS = 'structure';
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->object->toArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class StructureObject extends Model
|
||||
{
|
||||
public const CLASS_ALIAS = 'structureItem';
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return array_merge(
|
||||
$this->allowedMethodsForSiblings(),
|
||||
[
|
||||
'content',
|
||||
'id',
|
||||
'parent',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
35
public/site/plugins/kql/src/Kql/Interceptors/Cms/Translation.php
Executable file
35
public/site/plugins/kql/src/Kql/Interceptors/Cms/Translation.php
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
use Kirby\Kql\Interceptor;
|
||||
|
||||
class Translation extends Interceptor
|
||||
{
|
||||
public const CLASS_ALIAS = 'translation';
|
||||
|
||||
protected $toArray = [
|
||||
'code',
|
||||
'data',
|
||||
'direction',
|
||||
'id',
|
||||
'name',
|
||||
'locale',
|
||||
'author'
|
||||
];
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return [
|
||||
'code',
|
||||
'data',
|
||||
'dataWithFallback',
|
||||
'direction',
|
||||
'get',
|
||||
'id',
|
||||
'name',
|
||||
'locale',
|
||||
'author'
|
||||
];
|
||||
}
|
||||
}
|
||||
35
public/site/plugins/kql/src/Kql/Interceptors/Cms/User.php
Normal file
35
public/site/plugins/kql/src/Kql/Interceptors/Cms/User.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class User extends Model
|
||||
{
|
||||
public const CLASS_ALIAS = 'user';
|
||||
|
||||
protected $toArray = [
|
||||
'id',
|
||||
'name',
|
||||
'role',
|
||||
'username'
|
||||
];
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return array_merge(
|
||||
$this->allowedMethodsForFiles(),
|
||||
$this->allowedMethodsForModels(),
|
||||
$this->allowedMethodsForSiblings(),
|
||||
[
|
||||
'avatar',
|
||||
'email',
|
||||
'id',
|
||||
'isAdmin',
|
||||
'language',
|
||||
'modified',
|
||||
'name',
|
||||
'role',
|
||||
'username',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
18
public/site/plugins/kql/src/Kql/Interceptors/Cms/Users.php
Normal file
18
public/site/plugins/kql/src/Kql/Interceptors/Cms/Users.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Cms;
|
||||
|
||||
class Users extends Collection
|
||||
{
|
||||
public const CLASS_ALIAS = 'users';
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return array_merge(
|
||||
parent::allowedMethods(),
|
||||
[
|
||||
'role'
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Content;
|
||||
|
||||
use Kirby\Kql\Interceptor;
|
||||
|
||||
class Content extends Interceptor
|
||||
{
|
||||
public const CLASS_ALIAS = 'content';
|
||||
|
||||
public function __call($method, array $args = [])
|
||||
{
|
||||
if ($this->isAllowedMethod($method) === true) {
|
||||
return $this->object->$method(...$args);
|
||||
}
|
||||
|
||||
if (method_exists($this->object, $method) === false) {
|
||||
return $this->object->get($method);
|
||||
}
|
||||
|
||||
$this->forbiddenMethod($method);
|
||||
}
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return [
|
||||
'data',
|
||||
'fields',
|
||||
'has',
|
||||
'get',
|
||||
'keys',
|
||||
'not',
|
||||
];
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->object->toArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Content;
|
||||
|
||||
use Kirby\Kql\Interceptor;
|
||||
|
||||
class Field extends Interceptor
|
||||
{
|
||||
public const CLASS_ALIAS = 'field';
|
||||
|
||||
public function __call($method, array $args = [])
|
||||
{
|
||||
if ($this->isAllowedMethod($method) === true) {
|
||||
return $this->object->$method(...$args);
|
||||
}
|
||||
|
||||
// field methods
|
||||
$methods = array_keys($this->object::$methods);
|
||||
$method = strtolower($method);
|
||||
|
||||
if (in_array($method, $methods) === true) {
|
||||
return $this->object->$method(...$args);
|
||||
}
|
||||
|
||||
// aliases
|
||||
$aliases = array_keys($this->object::$aliases);
|
||||
$alias = strtolower($method);
|
||||
|
||||
if (in_array($alias, $aliases) === true) {
|
||||
return $this->object->$method(...$args);
|
||||
}
|
||||
|
||||
$this->forbiddenMethod($method);
|
||||
}
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return [
|
||||
'exists',
|
||||
'isEmpty',
|
||||
'isNotEmpty',
|
||||
'key',
|
||||
'or',
|
||||
'value'
|
||||
];
|
||||
}
|
||||
|
||||
public function toResponse()
|
||||
{
|
||||
return $this->object->toString();
|
||||
}
|
||||
}
|
||||
30
public/site/plugins/kql/src/Kql/Interceptors/Panel/Model.php
Executable file
30
public/site/plugins/kql/src/Kql/Interceptors/Panel/Model.php
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Panel;
|
||||
|
||||
use Kirby\Kql\Interceptor;
|
||||
|
||||
class Model extends Interceptor
|
||||
{
|
||||
public const CLASS_ALIAS = 'panel';
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return [
|
||||
'dragText',
|
||||
'image',
|
||||
'path',
|
||||
'url',
|
||||
];
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'dragText' => $this->dragText(),
|
||||
'image' => $this->image(),
|
||||
'path' => $this->path(),
|
||||
'url' => $this->url(),
|
||||
];
|
||||
}
|
||||
}
|
||||
24
public/site/plugins/kql/src/Kql/Interceptors/Toolkit/Obj.php
Executable file
24
public/site/plugins/kql/src/Kql/Interceptors/Toolkit/Obj.php
Executable file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql\Interceptors\Toolkit;
|
||||
|
||||
use Kirby\Kql\Interceptor;
|
||||
|
||||
class Obj extends Interceptor
|
||||
{
|
||||
public const CLASS_ALIAS = 'obj';
|
||||
|
||||
public function allowedMethods(): array
|
||||
{
|
||||
return [
|
||||
'get',
|
||||
'toArray',
|
||||
'toJson',
|
||||
];
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->object->toArray();
|
||||
}
|
||||
}
|
||||
226
public/site/plugins/kql/src/Kql/Kql.php
Normal file
226
public/site/plugins/kql/src/Kql/Kql.php
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Cms\Collection;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* ...
|
||||
*
|
||||
* @package Kirby KQL
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @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
|
||||
{
|
||||
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
|
||||
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,
|
||||
array|string|null $select = null,
|
||||
array $options = []
|
||||
) {
|
||||
if ($select === null) {
|
||||
return static::render($data);
|
||||
}
|
||||
|
||||
if ($select === '?') {
|
||||
return static::help($data);
|
||||
}
|
||||
|
||||
if ($data instanceof Collection) {
|
||||
return static::selectFromCollection($data, $select, $options);
|
||||
}
|
||||
|
||||
if (is_object($data) === true) {
|
||||
return static::selectFromObject($data, $select);
|
||||
}
|
||||
|
||||
if (is_array($data) === true) {
|
||||
return static::selectFromArray($data, $select);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function selectFromArray(array $array, array $select): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($select as $key => $selection) {
|
||||
if ($selection === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_int($key) === true) {
|
||||
$key = $selection;
|
||||
$selection = true;
|
||||
}
|
||||
|
||||
$result[$key] = $array[$key] ?? null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function selectFromCollection(
|
||||
Collection $collection,
|
||||
array|string $select,
|
||||
array $options = []
|
||||
): array {
|
||||
if ($options['pagination'] ?? false) {
|
||||
$collection = $collection->paginate($options['pagination']);
|
||||
}
|
||||
|
||||
$data = [];
|
||||
|
||||
foreach ($collection as $model) {
|
||||
$data[] = static::selectFromObject($model, $select);
|
||||
}
|
||||
|
||||
if ($pagination = $collection->pagination()) {
|
||||
return [
|
||||
'data' => $data,
|
||||
'pagination' => [
|
||||
'page' => $pagination->page(),
|
||||
'pages' => $pagination->pages(),
|
||||
'offset' => $pagination->offset(),
|
||||
'limit' => $pagination->limit(),
|
||||
'total' => $pagination->total(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function selectFromObject(
|
||||
object $object,
|
||||
array|string $select
|
||||
): array {
|
||||
// replace actual object with intercepting proxy class
|
||||
$object = Interceptor::replace($object);
|
||||
$result = [];
|
||||
|
||||
if (is_string($select) === true) {
|
||||
$select = Str::split($select);
|
||||
}
|
||||
|
||||
foreach ($select as $key => $selection) {
|
||||
if ($selection === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_int($key) === true) {
|
||||
$key = $selection;
|
||||
$selection = true;
|
||||
}
|
||||
|
||||
$result[$key] = static::fetch($object, $key, $selection);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
29
public/site/plugins/kql/src/Kql/Query.php
Normal file
29
public/site/plugins/kql/src/Kql/Query.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Kql;
|
||||
|
||||
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 <nico@getkirby.com>
|
||||
* @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
|
||||
{
|
||||
return is_object($result) ? Interceptor::replace($result): $result;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue