Upgrade to 4.0.0

This commit is contained in:
Bastian Allgeier
2023-11-28 09:33:56 +01:00
parent f96b96af76
commit 3b0b6546ca
480 changed files with 21371 additions and 13327 deletions

View File

@@ -17,6 +17,10 @@ trim_trailing_whitespace = true
indent_size = 4
insert_final_newline = true
[*.vue.php]
indent_size = 2
insert_final_newline = false
[*.yml]
indent_style = space

View File

@@ -1,7 +1,7 @@
# Kirby License Agreement
Published: March 21, 2023
Source: https://getkirby.com/license/2023-03-21
Published: November 28, 2023
Source: https://getkirby.com/license/2023-11-28
## About this Agreement
@@ -11,44 +11,79 @@ This End User License Agreement (the **"Agreement"**) is fundamental to the rela
If you do not agree to this Agreement, please do not download, install or use Kirby. Installation or use of Kirby signifies that you have read, understood, and agreed to be bound by this Agreement.
## Summary
This section summarizes the most important conditions of this Agreement to give you a quick overview:
- With your purchase you obtain a license. A license allows you to use Kirby according to this Agreement.
- Each project (defined by its URL) needs its own license. You need to purchase the right license for your project and/or client. You can find our license variants on <https://getkirby.com/buy>. In some explicitly listed cases, you can use Kirby without having to purchase a license.
- Each license includes any Kirby version that gets released within three years from the date when you first activated your license. We also provide free security updates for older versions that may protect your project beyond three years.
- After those three years, you can continue to use Kirby for your project with any of these versions as long as you want.
- To use any newer version released after this time, you will need to upgrade your license.
- Upgrading your license extends the timeframe for an additional three years during which you can use new releases. You can perform the upgrade at any time.
- You have the right to transfer or reassign a license to another person or project if needed.
- There are some restrictions for use of Kirby that you can find below.
For the full license details, please read the Agreement in full. Only the following sections are legally binding.
## Definitions
Before we get started with the conditions of the Agreement, let's define the terms that will be used throughout it:
- When we refer to **"You"**, we mean the licensee. Before purchasing Kirby, that's the individual or company that has downloaded and/or installed Kirby for a Development Installation, Private Installation or Extension Demo. When used for a Public Site, the licensee is the individual or company that has purchased the Kirby license. If you work on a client project and have purchased the Kirby license for your client, you (and _not_ the client) are the licensee.
- When we refer to **"We"**/**"Us"**/**"Our"**, we mean the licensor, the Content Folder GmbH & Co. KG. You can find Our company and contact information on Our [contact page](https://getkirby.com/contact).
- When we refer to **"You"**, we mean the licensee. Before purchasing Kirby, that's the individual or company that has downloaded and/or installed Kirby for a Development Installation, Private Installation or Extension Demo. When used for a Public Site, the licensee is the individual or company that has purchased the Kirby license. If you work on a client project and have purchased the Kirby license for your client, you (and _not_ the client) are the licensee.
- When we refer to **"We"**/**"Us"**/**"Our"**, we mean the licensor, the Content Folder GmbH & Co. KG. You can find Our company and contact information on Our [contact page](https://getkirby.com/contact).
- **"Client"** is the individual or company on whose behalf You create or work on a Website. This only applies if the client is not the licensee.
- A **"Website"** is a single Kirby project that is defined by its domain name and root directory (e.g. `https://sub.example.com` or `https://example.com/example/`). Each (sub)domain and root directory is a separate Website, even if the projects are related in any way. Exception: If You use the cross-domain multi-language feature with the same `content` folder, these domains count as the same Website.
You may use Kirby as a headless backend or as a static site generator. In these cases the Website is defined by the domain and root directory of the user- or visitor-facing frontend(s).
- A **"Development Installation"** is a Website that is installed purely for the purposes of development and client preview. It must only be accessible by a restricted number of users (like on a personal computer, on a server in a network with restricted access or when protecting a staging website with a password that only a restricted number of users know).
- A **"Private Installation"** is a Website that is installed purely for personal use. It must only be accessible by You and Your family.
- An **"Extension Demo"** is a Website with the single purpose to showcase a free or commercial Kirby theme or Kirby plugin, as long as that Website only contains demo content. If the showcased extension is a Kirby theme, the demo content must be exactly as shipped with the theme. Demos for Kirby plugins may _not_ contain any additional content that is not needed to showcase the plugin in use.
- A **"Public Site"** is a Website that is _neither_ a Development Installation, a Private Installation nor an Extension Demo.
- An **"Update"** is defined as a Kirby release which adds smaller new features, minor functionality enhancements or bug fixes. This class of release is identified by the change of the revision to the right of the first decimal point, e.g. 3.1 to 3.2, 3.X.1 to 3.X.2 or 3.X.X.1 to 3.X.X.2.
- An **"Upgrade"** is a major Kirby release which incorporates major new features or enhancements that increase the core functionality of Kirby to a larger extent. This class of release is identified by the change of the revision to the left of the first decimal point, e.g. 3.X to 4.0.
- A **"Minor Release"** is a stable Kirby release which adds smaller new features, minor functionality enhancements or bug fixes. This class of release is identified by the change of the revision to the right of the first decimal point, e.g. 4.1 to 4.2, 4.X.1 to 4.X.2.
- A **"Major Release"** is a stable Kirby release which incorporates major new features or enhancements that increase or change the core functionality of Kirby to a larger extent. It may also deprecate existing parts of the Source Code or change them in a breaking way. This class of release is identified by the change of the revision to the left of the first decimal point, e.g. 4.X to 5.0.
- A **"Major Generation"** is defined as all releases that share the revision to the left of the first decimal point, e.g. 4.0.0, 4.0.X, 4.X.0 and 4.X.X.
- The **"Source Code"** is defined as the contents of all files that are provided with Kirby and that make Kirby work. This includes (but is not limited to) all PHP, JavaScript, JSON, HTML and CSS files as well as all related image and other media files.
- The **"Activation Date"** determines the included updates. It is defined like this:
- For a newly purchased license, it is the date when the license was first activated for use with a Public Site.
- When You upgrade an already activated license, it is the date on which the upgrade was performed in Our license hub. If the license is still within the Included Updates Period, the Activation Date of the upgrade license will be set to the end of the Included Updates Period of the existing license.
- When You upgrade a license that had _not_ been activated before, the upgrade license adopts the unactivated state of the existing license. The Activation Date is set on first activation for use with a Public Site.
- The **"Included Updates Period"** is the time span of three (3) years after the Activation Date.
- Licensees (You), Clients and Websites are **"Qualified"** if they satisfy the purchase requirements from the ["Order Process" section](#order-process) of this Agreement.
Every time you see one of these capitalized terms in the following text, it has the meaning that has been explained above.
## Usage for a Public Site
Installing Kirby on or using it for a Public Site requires a [paid license](https://getkirby.com/buy).
Installing Kirby on or using it for a Public Site requires a [paid license](https://getkirby.com/buy). Once a paid license is needed, the license must be immediately activated to the Public Sites domain name and root directory via our license hub or the activation feature in the Kirby Panel.
As Kirby is software and software is intangible, We don't sell it as such. Instead, this Agreement grants a license for each purchase to install and use a single instance of Kirby on a **specific Website**. Additional Kirby licenses must be purchased in order to install and use Kirby on **additional Websites**.
The license is **non-exclusive** (meaning that You are not the only one who We will issue a license) and **generally non-transferable** (meaning that the one who purchases the license is the licensee).
The license is **non-exclusive** (meaning that You are not the only one to whom We will issue a license) and **generally non-transferable** (meaning that the one who purchases the license is the licensee).
On request, We will **transfer** a license to anyone who is also allowed to buy Kirby licenses by law and this Agreement. The new licensee will take over all rights and obligations of this Agreement from You at the moment We confirm the license transfer.
On request, We will **transfer** a license to anyone who would be allowed and Qualified to purchase the license by law and this Agreement. The new licensee will take over all rights and obligations of this Agreement from You at the moment We confirm the license transfer.
We will also **reassign** a license to another Website domain and root directory of Your choice, provided that You confirm that the previous Website is no longer in operation and will not be operated with the same license in the future.
We will also **reassign** a license to another Qualified Website domain and root directory, if You confirm that the previous Website is no longer in operation and will not be operated with the same license in the future.
If the new licensee, Website or Client in a transfer or reassignment is not Qualified for the existing license, You or the new licensee need to **upgrade the license to the qualifying terms and conditions** before the transfer or reassignment can be performed.
If you need to transfer your Kirby license to another individual or company (for example to your client or a new agency) or reassign it to a different project, please get in touch directly at <support@getkirby.com>.
A license is valid for all Updates of the same major Kirby release. We reserve the right to charge an **upgrade fee for Upgrade releases**. Whether a release is an Update or Upgrade is at Our sole discretion.
A license is valid for all Major Releases that We publish before the end of the Included Updates Period. It is also valid for all releases in those Major Generations independent of their release date. Whether a release is a Minor Release or Major Release is at Our sole discretion.
The use of releases in Major Generations that We publish after the Included Updates Period requires a **paid license upgrade**. An upgrade license replaces the existing license.
## Order Process
Our order process is conducted by Our online reseller [Paddle.com](https://paddle.com). Paddle.com is the Merchant of Record for all Our orders. Paddle provides all customer service inquiries and handles returns.
When purchasing a license, You are **responsible to choose the right license** based on You and the Website project. Different license variants can come with certain requirements towards You and/or the Website project. We publish all such requirements on <https://getkirby.com/buy> in a way that makes them clearly visible before the purchase. With Your purchase, You confirm that You and the Website project qualify for the selected license variant.
If the Website is created for a Client, You need to make sure that the **Client qualifies for the selected license**.
If You purchase licenses **in advance**, You need to ensure to only use the license(s) for projects that satisfy the requirements for the selected license variant(s).
We **reserve the right to verify** at any time after the purchase whether You, the Website and (if applicable) the Client are Qualified. Changes to the situation of You, the Website or the Client as well as changes to the published information on Our "Buy" page after the purchase or after the assignment to a Client do _not_ take effect on an existing license unless You upgrade the license or We transfer or reassign the license on Your request.
## Free Licenses
Kirby can be used **for free in the following cases**.
@@ -133,7 +168,7 @@ The following cases are exempted from this restriction:
E.g. the following cases are explicitly **_not_ allowed**:
- Selling, licensing or distributing a new product based on Kirby that modifies or hides Kirbys identity as a Content Management System (CMS)
- Forking Kirby and selling the modified version ([see above](#restrictions__modification-of-the-source-code))
- Forking Kirby and selling the modified version ([see above](#modification-of-the-source-code))
- Buying licenses in bulk and reselling them in your own shop
- Bundling or including Kirbys Source Code in the publication and/or distribution of a Websites source code or a (free or paid) theme or plugin (please use Git submodules or Composer or provide a link to Our repository or website instead)
@@ -168,6 +203,14 @@ You may also _not_:
Technical support is **provided as described on Our website** at <https://getkirby.com>. **No representations or guarantees** are made regarding the response time in which support questions are answered, however We will do Our best to respond quickly.
For each Major Generation, We aim to provide **security support for three (3) years** after the Major Release. Security support means that We will provide free security updates for the supported releases, which will include fixes for security vulnerabilities according to the following rules:
- We published a security advisory on <https://getkirby.com/security> within the respective security support period. We will publish vulnerabilities on this page as soon as they are known to Us and an official fix for any supported release is available.
- The latest release of the supported Major Generation is affected by the vulnerability.
With each vulnerability, We aim to publish the security advisory and security updates for all supported Major Generations at the same time.
You can find up-to-date information on our currently supported versions in our [public security policy](https://getkirby.com/security).
We reserve the right to **limit technical support for free licenses**.
## Refund Policy
@@ -204,6 +247,8 @@ YOU EXPRESSLY UNDERSTAND AND AGREE THAT **WE SHALL NOT BE LIABLE** FOR ANY DIREC
Bastian Allgeier **owns all rights**, title and interest to Kirby (including all intellectual property rights) and **reserves all rights to Kirby** that are not expressly granted in this Agreement.
In the event that Kirby will no longer be actively maintained, Bastian Allgeier will provide the Source Code under the terms of a free and open source software (FOSS) license as far as legally and contractually possible.
## Applicable Law & Place of Jurisdiction
1. For all disputes arising out of or in connection with this Agreement, the courts competent for Neckargemünd, Germany, shall have exclusive jurisdiction. However, We shall have the choice to file lawsuits against You before the courts competent for Your place of business.
@@ -219,4 +264,4 @@ Should any provision of this Agreement be or become invalid, void or unenforceab
Due to Kirby's flexibility, you may have special use cases or requirements that don't fit this Agreement.
If that's the case or if you have any questions, feel free to get in touch: <support@getkirby.com>. We are happy to think outside the box and find custom license solutions for your creative application of Kirby.
If that's the case or if you have any questions, feel free to [get in touch](mailto:support@getkirby.com). We are happy to think outside the box and find custom license solutions for your creative application of Kirby.

View File

@@ -1,3 +1,22 @@
# Security Policy
Please see the [Security Policy on the Kirby website](https://getkirby.com/security) for a list of the currently supported Kirby versions and of past security incidents as well as for information on how to report security vulnerabilities in the Kirby core or in the Panel.
## Supported versions and past security incidents
You can find up-to-date information on the security status of each version on <https://getkirby.com/security>.
## Security of your Kirby site
We have a detailed [security guide](https://getkirby.com/docs/guide/security) with information on how to keep your Kirby installation secure.
## Reporting a vulnerability
If you have spotted a vulnerability in Kirby's core or the Panel, please make sure to let us know immediately. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
You can always contact us directly at **<security@getkirby.com>**.
If you want to encrypt your message, our GPG key is [6E6B 057A F491 FFAD 363F 6F49 9101 10FA A459 E120](https://getkirby.com/pgp.asc).
You can also use the [security advisory form on GitHub](https://github.com/getkirby/kirby/security/advisories/new) to securely and privately report a vulnerability to us.
We will send you a response as soon as possible and will keep you informed on our progress towards a fix and announcement.
**Please do not write to us publicly, e.g. in the forum, on Discord or in a GitHub issue. A public report can give attackers valuable time to exploit the issue before it is fixed. By letting us know directly and coordinating the disclosure with us, you can help to protect other Kirby users from such attacks.**

View File

@@ -5,8 +5,8 @@
* stop at older or too recent versions
*/
if (
version_compare(PHP_VERSION, '8.0.0', '>=') === false ||
version_compare(PHP_VERSION, '8.3.0', '<') === false
version_compare(PHP_VERSION, '8.1.0', '>=') === false ||
version_compare(PHP_VERSION, '8.4.0', '<') === false
) {
die(include __DIR__ . '/views/php.php');
}

View File

@@ -1,9 +1,9 @@
{
"name": "getkirby/cms",
"description": "The Kirby 3 core",
"description": "The Kirby core",
"license": "proprietary",
"type": "kirby-cms",
"version": "3.9.8",
"version": "4.0.0",
"keywords": [
"kirby",
"cms",
@@ -24,7 +24,7 @@
"source": "https://github.com/getkirby/kirby"
},
"require": {
"php": ">=8.0.0 <8.3.0",
"php": "~8.1.0 || ~8.2.0 || ~8.3.0",
"ext-SimpleXML": "*",
"ext-ctype": "*",
"ext-curl": "*",
@@ -36,16 +36,17 @@
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"christian-riesen/base32": "1.6.0",
"claviska/simpleimage": "4.0.6",
"composer/semver": "3.4.0",
"filp/whoops": "2.15.4",
"getkirby/composer-installer": "^1.2.1",
"laminas/laminas-escaper": "2.12.0",
"laminas/laminas-escaper": "2.13.0",
"michelf/php-smartypants": "1.8.1",
"phpmailer/phpmailer": "6.8.1",
"phpmailer/phpmailer": "6.9.1",
"symfony/polyfill-intl-idn": "1.28.0",
"symfony/polyfill-mbstring": "1.28.0",
"symfony/yaml": "5.4.30"
"symfony/yaml": "6.3.8"
},
"replace": {
"symfony/polyfill-php72": "*"
@@ -79,7 +80,7 @@
},
"optimize-autoloader": true,
"platform": {
"php": "8.0.0"
"php": "8.1.0"
},
"platform-check": false
},
@@ -106,7 +107,7 @@
],
"fix": "php-cs-fixer fix",
"test": "phpunit --stderr",
"test:coverage": "phpunit --stderr --coverage-html=tests/coverage",
"test:coverage": "XDEBUG_MODE=coverage phpunit --stderr --coverage-html=tests/coverage",
"zip": "composer archive --format=zip --file=dist"
}
}

139
kirby/composer.lock generated
View File

@@ -4,8 +4,67 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3b71d9ba412291355108b25c00119867",
"content-hash": "c2116047cb2cec949e7949915b204002",
"packages": [
{
"name": "christian-riesen/base32",
"version": "1.6.0",
"source": {
"type": "git",
"url": "https://github.com/ChristianRiesen/base32.git",
"reference": "2e82dab3baa008e24a505649b0d583c31d31e894"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ChristianRiesen/base32/zipball/2e82dab3baa008e24a505649b0d583c31d31e894",
"reference": "2e82dab3baa008e24a505649b0d583c31d31e894",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.17",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^8.5.13 || ^9.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Base32\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Riesen",
"email": "chris.riesen@gmail.com",
"homepage": "http://christianriesen.com",
"role": "Developer"
}
],
"description": "Base32 encoder/decoder according to RFC 4648",
"homepage": "https://github.com/ChristianRiesen/base32",
"keywords": [
"base32",
"decode",
"encode",
"rfc4648"
],
"support": {
"issues": "https://github.com/ChristianRiesen/base32/issues",
"source": "https://github.com/ChristianRiesen/base32/tree/1.6.0"
},
"time": "2021-02-26T10:19:33+00:00"
},
{
"name": "claviska/simpleimage",
"version": "4.0.6",
@@ -260,33 +319,33 @@
},
{
"name": "laminas/laminas-escaper",
"version": "2.12.0",
"version": "2.13.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-escaper.git",
"reference": "ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490"
"reference": "af459883f4018d0f8a0c69c7a209daef3bf973ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490",
"reference": "ee7a4c37bf3d0e8c03635d5bddb5bb3184ead490",
"url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/af459883f4018d0f8a0c69c7a209daef3bf973ba",
"reference": "af459883f4018d0f8a0c69c7a209daef3bf973ba",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-mbstring": "*",
"php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0"
"php": "~8.1.0 || ~8.2.0 || ~8.3.0"
},
"conflict": {
"zendframework/zend-escaper": "*"
},
"require-dev": {
"infection/infection": "^0.26.6",
"laminas/laminas-coding-standard": "~2.4.0",
"infection/infection": "^0.27.0",
"laminas/laminas-coding-standard": "~2.5.0",
"maglnet/composer-require-checker": "^3.8.0",
"phpunit/phpunit": "^9.5.18",
"psalm/plugin-phpunit": "^0.17.0",
"vimeo/psalm": "^4.22.0"
"phpunit/phpunit": "^9.6.7",
"psalm/plugin-phpunit": "^0.18.4",
"vimeo/psalm": "^5.9"
},
"type": "library",
"autoload": {
@@ -318,7 +377,7 @@
"type": "community_bridge"
}
],
"time": "2022-10-10T10:11:09+00:00"
"time": "2023-10-10T08:35:13+00:00"
},
{
"name": "league/color-extractor",
@@ -437,16 +496,16 @@
},
{
"name": "phpmailer/phpmailer",
"version": "v6.8.1",
"version": "v6.9.1",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "e88da8d679acc3824ff231fdc553565b802ac016"
"reference": "039de174cd9c17a8389754d3b877a2ed22743e18"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/e88da8d679acc3824ff231fdc553565b802ac016",
"reference": "e88da8d679acc3824ff231fdc553565b802ac016",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/039de174cd9c17a8389754d3b877a2ed22743e18",
"reference": "039de174cd9c17a8389754d3b877a2ed22743e18",
"shasum": ""
},
"require": {
@@ -466,6 +525,7 @@
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
@@ -505,7 +565,7 @@
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.8.1"
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.1"
},
"funding": [
{
@@ -513,7 +573,7 @@
"type": "github"
}
],
"time": "2023-08-29T08:26:30+00:00"
"time": "2023-11-25T22:23:28+00:00"
},
{
"name": "psr/log",
@@ -567,25 +627,25 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v2.5.2",
"version": "v3.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66"
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
"reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=8.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.5-dev"
"dev-main": "3.4-dev"
},
"thanks": {
"name": "symfony/contracts",
@@ -614,7 +674,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0"
},
"funding": [
{
@@ -630,7 +690,7 @@
"type": "tidelift"
}
],
"time": "2022-01-02T09:53:40+00:00"
"time": "2023-05-23T14:45:45+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -970,31 +1030,28 @@
},
{
"name": "symfony/yaml",
"version": "v5.4.30",
"version": "v6.3.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "c6980e82a6656f6ebfabfd82f7585794cb122554"
"reference": "3493af8a8dad7fa91c77fa473ba23ecd95334a92"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/c6980e82a6656f6ebfabfd82f7585794cb122554",
"reference": "c6980e82a6656f6ebfabfd82f7585794cb122554",
"url": "https://api.github.com/repos/symfony/yaml/zipball/3493af8a8dad7fa91c77fa473ba23ecd95334a92",
"reference": "3493af8a8dad7fa91c77fa473ba23ecd95334a92",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1|^3",
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
"symfony/console": "<5.3"
"symfony/console": "<5.4"
},
"require-dev": {
"symfony/console": "^5.3|^6.0"
},
"suggest": {
"symfony/console": "For validating YAML files using the lint command"
"symfony/console": "^5.4|^6.0"
},
"bin": [
"Resources/bin/yaml-lint"
@@ -1025,7 +1082,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v5.4.30"
"source": "https://github.com/symfony/yaml/tree/v6.3.8"
},
"funding": [
{
@@ -1041,7 +1098,7 @@
"type": "tidelift"
}
],
"time": "2023-10-27T18:36:14+00:00"
"time": "2023-11-06T10:58:05+00:00"
}
],
"packages-dev": [],
@@ -1051,7 +1108,7 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=8.0.0 <8.3.0",
"php": "~8.1.0 || ~8.2.0 || ~8.3.0",
"ext-simplexml": "*",
"ext-ctype": "*",
"ext-curl": "*",
@@ -1066,7 +1123,7 @@
},
"platform-dev": [],
"platform-overrides": {
"php": "8.0.0"
"php": "8.1.0"
},
"plugin-api-version": "2.6.0"
}

View File

@@ -3,7 +3,6 @@
return [
// cms classes
'collection' => 'Kirby\Cms\Collection',
'field' => 'Kirby\Cms\Field',
'file' => 'Kirby\Cms\File',
'files' => 'Kirby\Cms\Files',
'find' => 'Kirby\Cms\Find',
@@ -24,6 +23,9 @@ return [
'users' => 'Kirby\Cms\Users',
'visitor' => 'Kirby\Cms\Visitor',
// content classes
'field' => 'Kirby\Content\Field',
// data handler
'data' => 'Kirby\Data\Data',
'json' => 'Kirby\Data\Json',
@@ -69,17 +71,25 @@ return [
'v' => 'Kirby\Toolkit\V',
'xml' => 'Kirby\Toolkit\Xml',
// TODO: remove in 4.0.0
'kirby\cms\asset' => 'Kirby\Filesystem\Asset',
'kirby\cms\dir' => 'Kirby\Filesystem\Dir',
'kirby\cms\filename' => 'Kirby\Filesystem\Filename',
'kirby\cms\filefoundation' => 'Kirby\Filesystem\IsFile',
'kirby\cms\form' => 'Kirby\Form\Form',
'kirby\cms\kirbytag' => 'Kirby\Text\KirbyTag',
'kirby\cms\kirbytags' => 'Kirby\Text\KirbyTags',
'kirby\cms\template' => 'Kirby\Template\Template',
'kirby\toolkit\dir' => 'Kirby\Filesystem\Dir',
'kirby\toolkit\f' => 'Kirby\Filesystem\F',
'kirby\toolkit\file' => 'Kirby\Filesystem\File',
'kirby\toolkit\mime' => 'Kirby\Filesystem\Mime',
// Deprecated aliases:
// Any of these might be removed at any point in the future
'kirby\cms\asset' => 'Kirby\Filesystem\Asset',
'kirby\cms\content' => 'Kirby\Content\Content',
'kirby\cms\contenttranslation' => 'Kirby\Content\ContentTranslation',
'kirby\cms\dir' => 'Kirby\Filesystem\Dir',
'kirby\cms\filename' => 'Kirby\Filesystem\Filename',
'kirby\cms\filefoundation' => 'Kirby\Filesystem\IsFile',
'kirby\cms\field' => 'Kirby\Content\Field',
'kirby\cms\form' => 'Kirby\Form\Form',
'kirby\cms\kirbytag' => 'Kirby\Text\KirbyTag',
'kirby\cms\kirbytags' => 'Kirby\Text\KirbyTags',
'kirby\cms\template' => 'Kirby\Template\Template',
'kirby\form\options' => 'Kirby\Options\Options',
'kirby\form\optionsapi' => 'Kirby\Options\OptionsApi',
'kirby\form\optionsquery' => 'Kirby\Options\OptionsQuery',
'kirby\toolkit\dir' => 'Kirby\Filesystem\Dir',
'kirby\toolkit\f' => 'Kirby\Filesystem\F',
'kirby\toolkit\file' => 'Kirby\Filesystem\File',
'kirby\toolkit\mime' => 'Kirby\Filesystem\Mime',
'kirby\toolkit\query' => 'Kirby\Query\Query',
];

View File

@@ -1,6 +1,6 @@
<?php
use Kirby\Exception\PermissionException;
use Kirby\Exception\AuthException;
return function () {
$auth = $this->kirby()->auth();
@@ -11,17 +11,17 @@ return function () {
$auth->type($allowImpersonation) === 'session' &&
$auth->csrf() === false
) {
throw new PermissionException('Unauthenticated');
throw new AuthException('Unauthenticated');
}
// get user from session or basic auth
if ($user = $auth->user(null, $allowImpersonation)) {
if ($user->role()->permissions()->for('access', 'panel') === false) {
throw new PermissionException(['key' => 'access.panel']);
throw new AuthException(['key' => 'access.panel']);
}
return $user;
}
throw new PermissionException('Unauthenticated');
throw new AuthException('Unauthenticated');
};

View File

@@ -3,6 +3,14 @@
/**
* Api Collection Definitions
*/
use Kirby\Cms\Files;
use Kirby\Cms\Languages;
use Kirby\Cms\Pages;
use Kirby\Cms\Roles;
use Kirby\Cms\Translations;
use Kirby\Cms\Users;
return [
/**
@@ -10,7 +18,7 @@ return [
*/
'children' => [
'model' => 'page',
'type' => 'Kirby\Cms\Pages',
'type' => Pages::class,
'view' => 'compact'
],
@@ -19,7 +27,7 @@ return [
*/
'files' => [
'model' => 'file',
'type' => 'Kirby\Cms\Files'
'type' => Files::class,
],
/**
@@ -27,7 +35,7 @@ return [
*/
'languages' => [
'model' => 'language',
'type' => 'Kirby\Cms\Languages'
'type' => Languages::class,
],
/**
@@ -35,7 +43,7 @@ return [
*/
'pages' => [
'model' => 'page',
'type' => 'Kirby\Cms\Pages',
'type' => Pages::class,
'view' => 'compact'
],
@@ -44,7 +52,7 @@ return [
*/
'roles' => [
'model' => 'role',
'type' => 'Kirby\Cms\Roles',
'type' => Roles::class,
'view' => 'compact'
],
@@ -53,7 +61,7 @@ return [
*/
'translations' => [
'model' => 'translation',
'type' => 'Kirby\Cms\Translations',
'type' => Translations::class,
'view' => 'compact'
],
@@ -63,7 +71,7 @@ return [
'users' => [
'default' => fn () => $this->users(),
'model' => 'user',
'type' => 'Kirby\Cms\Users',
'type' => Users::class,
'view' => 'compact'
]

View File

@@ -8,6 +8,7 @@ return [
'FileBlueprint' => include __DIR__ . '/models/FileBlueprint.php',
'FileVersion' => include __DIR__ . '/models/FileVersion.php',
'Language' => include __DIR__ . '/models/Language.php',
'License' => include __DIR__ . '/models/License.php',
'Page' => include __DIR__ . '/models/Page.php',
'PageBlueprint' => include __DIR__ . '/models/PageBlueprint.php',
'Role' => include __DIR__ . '/models/Role.php',

View File

@@ -59,7 +59,7 @@ return [
'url' => fn (File $file) => $file->url(),
'uuid' => fn (File $file) => $file->uuid()?->toString()
],
'type' => 'Kirby\Cms\File',
'type' => File::class,
'views' => [
'default' => [
'content',

View File

@@ -12,7 +12,6 @@ return [
'tabs' => fn (FileBlueprint $blueprint) => $blueprint->tabs(),
'title' => fn (FileBlueprint $blueprint) => $blueprint->title(),
],
'type' => 'Kirby\Cms\FileBlueprint',
'views' => [
],
'type' => FileBlueprint::class,
'views' => [],
];

View File

@@ -20,7 +20,7 @@ return [
'type' => fn (FileVersion $file) => $file->type(),
'url' => fn (FileVersion $file) => $file->url(),
],
'type' => 'Kirby\Cms\FileVersion',
'type' => FileVersion::class,
'views' => [
'default' => [
'dimensions',

View File

@@ -15,7 +15,7 @@ return [
'rules' => fn (Language $language) => $language->rules(),
'url' => fn (Language $language) => $language->url(),
],
'type' => 'Kirby\Cms\Language',
'type' => Language::class,
'views' => [
'default' => [
'code',

View File

@@ -0,0 +1,17 @@
<?php
use Kirby\Cms\License;
/**
* Page
*/
return [
'fields' => [
'status' => fn (License $license) => $license->status()->value(),
'code' => function (License $license) {
return $this->kirby()->user()->isAdmin() ? $license->code() : $license->code(true);
},
'type' => fn (License $license) => $license->type()->label(),
],
'type' => License::class,
];

View File

@@ -40,7 +40,7 @@ return [
'url' => fn (Page $page) => $page->url(),
'uuid' => fn (Page $page) => $page->uuid()?->toString()
],
'type' => 'Kirby\Cms\Page',
'type' => Page::class,
'views' => [
'compact' => [
'id',

View File

@@ -15,7 +15,6 @@ return [
'tabs' => fn (PageBlueprint $blueprint) => $blueprint->tabs(),
'title' => fn (PageBlueprint $blueprint) => $blueprint->title(),
],
'type' => 'Kirby\Cms\PageBlueprint',
'views' => [
],
'type' => PageBlueprint::class,
'views' => [],
];

View File

@@ -12,7 +12,7 @@ return [
'permissions' => fn (Role $role) => $role->permissions()->toArray(),
'title' => fn (Role $role) => $role->title(),
],
'type' => 'Kirby\Cms\Role',
'type' => Role::class,
'views' => [
'compact' => [
'description',

View File

@@ -19,7 +19,7 @@ return [
'title' => fn (Site $site) => $site->title()->value(),
'url' => fn (Site $site) => $site->url(),
],
'type' => 'Kirby\Cms\Site',
'type' => Site::class,
'views' => [
'compact' => [
'title',

View File

@@ -12,6 +12,6 @@ return [
'tabs' => fn (SiteBlueprint $blueprint) => $blueprint->tabs(),
'title' => fn (SiteBlueprint $blueprint) => $blueprint->title(),
],
'type' => 'Kirby\Cms\SiteBlueprint',
'type' => SiteBlueprint::class,
'views' => [],
];

View File

@@ -49,7 +49,7 @@ return [
return null;
}
],
'type' => 'Kirby\Cms\System',
'type' => System::class,
'views' => [
'login' => [
'authStatus',

View File

@@ -13,7 +13,7 @@ return [
'id' => fn (Translation $translation) => $translation->id(),
'name' => fn (Translation $translation) => $translation->name(),
],
'type' => 'Kirby\Cms\Translation',
'type' => Translation::class,
'views' => [
'compact' => [
'direction',

View File

@@ -27,7 +27,7 @@ return [
'username' => fn (User $user) => $user->username(),
'uuid' => fn (User $user) => $user->uuid()?->toString()
],
'type' => 'Kirby\Cms\User',
'type' => User::class,
'views' => [
'default' => [
'avatar',

View File

@@ -12,7 +12,6 @@ return [
'tabs' => fn (UserBlueprint $blueprint) => $blueprint->tabs(),
'title' => fn (UserBlueprint $blueprint) => $blueprint->title(),
],
'type' => 'Kirby\Cms\UserBlueprint',
'views' => [
],
'type' => UserBlueprint::class,
'views' => [],
];

View File

@@ -19,7 +19,10 @@ return function ($kirby) {
// only add the language routes if the
// multi language setup is activated
if ($kirby->option('languages', false) !== false) {
$routes = array_merge($routes, include __DIR__ . '/routes/languages.php');
$routes = array_merge(
$routes,
include __DIR__ . '/routes/languages.php'
);
}
return $routes;

View File

@@ -1,7 +1,8 @@
<?php
// routing pattern to match all models with files
$pattern = '(account|pages/[^/]+|site|users/[^/]+)';
$filePattern = '(account/|pages/[^/]+/|site/|users/[^/]+/|)files/(:any)';
$parentPattern = '(account|pages/[^/]+|site|users/[^/]+)/files';
/**
* Files Routes
@@ -9,14 +10,14 @@ $pattern = '(account|pages/[^/]+|site|users/[^/]+)';
return [
[
'pattern' => $pattern . '/files/(:any)/sections/(:any)',
'pattern' => $filePattern . '/sections/(:any)',
'method' => 'GET',
'action' => function (string $path, string $filename, string $sectionName) {
return $this->file($path, $filename)->blueprint()->section($sectionName)?->toResponse();
}
],
[
'pattern' => $pattern . '/files/(:any)/fields/(:any)/(:all?)',
'pattern' => $filePattern . '/fields/(:any)/(:all?)',
'method' => 'ALL',
'action' => function (string $parent, string $filename, string $fieldName, string $path = null) {
if ($file = $this->file($parent, $filename)) {
@@ -25,14 +26,14 @@ return [
}
],
[
'pattern' => $pattern . '/files',
'pattern' => $parentPattern,
'method' => 'GET',
'action' => function (string $path) {
return $this->parent($path)->files()->sorted();
return $this->files($path)->sorted();
}
],
[
'pattern' => $pattern . '/files',
'pattern' => $parentPattern,
'method' => 'POST',
'action' => function (string $path) {
// move_uploaded_file() not working with unit test
@@ -54,10 +55,10 @@ return [
}
],
[
'pattern' => $pattern . '/files/search',
'pattern' => $parentPattern . '/search',
'method' => 'GET|POST',
'action' => function (string $path) {
$files = $this->parent($path)->files();
$files = $this->files($path);
if ($this->requestMethod() === 'GET') {
return $files->search($this->requestQuery('q'));
@@ -67,24 +68,24 @@ return [
}
],
[
'pattern' => $pattern . '/files/sort',
'pattern' => $parentPattern . '/sort',
'method' => 'PATCH',
'action' => function (string $path) {
return $this->parent($path)->files()->changeSort(
return $this->files($path)->changeSort(
$this->requestBody('files'),
$this->requestBody('index')
);
}
],
[
'pattern' => $pattern . '/files/(:any)',
'pattern' => $filePattern,
'method' => 'GET',
'action' => function (string $path, string $filename) {
return $this->file($path, $filename);
}
],
[
'pattern' => $pattern . '/files/(:any)',
'pattern' => $filePattern,
'method' => 'PATCH',
'action' => function (string $path, string $filename) {
return $this->file($path, $filename)->update(
@@ -95,7 +96,7 @@ return [
}
],
[
'pattern' => $pattern . '/files/(:any)',
'pattern' => $filePattern,
'method' => 'POST',
'action' => function (string $path, string $filename) {
// move the source file from the temp dir
@@ -105,28 +106,29 @@ return [
}
],
[
'pattern' => $pattern . '/files/(:any)',
'pattern' => $filePattern,
'method' => 'DELETE',
'action' => function (string $path, string $filename) {
return $this->file($path, $filename)->delete();
}
],
[
'pattern' => $pattern . '/files/(:any)/name',
'pattern' => $filePattern . '/name',
'method' => 'PATCH',
'action' => function (string $path, string $filename) {
return $this->file($path, $filename)->changeName($this->requestBody('name'));
}
],
[
'pattern' => 'files/search',
'pattern' => $parentPattern . '/search',
'method' => 'GET|POST',
'action' => function () {
$files = $this
->site()
->index(true)
->filter('isReadable', true)
->files();
->filter('isListable', true)
->files()
->filter('isListable', true);
if ($this->requestMethod() === 'GET') {
return $files->search($this->requestQuery('q'));

View File

@@ -0,0 +1,35 @@
<?php
// @codeCoverageIgnoreStart
return [
'routes' => function ($kirby) {
return [
[
'pattern' => 'query',
'method' => 'POST|GET',
'auth' => $kirby->option('kql.auth') !== false,
'action' => function () use ($kirby) {
$kql = '\Kirby\Kql\Kql';
if (class_exists($kql) === false) {
return [
'code' => 500,
'status' => 'error',
'message' => 'KQL plugin is not installed',
];
}
$input = $kirby->request()->get();
$result = $kql::run($input);
return [
'code' => 200,
'result' => $result,
'status' => 'ok',
];
}
]
];
}
];
// @codeCoverageIgnoreEnd

View File

@@ -4,8 +4,9 @@
/**
* Page Routes
*/
return [
return [
[
'pattern' => 'pages/(:any)',
'method' => 'GET',

View File

@@ -75,7 +75,7 @@ return [
$pages = $this
->site()
->index(true)
->filter('isReadable', true);
->filter('isListable', true);
if ($this->requestMethod() === 'GET') {
return $pages->search($this->requestQuery('q'));

View File

@@ -8,6 +8,7 @@ return function () {
'label' => I18n::translate('view.account'),
'search' => 'users',
'dialogs' => require __DIR__ . '/account/dialogs.php',
'drawers' => require __DIR__ . '/account/drawers.php',
'dropdowns' => require __DIR__ . '/account/dropdowns.php',
'views' => require __DIR__ . '/account/views.php'
];

View File

@@ -1,5 +1,7 @@
<?php
use Kirby\Panel\UserTotpEnableDialog;
$dialogs = require __DIR__ . '/../users/dialogs.php';
return [
@@ -46,6 +48,13 @@ return [
'submit' => $dialogs['user.delete']['submit'],
],
// account fields dialogs
'account.fields' => [
'pattern' => '(account)/files/(:any)/fields/(:any)/(:all?)',
'load' => $dialogs['user.fields']['load'],
'submit' => $dialogs['user.fields']['submit']
],
// change file name
'account.file.changeName' => [
'pattern' => '(account)/files/(:any)/changeName',
@@ -60,6 +69,13 @@ return [
'submit' => $dialogs['user.file.changeSort']['submit'],
],
// change file template
'account.file.changeTemplate' => [
'pattern' => '(account)/files/(:any)/changeTemplate',
'load' => $dialogs['user.file.changeTemplate']['load'],
'submit' => $dialogs['user.file.changeTemplate']['submit'],
],
// delete
'account.file.delete' => [
'pattern' => '(account)/files/(:any)/delete',
@@ -67,4 +83,24 @@ return [
'submit' => $dialogs['user.file.delete']['submit'],
],
// account file fields dialogs
'account.file.fields' => [
'pattern' => '(account)/files/(:any)/fields/(:any)/(:all?)',
'load' => $dialogs['user.file.fields']['load'],
'submit' => $dialogs['user.file.fields']['submit']
],
// account enable TOTP
'account.totp.enable' => [
'pattern' => '(account)/totp/enable',
'load' => fn () => (new UserTotpEnableDialog())->load(),
'submit' => fn () => (new UserTotpEnableDialog())->submit()
],
// account disable TOTP
'account.totp.disable' => [
'pattern' => '(account)/totp/disable',
'load' => $dialogs['user.totp.disable']['load'],
'submit' => $dialogs['user.totp.disable']['submit']
],
];

View File

@@ -0,0 +1,19 @@
<?php
$drawers = require __DIR__ . '/../users/drawers.php';
return [
// account fields drawers
'account.fields' => [
'pattern' => '(account)/files/(:any)/fields/(:any)/(:all?)',
'load' => $drawers['user.fields']['load'],
'submit' => $drawers['user.fields']['submit']
],
// account file fields drawers
'account.file.fields' => [
'pattern' => '(account)/files/(:any)/fields/(:any)/(:all?)',
'load' => $drawers['user.file.fields']['load'],
'submit' => $drawers['user.file.fields']['submit']
],
];

View File

@@ -2,6 +2,7 @@
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Toolkit\I18n;
return [
'account' => [
@@ -19,6 +20,13 @@ return [
],
'account.password' => [
'pattern' => 'reset-password',
'action' => fn () => ['component' => 'k-reset-password-view']
'action' => fn () => [
'component' => 'k-reset-password-view',
'breadcrumb' => [
[
'label' => I18n::translate('view.resetPassword')
]
]
]
]
];

View File

@@ -0,0 +1,61 @@
<?php
use Kirby\Cms\Find;
use Kirby\Panel\Field;
return [
'model' => [
'load' => function (
string $modelPath,
string $fieldName,
string|null $path = null
) {
return Field::dialog(
model: Find::parent($modelPath),
fieldName: $fieldName,
path: $path,
method: 'GET'
);
},
'submit' => function (
string $modelPath,
string $fieldName,
string|null $path = null
) {
return Field::dialog(
model: Find::parent($modelPath),
fieldName: $fieldName,
path: $path,
method: 'POST'
);
}
],
'file' => [
'load' => function (
string $modelPath,
string $filename,
string $fieldName,
string|null $path = null
) {
return Field::dialog(
model: Find::file($modelPath, $filename),
fieldName: $fieldName,
path: $path,
method: 'GET'
);
},
'submit' => function (
string $modelPath,
string $filename,
string $fieldName,
string|null $path = null
) {
return Field::dialog(
model: Find::file($modelPath, $filename),
fieldName: $fieldName,
path: $path,
method: 'POST'
);
}
],
];

View File

@@ -0,0 +1,61 @@
<?php
use Kirby\Cms\Find;
use Kirby\Panel\Field;
return [
'model' => [
'load' => function (
string $modelPath,
string $fieldName,
string|null $path = null
) {
return Field::drawer(
model: Find::parent($modelPath),
fieldName: $fieldName,
path: $path,
method: 'GET'
);
},
'submit' => function (
string $modelPath,
string $fieldName,
string|null $path = null
) {
return Field::drawer(
model: Find::parent($modelPath),
fieldName: $fieldName,
path: $path,
method: 'POST'
);
}
],
'file' => [
'load' => function (
string $modelPath,
string $filename,
string $fieldName,
string|null $path = null
) {
return Field::drawer(
model: Find::file($modelPath, $filename),
fieldName: $fieldName,
path: $path,
method: 'GET'
);
},
'submit' => function (
string $modelPath,
string $filename,
string $fieldName,
string|null $path = null
) {
return Field::drawer(
model: Find::file($modelPath, $filename),
fieldName: $fieldName,
path: $path,
method: 'POST'
);
}
],
];

View File

@@ -40,7 +40,8 @@ return [
},
'submit' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
$renamed = $file->changeName($file->kirby()->request()->get('name'));
$name = $file->kirby()->request()->get('name');
$renamed = $file->changeName($name);
$oldUrl = $file->panel()->url(true);
$newUrl = $renamed->panel()->url(true);
$response = [
@@ -96,6 +97,44 @@ return [
}
],
'changeTemplate' => [
'load' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
$blueprints = $file->blueprints();
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'warning' => [
'type' => 'info',
'theme' => 'notice',
'text' => I18n::translate('file.changeTemplate.notice')
],
'template' => Field::template($blueprints, [
'required' => true
])
],
'theme' => 'notice',
'submitButton' => I18n::translate('change'),
'value' => [
'template' => $file->template()
]
]
];
},
'submit' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
$template = $file->kirby()->request()->get('template');
$file->changeTemplate($template);
return [
'event' => 'file.changeTemplate',
];
}
],
'delete' => [
'load' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
@@ -129,4 +168,5 @@ return [
];
}
],
];

View File

@@ -0,0 +1,11 @@
<?php
return function () {
return [
'icon' => 'lab',
'label' => 'Lab',
'menu' => false,
'drawers' => require __DIR__ . '/lab/drawers.php',
'views' => require __DIR__ . '/lab/views.php'
];
};

View File

@@ -0,0 +1,30 @@
<?php
use Kirby\Panel\Lab\Docs;
return [
'lab.docs' => [
'pattern' => 'lab/docs/(:any)',
'load' => function (string $component) {
if (Docs::installed() === false) {
return [
'component' => 'k-text-drawer',
'props' => [
'text' => 'The UI docs are not installed.'
]
];
}
$docs = new Docs($component);
return [
'component' => 'k-lab-docs-drawer',
'props' => [
'icon' => 'book',
'title' => $component,
'docs' => $docs->toArray()
]
];
},
],
];

View File

@@ -0,0 +1,138 @@
<?php
use Kirby\Panel\Lab\Category;
use Kirby\Panel\Lab\Docs;
return [
'lab' => [
'pattern' => 'lab',
'action' => function () {
return [
'component' => 'k-lab-index-view',
'props' => [
'categories' => Category::all(),
'info' => Category::installed() ? null : 'The default Lab examples are not installed.',
'tab' => 'examples',
],
];
}
],
'lab.docs' => [
'pattern' => 'lab/docs',
'action' => function () {
$props = match (Docs::installed()) {
true => [
'categories' => [['examples' => Docs::all()]],
'tab' => 'docs',
],
false => [
'info' => 'The UI docs are not installed.',
'tab' => 'docs',
]
};
return [
'component' => 'k-lab-index-view',
'title' => 'Docs',
'breadcrumb' => [
[
'label' => 'Docs',
'link' => 'lab/docs'
]
],
'props' => $props,
];
}
],
'lab.doc' => [
'pattern' => 'lab/docs/(:any)',
'action' => function (string $component) {
$crumbs = [
[
'label' => 'Docs',
'link' => 'lab/docs'
],
[
'label' => $component,
'link' => 'lab/docs/' . $component
]
];
if (Docs::installed() === false) {
return [
'component' => 'k-lab-index-view',
'title' => $component,
'breadcrumb' => $crumbs,
'props' => [
'info' => 'The UI docs are not installed.',
'tab' => 'docs',
],
];
}
$docs = new Docs($component);
return [
'component' => 'k-lab-docs-view',
'title' => $component,
'breadcrumb' => $crumbs,
'props' => [
'component' => $component,
'docs' => $docs->toArray(),
'lab' => $docs->lab()
]
];
}
],
'lab.vue' => [
'pattern' => [
'lab/(:any)/(:any)/index.vue',
'lab/(:any)/(:any)/(:any)/index.vue'
],
'action' => function (
string $category,
string $id,
string|null $tab = null
) {
return Category::factory($category)->example($id, $tab)->serve();
}
],
'lab.example' => [
'pattern' => 'lab/(:any)/(:any)/(:any?)',
'action' => function (
string $category,
string $id,
string|null $tab = null
) {
$category = Category::factory($category);
$example = $category->example($id, $tab);
$props = $example->props();
$vue = $example->vue();
return [
'component' => 'k-lab-playground-view',
'breadcrumb' => [
[
'label' => $category->name(),
],
[
'label' => $example->title(),
'link' => $example->url()
]
],
'props' => [
'docs' => $props['docs'] ?? null,
'examples' => $vue['examples'],
'file' => $example->module(),
'github' => $example->github(),
'props' => $props,
'styles' => $vue['style'],
'tab' => $example->tab(),
'tabs' => array_values($example->tabs()),
'template' => $vue['template'],
'title' => $example->title(),
],
];
}
]
];

View File

@@ -4,7 +4,7 @@ use Kirby\Toolkit\I18n;
return function ($kirby) {
return [
'icon' => 'globe',
'icon' => 'translate',
'label' => I18n::translate('view.languages'),
'menu' => true,
'dialogs' => require __DIR__ . '/languages/dialogs.php',

View File

@@ -2,13 +2,15 @@
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Panel\Field;
use Kirby\Cms\LanguageVariable;
use Kirby\Exception\NotFoundException;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\I18n;
$languageDialogFields = [
'name' => [
'counter' => false,
'label' => I18n::translate('language.name'),
'type' => 'text',
'required' => true,
@@ -19,7 +21,7 @@ $languageDialogFields = [
'type' => 'text',
'required' => true,
'counter' => false,
'icon' => 'globe',
'icon' => 'translate',
'width' => '1/2'
],
'direction' => [
@@ -34,11 +36,27 @@ $languageDialogFields = [
'width' => '1/2'
],
'locale' => [
'label' => I18n::translate('language.locale'),
'type' => 'text',
'counter' => false,
'label' => I18n::translate('language.locale'),
'type' => 'text',
],
];
$translationDialogFields = [
'key' => [
'counter' => false,
'icon' => null,
'label' => I18n::translate('language.variable.key'),
'type' => 'text'
],
'value' => [
'buttons' => false,
'counter' => false,
'label' => I18n::translate('language.variable.value'),
'type' => 'textarea'
]
];
return [
// create language
@@ -92,8 +110,10 @@ return [
},
'submit' => function (string $id) {
Find::language($id)->delete();
return [
'event' => 'language.delete',
'event' => 'language.delete',
'redirect' => 'languages'
];
}
],
@@ -152,4 +172,95 @@ return [
];
}
],
'language.translation.create' => [
'pattern' => 'languages/(:any)/translations/create',
'load' => function (string $languageCode) use ($translationDialogFields) {
// find the language to make sure it exists
Find::language($languageCode);
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => $translationDialogFields,
'size' => 'large',
],
];
},
'submit' => function (string $languageCode) {
$request = App::instance()->request();
$language = Find::language($languageCode);
$key = $request->get('key', '');
$value = $request->get('value', '');
LanguageVariable::create($key, $value);
if ($language->isDefault() === false) {
$language->variable($key)->update($value);
}
return true;
}
],
'language.translation.delete' => [
'pattern' => 'languages/(:any)/translations/(:any)/delete',
'load' => function (string $languageCode, string $translationKey) {
$variable = Find::language($languageCode)->variable($translationKey, true);
if ($variable->exists() === false) {
throw new NotFoundException([
'key' => 'language.variable.notFound'
]);
}
return [
'component' => 'k-remove-dialog',
'props' => [
'text' => I18n::template('language.variable.delete.confirm', [
'key' => Escape::html($variable->key())
])
],
];
},
'submit' => function (string $languageCode, string $translationKey) {
return Find::language($languageCode)->variable($translationKey, true)->delete();
}
],
'language.translation.update' => [
'pattern' => 'languages/(:any)/translations/(:any)/update',
'load' => function (string $languageCode, string $translationKey) use ($translationDialogFields) {
$variable = Find::language($languageCode)->variable($translationKey, true);
if ($variable->exists() === false) {
throw new NotFoundException([
'key' => 'language.variable.notFound'
]);
}
$fields = $translationDialogFields;
$fields['key']['disabled'] = true;
$fields['value']['autofocus'] = true;
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => $fields,
'size' => 'large',
'value' => [
'key' => $variable->key(),
'value' => $variable->value()
]
],
];
},
'submit' => function (string $languageCode, string $translationKey) {
Find::language($languageCode)->variable($translationKey, true)->update(
App::instance()->request()->get('value')
);
return true;
}
]
];

View File

@@ -1,9 +1,104 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\I18n;
return [
'language' => [
'pattern' => 'languages/(:any)',
'when' => function (): bool {
return App::instance()->option('languages.variables', true) !== false;
},
'action' => function (string $code) {
$language = Find::language($code);
$link = '/languages/' . $language->code();
$strings = [];
$foundation = App::instance()->defaultLanguage()->translations();
$translations = $language->translations();
ksort($foundation);
foreach ($foundation as $key => $value) {
$strings[] = [
'key' => $key,
'value' => $translations[$key] ?? null,
'options' => [
[
'click' => 'update',
'icon' => 'edit',
'text' => I18n::translate('edit'),
],
[
'click' => 'delete',
'disabled' => $language->isDefault() === false,
'icon' => 'trash',
'text' => I18n::translate('delete'),
]
]
];
}
$next = function () use ($language) {
if ($next = $language->next()) {
return [
'link' => '/languages/' . $next->code(),
'title' => $next->name(),
];
}
};
$prev = function () use ($language) {
if ($prev = $language->prev()) {
return [
'link' => '/languages/' . $prev->code(),
'title' => $prev->name(),
];
}
};
return [
'component' => 'k-language-view',
'breadcrumb' => [
[
'label' => $name = $language->name(),
'link' => $link,
]
],
'props' => [
'deletable' => $language->isDeletable(),
'code' => Escape::html($language->code()),
'default' => $language->isDefault(),
'direction' => $language->direction(),
'id' => $language->code(),
'info' => [
[
'label' => 'Status',
'value' => I18n::translate('language.' . ($language->isDefault() ? 'default' : 'secondary')),
],
[
'label' => I18n::translate('language.code'),
'value' => $language->code(),
],
[
'label' => I18n::translate('language.locale'),
'value' => $language->locale(LC_ALL)
],
[
'label' => I18n::translate('language.direction'),
'value' => I18n::translate('language.direction.' . $language->direction()),
],
],
'name' => $name,
'next' => $next,
'prev' => $prev,
'translations' => $strings,
'url' => $language->url(),
]
];
}
],
'languages' => [
'pattern' => 'languages',
'action' => function () {
@@ -13,13 +108,15 @@ return [
'component' => 'k-languages-view',
'props' => [
'languages' => $kirby->languages()->values(fn ($language) => [
'default' => $language->isDefault(),
'id' => $language->code(),
'info' => Escape::html($language->code()),
'text' => Escape::html($language->name()),
])
'deletable' => $language->isDeletable(),
'default' => $language->isDefault(),
'id' => $language->code(),
'info' => Escape::html($language->code()),
'text' => Escape::html($language->name()),
]),
'variables' => $kirby->option('languages.variables', true)
]
];
}
],
]
];

View File

@@ -0,0 +1,11 @@
<?php
use Kirby\Toolkit\I18n;
return function () {
return [
'icon' => 'search',
'label' => I18n::translate('search'),
'views' => require __DIR__ . '/search/views.php'
];
};

View File

@@ -0,0 +1,17 @@
<?php
use Kirby\Cms\App;
return [
'search' => [
'pattern' => 'search',
'action' => function () {
return [
'component' => 'k-search-view',
'props' => [
'type' => App::instance()->request()->get('type'),
]
];
}
],
];

View File

@@ -11,7 +11,9 @@ return function ($kirby) {
'label' => $kirby->site()->blueprint()->title() ?? I18n::translate('view.site'),
'menu' => true,
'dialogs' => require __DIR__ . '/site/dialogs.php',
'drawers' => require __DIR__ . '/site/drawers.php',
'dropdowns' => require __DIR__ . '/site/dropdowns.php',
'requests' => require __DIR__ . '/site/requests.php',
'searches' => require __DIR__ . '/site/searches.php',
'views' => require __DIR__ . '/site/views.php',
];

View File

@@ -2,14 +2,19 @@
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Cms\PageRules;
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\PermissionException;
use Kirby\Panel\ChangesDialog;
use Kirby\Panel\Field;
use Kirby\Panel\PageCreateDialog;
use Kirby\Panel\Panel;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\Str;
use Kirby\Uuid\Uuids;
$fields = require __DIR__ . '/../fields/dialogs.php';
$files = require __DIR__ . '/../files/dialogs.php';
return [
@@ -155,10 +160,16 @@ return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'notice' => [
'type' => 'info',
'theme' => 'notice',
'text' => I18n::translate('page.changeTemplate.notice')
],
'template' => Field::template($blueprints, [
'required' => true
])
],
'theme' => 'notice',
'submitButton' => I18n::translate('change'),
'value' => [
'template' => $page->intendedTemplate()->name()
@@ -167,9 +178,10 @@ return [
];
},
'submit' => function (string $id) {
$request = App::instance()->request();
$page = Find::page($id);
$template = App::instance()->request()->get('template');
Find::page($id)->changeTemplate($request->get('template'));
$page->changeTemplate($template);
return [
'event' => 'page.changeTemplate',
@@ -224,17 +236,8 @@ return [
$slug = trim($request->get('slug', ''));
// basic input validation before we move on
if (Str::length($title) === 0) {
throw new InvalidArgumentException([
'key' => 'page.changeTitle.empty'
]);
}
if (Str::length($slug) === 0) {
throw new InvalidArgumentException([
'key' => 'page.slug.invalid'
]);
}
PageRules::validateTitleLength($title);
PageRules::validateSlugLength($slug);
// nothing changed
if ($page->title()->value() === $title && $page->slug() === $slug) {
@@ -277,93 +280,30 @@ return [
'page.create' => [
'pattern' => 'pages/create',
'load' => function () {
$kirby = App::instance();
$request = $kirby->request();
$request = App::instance()->request();
$dialog = new PageCreateDialog(
parentId: $request->get('parent'),
sectionId: $request->get('section'),
slug: $request->get('slug'),
template: $request->get('template'),
title: $request->get('title'),
viewId: $request->get('view'),
);
// the parent model for the new page
$parent = $request->get('parent', 'site');
// the view on which the add button is located
// this is important to find the right section
// and provide the correct templates for the new page
$view = $request->get('view', $parent);
// templates will be fetched depending on the
// section settings in the blueprint
$section = $request->get('section');
// this is the parent model
$model = Find::parent($parent);
// this is the view model
// i.e. site if the add button is on
// the dashboard
$view = Find::parent($view);
// available blueprints/templates for the new page
// are always loaded depending on the matching section
// in the view model blueprint
$blueprints = $view->blueprints($section);
// the pre-selected template
$template = $blueprints[0]['name'] ?? $blueprints[0]['value'] ?? null;
$fields = [
'parent' => Field::hidden(),
'title' => Field::title([
'required' => true,
'preselect' => true
]),
'slug' => Field::slug([
'required' => true,
'sync' => 'title',
'path' => empty($model->id()) === false ? '/' . $model->id() . '/' : '/'
]),
'template' => Field::hidden()
];
// only show template field if > 1 templates available
// or when in debug mode
if (count($blueprints) > 1 || $kirby->option('debug') === true) {
$fields['template'] = Field::template($blueprints, [
'required' => true
]);
}
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => $fields,
'submitButton' => I18n::translate('page.draft.create'),
'value' => [
'parent' => $parent,
'slug' => '',
'template' => $template,
'title' => '',
]
]
];
return $dialog->load();
},
'submit' => function () {
$request = App::instance()->request();
$title = trim($request->get('title', ''));
$dialog = new PageCreateDialog(
parentId: $request->get('parent'),
sectionId: $request->get('section'),
slug: $request->get('slug'),
template: $request->get('template'),
title: $request->get('title'),
viewId: $request->get('view'),
);
if (Str::length($title) === 0) {
throw new InvalidArgumentException([
'key' => 'page.changeTitle.empty'
]);
}
$page = Find::parent($request->get('parent', 'site'))->createChild([
'content' => ['title' => $title],
'slug' => $request->get('slug'),
'template' => $request->get('template'),
]);
return [
'event' => 'page.create',
'redirect' => $page->panel()->url(true)
];
return $dialog->submit($request->get());
}
],
@@ -529,6 +469,13 @@ return [
}
],
// page field dialogs
'page.fields' => [
'pattern' => '(pages/.*?)/fields/(:any)/(:all?)',
'load' => $fields['model']['load'],
'submit' => $fields['model']['submit']
],
// change filename
'page.file.changeName' => [
'pattern' => '(pages/.*?)/files/(:any)/changeName',
@@ -543,6 +490,13 @@ return [
'submit' => $files['changeSort']['submit'],
],
// change template
'page.file.changeTemplate' => [
'pattern' => '(pages/.*?)/files/(:any)/changeTemplate',
'load' => $files['changeTemplate']['load'],
'submit' => $files['changeTemplate']['submit'],
],
// delete
'page.file.delete' => [
'pattern' => '(pages/.*?)/files/(:any)/delete',
@@ -550,6 +504,56 @@ return [
'submit' => $files['delete']['submit'],
],
// page file field dialogs
'page.file.fields' => [
'pattern' => '(pages/.*?)/files/(:any)/fields/(:any)/(:all?)',
'load' => $fields['file']['load'],
'submit' => $fields['file']['submit'],
],
// move page
'page.move' => [
'pattern' => 'pages/(:any)/move',
'load' => function (string $id) {
$page = Find::page($id);
$parent = $page->parentModel();
if (Uuids::enabled() === false) {
$parentId = $parent?->id() ?? '/';
} else {
$parentId = $parent?->uuid()->toString() ?? 'site://';
}
return [
'component' => 'k-page-move-dialog',
'props' => [
'value' => [
'move' => $page->panel()->url(true),
'parent' => $parentId
]
]
];
},
'submit' => function (string $id) {
$kirby = App::instance();
$parentId = $kirby->request()->get('parent');
$parent = (empty($parentId) === true || $parentId === '/' || $parentId === 'site://') ? $kirby->site() : Find::page($parentId);
$oldPage = Find::page($id);
$newPage = $oldPage->move($parent);
return [
'event' => 'page.move',
'redirect' => $newPage->panel()->url(true),
'dispatch' => [
'content/move' => [
$oldPage->panel()->url(true),
$newPage->panel()->url(true)
]
],
];
}
],
// change site title
'site.changeTitle' => [
'pattern' => 'site/changeTitle',
@@ -572,14 +576,21 @@ return [
},
'submit' => function () {
$kirby = App::instance();
$kirby->site()->changeTitle($kirby->request()->get('title'));
return [
'event' => 'site.changeTitle',
];
}
],
// site field dialogs
'site.fields' => [
'pattern' => '(site)/fields/(:any)/(:all?)',
'load' => $fields['model']['load'],
'submit' => $fields['model']['submit'],
],
// change filename
'site.file.changeName' => [
'pattern' => '(site)/files/(:any)/changeName',
@@ -594,6 +605,13 @@ return [
'submit' => $files['changeSort']['submit'],
],
// change template
'site.file.changeTemplate' => [
'pattern' => '(site)/files/(:any)/changeTemplate',
'load' => $files['changeTemplate']['load'],
'submit' => $files['changeTemplate']['submit'],
],
// delete
'site.file.delete' => [
'pattern' => '(site)/files/(:any)/delete',
@@ -601,4 +619,24 @@ return [
'submit' => $files['delete']['submit'],
],
// site file field dialogs
'site.file.fields' => [
'pattern' => '(site)/files/(:any)/fields/(:any)/(:all?)',
'load' => $fields['file']['load'],
'submit' => $fields['file']['submit'],
],
// content changes
'changes' => [
'pattern' => 'changes',
'load' => function () {
$dialog = new ChangesDialog();
return $dialog->load();
},
'submit' => function () {
$dialog = new ChangesDialog();
$ids = App::instance()->request()->get('ids');
return $dialog->submit($ids);
}
],
];

View File

@@ -0,0 +1,33 @@
<?php
$fields = require __DIR__ . '/../fields/drawers.php';
return [
// page field drawers
'page.fields' => [
'pattern' => '(pages/.*?)/fields/(:any)/(:all?)',
'load' => $fields['model']['load'],
'submit' => $fields['model']['submit']
],
// page file field drawers
'page.file.fields' => [
'pattern' => '(pages/.*?)/files/(:any)/fields/(:any)/(:all?)',
'load' => $fields['file']['load'],
'submit' => $fields['file']['submit'],
],
// site field drawers
'site.fields' => [
'pattern' => '(site)/fields/(:any)/(:all?)',
'load' => $fields['model']['load'],
'submit' => $fields['model']['submit'],
],
// site file field drawers
'site.file.fields' => [
'pattern' => '(site)/files/(:any)/fields/(:any)/(:all?)',
'load' => $fields['file']['load'],
'submit' => $fields['file']['submit'],
],
];

View File

@@ -1,14 +1,9 @@
<?php
use Kirby\Panel\Dropdown;
$files = require __DIR__ . '/../files/dropdowns.php';
return [
'changes' => [
'pattern' => 'changes',
'options' => fn () => Dropdown::changes()
],
'page' => [
'pattern' => 'pages/(:any)',
'options' => function (string $path) {

View File

@@ -0,0 +1,66 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Toolkit\I18n;
return [
'tree' => [
'pattern' => 'site/tree',
'action' => function () {
$kirby = App::instance();
$request = $kirby->request();
$move = $request->get('move');
$move = $move ? Find::parent($move) : null;
$parent = $request->get('parent');
if ($parent === null) {
$site = $kirby->site();
$panel = $site->panel();
$uuid = $site->uuid()?->toString();
$url = $site->url();
$value = $uuid ?? '/';
return [
[
'children' => $panel->url(true),
'disabled' => $move?->isMovableTo($site) === false,
'hasChildren' => true,
'icon' => 'home',
'id' => '/',
'label' => I18n::translate('view.site'),
'open' => false,
'url' => $url,
'uuid' => $uuid,
'value' => $value
]
];
}
$parent = Find::parent($parent);
$pages = [];
foreach ($parent->childrenAndDrafts()->filterBy('isListable', true) as $child) {
$panel = $child->panel();
$uuid = $child->uuid()?->toString();
$url = $child->url();
$value = $uuid ?? $child->id();
$pages[] = [
'children' => $panel->url(true),
'disabled' => $move?->isMovableTo($child) === false,
'hasChildren' => $child->hasChildren() === true || $child->hasDrafts() === true,
'icon' => $panel->image()['icon'] ?? null,
'id' => $child->id(),
'open' => false,
'label' => $child->title()->value(),
'url' => $url,
'uuid' => $uuid,
'value' => $value
];
}
return $pages;
}
]
];

View File

@@ -8,50 +8,49 @@ return [
'pages' => [
'label' => I18n::translate('pages'),
'icon' => 'page',
'query' => function (string $query = null) {
$pages = App::instance()->site()
'query' => function (string $query = null, int $limit, int $page) {
$kirby = App::instance();
$pages = $kirby->site()
->index(true)
->search($query)
->filter('isReadable', true)
->limit(10);
->filter('isListable', true)
->paginate($limit, $page);
$results = [];
foreach ($pages as $page) {
$results[] = [
return [
'results' => $pages->values(fn ($page) => [
'image' => $page->panel()->image(),
'text' => Escape::html($page->title()->value()),
'link' => $page->panel()->url(true),
'info' => Escape::html($page->id())
];
}
return $results;
'info' => Escape::html($page->id()),
'uuid' => $page->uuid()->toString(),
]),
'pagination' => $pages->pagination()->toArray()
];
}
],
'files' => [
'label' => I18n::translate('files'),
'icon' => 'image',
'query' => function (string $query = null) {
$files = App::instance()->site()
'query' => function (string $query = null, int $limit, int $page) {
$kirby = App::instance();
$files = $kirby->site()
->index(true)
->filter('isReadable', true)
->filter('isListable', true)
->files()
->filter('isListable', true)
->search($query)
->limit(10);
->paginate($limit, $page);
$results = [];
foreach ($files as $file) {
$results[] = [
return [
'results' => $files->values(fn ($file) => [
'image' => $file->panel()->image(),
'text' => Escape::html($file->filename()),
'link' => $file->panel()->url(true),
'info' => Escape::html($file->id())
];
}
return $results;
'info' => Escape::html($file->id()),
'uuid' => $file->uuid()->toString(),
]),
'pagination' => $files->pagination()->toArray()
];
}
]
];

View File

@@ -1,6 +1,7 @@
<?php
use Kirby\Cms\App;
use Kirby\Exception\LogicException;
use Kirby\Panel\Field;
use Kirby\Toolkit\I18n;
@@ -8,64 +9,85 @@ return [
// license key
'license' => [
'load' => function () {
$license = App::instance()->system()->license();
// @codeCoverageIgnoreStart
// the system is registered but the license
// key is only visible for admins
if ($license === true) {
$license = 'Kirby 3';
}
// @codeCoverageIgnoreEnd
$kirby = App::instance();
$license = $kirby->system()->license();
$obfuscated = $kirby->user()->isAdmin() === false;
$status = $license->status();
$renewable = $status->renewable();
return [
'component' => 'k-form-dialog',
'component' => 'k-license-dialog',
'props' => [
'size' => 'medium',
'fields' => [
'license' => [
'type' => 'info',
'label' => I18n::translate('license'),
'text' => $license ? $license : I18n::translate('license.unregistered.label'),
'theme' => $license ? 'code' : 'negative',
'help' => $license ?
// @codeCoverageIgnoreStart
'<a href="https://hub.getkirby.com">' . I18n::translate('license.manage') . ' &rarr;</a>' :
// @codeCoverageIgnoreEnd
'<a href="https://getkirby.com/buy">' . I18n::translate('license.buy') . ' &rarr;</a>'
]
'license' => [
'code' => $license->code($obfuscated),
'icon' => $status->icon(),
'info' => $status->info($license->renewal('Y-m-d')),
'theme' => $status->theme(),
'type' => $license->label(),
],
'submitButton' => false,
'cancelButton' => false,
'cancelButton' => $renewable,
'submitButton' => $renewable ? [
'icon' => 'refresh',
'text' => I18n::translate('renew'),
'theme' => 'love',
] : false,
]
];
},
'submit' => function () {
// @codeCoverageIgnoreStart
$response = App::instance()->system()->license()->upgrade();
// the upgrade is still needed
if ($response['status'] === 'upgrade') {
return [
'redirect' => $response['url']
];
}
// the upgrade has already been completed
if ($response['status'] === 'complete') {
return [
'event' => 'system.renew',
'message' => I18n::translate('license.success')
];
}
throw new LogicException('The upgrade failed');
// @codeCoverageIgnoreEnd
}
],
// license registration
'registration' => [
'load' => function () {
$system = App::instance()->system();
$local = $system->isLocal();
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'domain' => [
'label' => I18n::translate('license.activate.label'),
'type' => 'info',
'theme' => $system->isLocal() ? 'notice' : 'info',
'text' => I18n::template('license.register.' . ($system->isLocal() ? 'local' : 'domain'), ['host' => $system->indexUrl()])
'theme' => $local ? 'warning' : 'info',
'text' => I18n::template('license.activate.' . ($local ? 'local' : 'domain'), ['host' => $system->indexUrl()])
],
'license' => [
'label' => I18n::translate('license.register.label'),
'label' => I18n::translate('license.code.label'),
'type' => 'text',
'required' => true,
'counter' => false,
'placeholder' => 'K3-',
'help' => I18n::translate('license.register.help')
'placeholder' => 'K-',
'help' => I18n::translate('license.code.help') . ' ' . '<a href="https://getkirby.com/buy" target="_blank">' . I18n::translate('license.buy') . ' &rarr;</a>'
],
'email' => Field::email(['required' => true])
],
'submitButton' => I18n::translate('license.register'),
'submitButton' => [
'icon' => 'key',
'text' => I18n::translate('activate'),
'theme' => 'love',
],
'value' => [
'license' => null,
'email' => null
@@ -83,7 +105,7 @@ return [
return [
'event' => 'system.register',
'message' => I18n::translate('license.register.success')
'message' => I18n::translate('license.success')
];
// @codeCoverageIgnoreEnd
}

View File

@@ -14,28 +14,29 @@ return [
$environment = [
[
'label' => $license ? I18n::translate('license') : I18n::translate('license.register.label'),
'value' => $license ? 'Kirby 3' : I18n::translate('license.unregistered.label'),
'theme' => $license ? null : 'negative',
'dialog' => $license ? 'license' : 'registration'
'label' => $license->status()->label(),
'value' => $license->label(),
'theme' => $license->status()->theme(),
'icon' => $license->status()->icon(),
'dialog' => $license->status()->dialog()
],
[
'label' => $updateStatus?->label() ?? I18n::translate('version'),
'value' => $kirby->version(),
'link' => (
$updateStatus ?
$updateStatus->url() :
'https://github.com/getkirby/kirby/releases/tag/' . $kirby->version()
),
'theme' => $updateStatus?->theme()
'link' => $updateStatus?->url() ??
'https://github.com/getkirby/kirby/releases/tag/' . $kirby->version(),
'theme' => $updateStatus?->theme(),
'icon' => $updateStatus?->icon() ?? 'info'
],
[
'label' => 'PHP',
'value' => phpversion()
'value' => phpversion(),
'icon' => 'code'
],
[
'label' => I18n::translate('server'),
'value' => $system->serverSoftware() ?? '?'
'value' => $system->serverSoftware() ?? '?',
'icon' => 'server'
]
];

View File

@@ -9,6 +9,7 @@ return function ($kirby) {
'search' => 'users',
'menu' => true,
'dialogs' => require __DIR__ . '/users/dialogs.php',
'drawers' => require __DIR__ . '/users/drawers.php',
'dropdowns' => require __DIR__ . '/users/dropdowns.php',
'searches' => require __DIR__ . '/users/searches.php',
'views' => require __DIR__ . '/users/views.php'

View File

@@ -6,9 +6,11 @@ use Kirby\Cms\UserRules;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Panel\Field;
use Kirby\Panel\Panel;
use Kirby\Panel\UserTotpDisableDialog;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\I18n;
$fields = require __DIR__ . '/../fields/dialogs.php';
$files = require __DIR__ . '/../files/dialogs.php';
return [
@@ -18,6 +20,12 @@ return [
'pattern' => 'users/create',
'load' => function () {
$kirby = App::instance();
// get default value for role
if ($role = $kirby->request()->get('role')) {
$role = $kirby->roles()->find($role)?->id();
}
return [
'component' => 'k-form-dialog',
'props' => [
@@ -41,7 +49,7 @@ return [
'email' => '',
'password' => '',
'translation' => $kirby->panelLanguage(),
'role' => $kirby->user()->role()->name()
'role' => $role ?? $kirby->user()->role()->name()
]
]
];
@@ -287,6 +295,13 @@ return [
}
],
// user field dialogs
'user.fields' => [
'pattern' => '(users/.*?)/fields/(:any)/(:all?)',
'load' => $fields['model']['load'],
'submit' => $fields['model']['submit']
],
// change file name
'user.file.changeName' => [
'pattern' => '(users/.*?)/files/(:any)/changeName',
@@ -301,11 +316,31 @@ return [
'submit' => $files['changeSort']['submit'],
],
// change file template
'user.file.changeTemplate' => [
'pattern' => '(users/.*?)/files/(:any)/changeTemplate',
'load' => $files['changeTemplate']['load'],
'submit' => $files['changeTemplate']['submit'],
],
// delete file
'user.file.delete' => [
'pattern' => '(users/.*?)/files/(:any)/delete',
'load' => $files['delete']['load'],
'submit' => $files['delete']['submit'],
]
],
// user file fields dialogs
'user.file.fields' => [
'pattern' => '(users/.*?)/files/(:any)/fields/(:any)/(:all?)',
'load' => $fields['file']['load'],
'submit' => $fields['file']['submit']
],
// user disable TOTP
'user.totp.disable' => [
'pattern' => 'users/(:any)/totp/disable',
'load' => fn (string $id) => (new UserTotpDisableDialog($id))->load(),
'submit' => fn (string $id) => (new UserTotpDisableDialog($id))->submit()
],
];

View File

@@ -0,0 +1,18 @@
<?php
$fields = require __DIR__ . '/../fields/drawers.php';
return [
// user field drawers
'user.fields' => [
'pattern' => '(users/.*?)/fields/(:any)/(:all?)',
'load' => $fields['model']['load'],
'submit' => $fields['model']['submit']
],
// user file fields drawers
'user.file.fields' => [
'pattern' => '(users/.*?)/files/(:any)/fields/(:any)/(:all?)',
'load' => $fields['file']['load'],
'submit' => $fields['file']['submit']
],
];

View File

@@ -8,20 +8,22 @@ return [
'users' => [
'label' => I18n::translate('users'),
'icon' => 'users',
'query' => function (string $query = null) {
$users = App::instance()->users()->search($query)->limit(10);
$results = [];
'query' => function (string $query = null, int $limit, int $page) {
$kirby = App::instance();
$users = $kirby->users()
->search($query)
->paginate($limit, $page);
foreach ($users as $user) {
$results[] = [
return [
'results' => $users->values(fn ($user) => [
'image' => $user->panel()->image(),
'text' => Escape::html($user->username()),
'link' => $user->panel()->url(true),
'info' => Escape::html($user->role()->title())
];
}
return $results;
'info' => Escape::html($user->role()->title()),
'uuid' => $user->uuid()->toString(),
]),
'pagination' => $users->pagination()->toArray()
];
}
]
];

View File

@@ -31,6 +31,10 @@ return [
$users = $users->role($role);
}
// sort users alphabetically
$users = $users->sortBy('username', 'asc');
// paginate
$users = $users->paginate([
'limit' => 20,
'page' => $kirby->request()->get('page')

View File

@@ -8,7 +8,7 @@ fields:
query: model.images
multiple: true
layout: cards
size: tiny
size: small
empty: field.blocks.gallery.images.empty
uploads:
template: blocks/image

View File

@@ -5,20 +5,31 @@ preview: heading
fields:
level:
label: field.blocks.heading.level
type: select
type: toggles
empty: false
default: "h2"
width: 1/6
labels: false
options:
- h1
- h2
- h3
- h4
- h5
- h6
- value: h1
icon: h1
text: H1
- value: h2
icon: h2
text: H2
- value: h3
icon: h3
text: H3
- value: h4
icon: h4
text: H4
- value: h5
icon: h5
text: H5
- value: h6
icon: h6
text: H6
text:
label: field.blocks.heading.text
type: writer
inline: true
width: 5/6
placeholder: field.blocks.heading.placeholder

View File

@@ -8,8 +8,8 @@ fields:
columns: 2
default: "kirby"
options:
kirby: Kirby
web: Web
kirby: "{{ t('field.blocks.image.location.internal') }}"
web: "{{ t('field.blocks.image.location.external') }}"
image:
label: field.blocks.image.name
type: files

View File

@@ -1,56 +0,0 @@
name: Code
icon: code
fields:
code:
label: Code
type: textarea
buttons: false
font: monospace
language:
label: Language
type: select
default: text
options:
bash: Bash
basic: BASIC
c: C
clojure: Clojure
cpp: C++
csharp: C#
css: CSS
diff: Diff
elixir: Elixir
elm: Elm
erlang: Erlang
go: Go
graphql: GraphQL
haskell: Haskell
html: HTML
java: Java
js: JavaScript
json: JSON
latext: LaTeX
less: Less
lisp: Lisp
lua: Lua
makefile: Makefile
markdown: Markdown
markup: Markup
objectivec: Objective-C
pascal: Pascal
perl: Perl
php: PHP
text: Plain Text
python: Python
r: R
ruby: Ruby
rust: Rust
sass: Sass
scss: SCSS
shell: Shell
sql: SQL
swift: Swift
typescript: TypeScript
vbnet: VB.net
xml: XML
yaml: YAML

View File

@@ -1,20 +0,0 @@
icon: title
fields:
text:
type: text
level:
type: select
width: 1/2
empty: false
default: "2"
options:
- value: "1"
text: Heading 1
- value: "2"
text: Heading 2
- value: "3"
text: Heading 3
id:
type: text
label: ID
width: 1/2

View File

@@ -1,16 +0,0 @@
name: Image
icon: image
fields:
image:
type: files
multiple: false
alt:
type: text
icon: title
caption:
type: writer
inline: true
icon: text
link:
type: text
icon: url

View File

@@ -1,12 +0,0 @@
name: Quote
icon: quote
fields:
text:
label: Quote Text
type: writer
inline: true
citation:
label: Citation
type: writer
inline: true
placeholder: by …

View File

@@ -1,25 +0,0 @@
name: Table
icon: menu
fields:
rows:
label: Menu
type: structure
columns:
dish: true
description: true
price:
before:
width: 1/4
align: right
fields:
dish:
label: Dish
type: text
description:
label: Description
type: text
price:
label: Price
type: number
before:
step: 0.01

View File

@@ -1,5 +0,0 @@
name: Text
icon: text
fields:
text:
type: writer

View File

@@ -1,8 +0,0 @@
name: Video
icon: video
label: "{{ url }}"
fields:
url:
type: url
caption:
type: writer

View File

@@ -1,2 +0,0 @@
name: File
title: file

View File

@@ -1,3 +0,0 @@
name: Page
title: Page

View File

@@ -1,7 +0,0 @@
name: Site
title: Site
sections:
pages:
headline: Pages
type: pages

View File

@@ -8,6 +8,7 @@ use Kirby\Cms\Page;
use Kirby\Cms\User;
use Kirby\Data\Data;
use Kirby\Email\PHPMailer as Emailer;
use Kirby\Exception\NotFoundException;
use Kirby\Filesystem\F;
use Kirby\Filesystem\Filename;
use Kirby\Http\Uri;
@@ -19,13 +20,13 @@ use Kirby\Text\Markdown;
use Kirby\Text\SmartyPants;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Str;
use Kirby\Uuid\Uuid;
return [
/**
* Used by the `css()` helper
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string $url Relative or absolute URL
* @param string|array $options An array of attributes for the link tag or a media attribute string
*/
@@ -33,35 +34,39 @@ return [
/**
* Add your own email provider
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param array $props
* @param bool $debug
*/
'email' => function (App $kirby, array $props = [], bool $debug = false) {
'email' => function (
App $kirby,
array $props = [],
bool $debug = false
) {
return new Emailer($props, $debug);
},
/**
* Modify URLs for file objects
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param \Kirby\Cms\File $file The original file object
* @return string
*/
'file::url' => function (App $kirby, File $file): string {
'file::url' => function (
App $kirby,
File $file
): string {
return $file->mediaUrl();
},
/**
* Adapt file characteristics
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param \Kirby\Cms\File|\Kirby\Filesystem\Asset $file The file object
* @param array $options All thumb options (width, height, crop, blur, grayscale)
* @return \Kirby\Cms\File|\Kirby\Cms\FileVersion|\Kirby\Filesystem\Asset
*/
'file::version' => function (App $kirby, $file, array $options = []) {
'file::version' => function (
App $kirby,
$file,
array $options = []
) {
// if file is not resizable, return
if ($file->isResizable() === false) {
return $file;
@@ -100,7 +105,6 @@ return [
/**
* Used by the `js()` helper
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string $url Relative or absolute URL
* @param string|array $options An array of attributes for the link tag or a media attribute string
*/
@@ -109,10 +113,8 @@ return [
/**
* Add your own Markdown parser
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string $text Text to parse
* @param array $options Markdown options
* @return string
*/
'markdown' => function (
App $kirby,
@@ -140,9 +142,9 @@ return [
'search' => function (
App $kirby,
Collection $collection,
string|null $query = '',
array|string $params = []
): Collection|bool {
string|null $query = null,
string|array $params = []
): Collection {
if (is_string($params) === true) {
$params = ['fields' => Str::split($params, '|')];
}
@@ -154,8 +156,9 @@ return [
'words' => false,
];
$options = array_merge($defaults, $params);
$query = trim($query ?? '');
$collection = clone $collection;
$options = array_merge($defaults, $params);
$query = trim($query ?? '');
// empty or too short search query
if (Str::length($query) < $options['minlength']) {
@@ -262,12 +265,14 @@ return [
/**
* Add your own SmartyPants parser
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string $text Text to parse
* @param array $options SmartyPants options
* @return string
*/
'smartypants' => function (App $kirby, string $text = null, array $options = []): string {
'smartypants' => function (
App $kirby,
string $text = null,
array $options = []
): string {
static $smartypants;
static $config;
@@ -284,43 +289,55 @@ return [
/**
* Add your own snippet loader
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string|array $name Snippet name
* @param array $data Data array for the snippet
*/
'snippet' => function (App $kirby, string|array|null $name, array $data = [], bool $slots = false): Snippet|string {
'snippet' => function (
App $kirby,
string|array|null $name,
array $data = [],
bool $slots = false
): Snippet|string {
return Snippet::factory($name, $data, $slots);
},
/**
* Add your own template engine
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string $name Template name
* @param string $type Extension type
* @param string $defaultType Default extension type
* @return \Kirby\Template\Template
*/
'template' => function (App $kirby, string $name, string $type = 'html', string $defaultType = 'html') {
'template' => function (
App $kirby,
string $name,
string $type = 'html',
string $defaultType = 'html'
) {
return new Template($name, $type, $defaultType);
},
/**
* Add your own thumb generator
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string $src Root of the original file
* @param string $dst Template string for the root to the desired destination
* @param array $options All thumb options that should be applied: `width`, `height`, `crop`, `blur`, `grayscale`
* @return string
*/
'thumb' => function (App $kirby, string $src, string $dst, array $options): string {
'thumb' => function (
App $kirby,
string $src,
string $dst,
array $options
): string {
$darkroom = Darkroom::factory(
$kirby->option('thumbs.driver', 'gd'),
$kirby->option('thumbs', [])
);
$options = $darkroom->preprocess($src, $options);
$root = (new Filename($src, $dst, $options))->toString();
$options = $darkroom->preprocess($src, $options);
$root = (new Filename($src, $dst, $options))->toString();
F::copy($src, $root, true);
$darkroom->process($root, $options);
@@ -331,12 +348,15 @@ return [
/**
* Modify all URLs
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string|null $path URL path
* @param array|string|null $options Array of options for the Uri class
* @return string
* @throws \Kirby\Exception\NotFoundException If an invalid UUID was passed
*/
'url' => function (App $kirby, string $path = null, $options = null): string {
'url' => function (
App $kirby,
string $path = null,
$options = null
): string {
$language = null;
// get language from simple string option
@@ -378,6 +398,23 @@ return [
return $path;
}
// support UUIDs
if (
$path !== null &&
(
Uuid::is($path, 'page') === true ||
Uuid::is($path, 'file') === true
)
) {
$model = Uuid::for($path)->model();
if ($model === null) {
throw new NotFoundException('The model could not be found for "' . $path . '" uuid');
}
$path = $model->url();
}
$url = Url::makeAbsolute($path, $kirby->url());
if ($options === null) {

View File

@@ -0,0 +1,98 @@
<?php
use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Str;
return [
'props' => [
/**
* Unset inherited props
*/
'after' => null,
'before' => null,
/**
* Whether to allow alpha transparency in the color
*/
'alpha' => function (bool $alpha = false) {
return $alpha;
},
/**
* The CSS format (hex, rgb, hsl) to display and store the value
*/
'format' => function (string $format = 'hex'): string {
if (in_array($format, ['hex', 'hsl', 'rgb']) === false) {
throw new InvalidArgumentException('Unsupported format for color field (supported: hex, rgb, hsl)');
}
return $format;
},
/**
* Change mode to disable the color picker (`input`) or to only
* show the `options` as toggles
*/
'mode' => function (string $mode = 'picker'): string {
if (in_array($mode, ['picker', 'input', 'options']) === false) {
throw new InvalidArgumentException('Unsupported mode for color field (supported: picker, input, options)');
}
return $mode;
},
/**
* List of colors that will be shown as buttons
* to directly select them
*/
'options' => function (array $options = []): array {
return $options;
}
],
'computed' => [
'default' => function (): string {
return Str::lower($this->default);
},
'options' => function (): array {
return A::map(array_keys($this->options), fn ($key) => [
'value' => $this->options[$key],
'text' => is_string($key) ? $key : null
]);
}
],
'validations' => [
'color' => function ($value) {
if (empty($value) === true) {
return true;
}
if (
$this->format === 'hex' &&
preg_match('/^#([\da-f]{3,4}){1,2}$/i', $value) !== 1
) {
throw new InvalidArgumentException([
'key' => 'validation.color',
'data' => ['format' => 'hex']
]);
}
if (
$this->format === 'rgb' &&
preg_match('/^rgba?\(\s*(\d{1,3})(%?)(?:,|\s)+(\d{1,3})(%?)(?:,|\s)+(\d{1,3})(%?)(?:,|\s|\/)*(\d*(?:\.\d+)?)(%?)\s*\)?$/i', $value) !== 1
) {
throw new InvalidArgumentException([
'key' => 'validation.color',
'data' => ['format' => 'rgb']
]);
}
if (
$this->format === 'hsl' &&
preg_match('/^hsla?\(\s*(\d{1,3}\.?\d*)(deg|rad|grad|turn)?(?:,|\s)+(\d{1,3})%(?:,|\s)+(\d{1,3})%(?:,|\s|\/)*(\d*(?:\.\d+)?)(%?)\s*\)?$/i', $value) !== 1
) {
throw new InvalidArgumentException([
'key' => 'validation.color',
'data' => ['format' => 'hsl']
]);
}
}
]
];

View File

@@ -1,5 +1,6 @@
<?php
use Kirby\Cms\ModelWithContent;
use Kirby\Data\Data;
use Kirby\Toolkit\A;
@@ -36,7 +37,10 @@ return [
'parentModel' => function () {
if (
is_string($this->parent) === true &&
$model = $this->model()->query($this->parent, 'Kirby\Cms\Model')
$model = $this->model()->query(
$this->parent,
ModelWithContent::class
)
) {
return $model;
}

View File

@@ -14,13 +14,6 @@ return [
'icon' => null,
'placeholder' => null,
'required' => null,
'translate' => null,
/**
* If `false`, the prepended number will be hidden
*/
'numbered' => function (bool $numbered = true) {
return $numbered;
}
'translate' => null
]
];

View File

@@ -1,3 +1,5 @@
<?php
return [];
return [
'hidden' => true
];

View File

@@ -12,7 +12,6 @@ return [
'before' => null,
'default' => null,
'disabled' => null,
'icon' => null,
'placeholder' => null,
'required' => null,
'translate' => null,

View File

@@ -0,0 +1,156 @@
<?php
use Kirby\Exception\InvalidArgumentException;
use Kirby\Http\Url;
use Kirby\Toolkit\Str;
use Kirby\Toolkit\V;
return [
'props' => [
'after' => null,
'before' => null,
'icon' => null,
'placeholder' => null,
/**
* @values 'anchor', 'url, 'page, 'file', 'email', 'tel', 'custom'
*/
'options' => function (array|null $options = null): array {
return $options ?? [
'url',
'page',
'file',
'email',
'tel',
'anchor',
'custom'
];
},
'value' => function (string|null $value = null) {
return $value ?? '';
}
],
'methods' => [
'activeTypes' => function () {
return array_filter($this->availableTypes(), function (string $type) {
return in_array($type, $this->props['options']) === true;
}, ARRAY_FILTER_USE_KEY);
},
'availableTypes' => function () {
return [
'anchor' => [
'detect' => function (string $value): bool {
return Str::startsWith($value, '#') === true;
},
'link' => function (string $value): string {
return $value;
},
'validate' => function (string $value): bool {
return Str::startsWith($value, '#') === true;
},
],
'email' => [
'detect' => function (string $value): bool {
return Str::startsWith($value, 'mailto:') === true;
},
'link' => function (string $value): string {
return str_replace('mailto:', '', $value);
},
'validate' => function (string $value): bool {
return V::email($value);
},
],
'file' => [
'detect' => function (string $value): bool {
return Str::startsWith($value, 'file://') === true;
},
'link' => function (string $value): string {
return $value;
},
'validate' => function (string $value): bool {
return V::uuid($value, 'file');
},
],
'page' => [
'detect' => function (string $value): bool {
return Str::startsWith($value, 'page://') === true;
},
'link' => function (string $value): string {
return $value;
},
'validate' => function (string $value): bool {
return V::uuid($value, 'page');
},
],
'tel' => [
'detect' => function (string $value): bool {
return Str::startsWith($value, 'tel:') === true;
},
'link' => function (string $value): string {
return str_replace('tel:', '', $value);
},
'validate' => function (string $value): bool {
return V::tel($value);
},
],
'url' => [
'detect' => function (string $value): bool {
return Str::startsWith($value, 'http://') === true || Str::startsWith($value, 'https://') === true;
},
'link' => function (string $value): string {
return $value;
},
'validate' => function (string $value): bool {
return V::url($value);
},
],
// needs to come last
'custom' => [
'detect' => function (string $value): bool {
return true;
},
'link' => function (string $value): string {
return $value;
},
'validate' => function (): bool {
return true;
},
]
];
},
],
'validations' => [
'value' => function (string|null $value) {
if (empty($value) === true) {
return true;
}
$detected = false;
foreach ($this->activeTypes() as $type => $options) {
if ($options['detect']($value) !== true) {
continue;
}
$link = $options['link']($value);
$detected = true;
if ($options['validate']($link) === false) {
throw new InvalidArgumentException([
'key' => 'validation.' . $type
]);
}
}
// none of the configured types has been detected
if ($detected === false) {
throw new InvalidArgumentException([
'key' => 'validation.linkType'
]);
}
return true;
},
]
];

View File

@@ -7,6 +7,12 @@ return [
*/
'marks' => function ($marks = true) {
return $marks;
},
/**
* Sets the allowed nodes. Available nodes: `bulletList`, `orderedList`
*/
'nodes' => function ($nodes = null) {
return $nodes;
}
],
'computed' => [

View File

@@ -12,7 +12,7 @@ return [
},
/**
* Layout size for cards: `tiny`, `small`, `medium`, `large` or `huge`
* Layout size for cards: `tiny`, `small`, `medium`, `large`, `huge`, `full`
*/
'size' => function (string $size = 'auto') {
return $size;

View File

@@ -36,7 +36,7 @@ return [
},
'sanitizeOption' => function ($value) {
$options = array_column($this->options(), 'value');
return in_array($value, $options, true) === true ? $value : null;
return in_array($value, $options) === true ? $value : null;
},
'sanitizeOptions' => function ($values) {
$options = array_column($this->options(), 'value');

View File

@@ -3,6 +3,7 @@
use Kirby\Cms\Api;
use Kirby\Cms\File;
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
return [
'props' => [
@@ -22,18 +23,23 @@ return [
$uploads = [];
}
$template = $uploads['template'] ?? null;
$uploads['accept'] = '*';
if ($template = $uploads['template'] ?? null) {
// get parent object for upload target
$parent = $this->uploadParent($uploads['parent'] ?? null);
if ($parent === null) {
throw new InvalidArgumentException('"' . $uploads['parent'] . '" could not be resolved as a valid parent for the upload');
}
if ($template) {
$file = new File([
'filename' => 'tmp',
'parent' => $this->model(),
'parent' => $parent,
'template' => $template
]);
$uploads['accept'] = $file->blueprint()->acceptMime();
} else {
$uploads['accept'] = '*';
}
return $uploads;
@@ -45,15 +51,7 @@ return [
throw new Exception('Uploads are disabled for this field');
}
if ($parentQuery = ($params['parent'] ?? null)) {
$parent = $this->model()->query($parentQuery);
} else {
$parent = $this->model();
}
if ($parent instanceof File) {
$parent = $parent->parent();
}
$parent = $this->uploadParent($params['parent'] ?? null);
return $api->upload(function ($source, $filename) use ($parent, $params, $map) {
$props = [
@@ -71,6 +69,19 @@ return [
return $map($file, $parent);
});
},
'uploadParent' => function (string $parentQuery = null) {
$parent = $this->model();
if ($parentQuery) {
$parent = $parent->query($parentQuery);
}
if ($parent instanceof File) {
$parent = $parent->parent();
}
return $parent;
}
]
];

View File

@@ -1,35 +1,23 @@
<?php
use Kirby\Toolkit\Str;
use Kirby\Toolkit\V;
return [
'extends' => 'tags',
'props' => [
/**
* Unset inherited props
* If set to `all`, any type of input is accepted. If set to `options` only the predefined options are accepted as input.
*/
'accept' => null,
'accept' => function ($value = 'options') {
return V::in($value, ['all', 'options']) ? $value : 'all';
},
/**
* Custom icon to replace the arrow down.
*/
'icon' => function (string $icon = null) {
'icon' => function (string $icon = 'checklist') {
return $icon;
},
/**
* Enable/disable the search in the dropdown
* Also limit displayed items (display: 20)
* and set minimum number of characters to search (min: 3)
*/
'search' => function ($search = true) {
return $search;
},
/**
* If `true`, selected entries will be sorted
* according to their position in the dropdown
*/
'sort' => function (bool $sort = false) {
return $sort;
},
],
'methods' => [
'toValues' => function ($value) {

View File

@@ -47,7 +47,7 @@ return [
},
'fields' => function () {
if (empty($this->fields) === true) {
throw new Exception('Please provide some fields for the object');
return [];
}
return $this->form()->fields()->toArray();

View File

@@ -1,8 +1,11 @@
<?php
use Kirby\Data\Data;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Form\Form;
use Kirby\Toolkit\A;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\Str;
return [
'mixins' => ['min'],
@@ -49,7 +52,7 @@ return [
/**
* Fields setup for the structure form. Works just like fields in regular forms.
*/
'fields' => function (array $fields) {
'fields' => function (array $fields = []) {
return $fields;
},
/**
@@ -99,57 +102,54 @@ return [
},
'fields' => function () {
if (empty($this->fields) === true) {
throw new Exception('Please provide some fields for the structure');
return [];
}
return $this->form()->fields()->toArray();
},
'columns' => function () {
$columns = [];
$mobile = 0;
$columns = [];
$blueprint = $this->columns;
if (empty($this->columns) === true) {
foreach ($this->fields as $field) {
// Skip hidden and unsaveable fields
// They should never be included as column
if ($field['type'] === 'hidden' || $field['saveable'] === false) {
continue;
}
// if no custom columns have been defined,
// gather all fields as columns
if (empty($blueprint) === true) {
// skip hidden fields
$fields = array_filter(
$this->fields,
fn ($field) =>
$field['type'] !== 'hidden' && $field['hidden'] !== true
);
$fields = array_column($fields, 'name');
$blueprint = array_fill_keys($fields, true);
}
$columns[$field['name']] = [
'type' => $field['type'],
'label' => $field['label'] ?? $field['name']
];
foreach ($blueprint as $name => $column) {
$field = $this->fields[$name] ?? null;
// Skip empty and unsaveable fields
// They should never be included as column
if (
empty($field) === true ||
$field['saveable'] === false
) {
continue;
}
} else {
foreach ($this->columns as $columnName => $columnProps) {
if (is_array($columnProps) === false) {
$columnProps = [];
}
$field = $this->fields[$columnName] ?? null;
if (
empty($field) === true ||
$field['saveable'] === false
) {
continue;
}
if (($columnProps['mobile'] ?? false) === true) {
$mobile++;
}
$columns[$columnName] = array_merge([
'type' => $field['type'],
'label' => $field['label'] ?? $field['name']
], $columnProps);
if (is_array($column) === false) {
$column = [];
}
$column['type'] ??= $field['type'];
$column['label'] ??= $field['label'] ?? $name;
$column['label'] = I18n::translate($column['label'], $column['label']);
$columns[$name] = $column;
}
// make the first column visible on mobile
// if no other mobile columns are defined
if ($mobile === 0) {
if (in_array(true, array_column($columns, 'mobile')) === false) {
$columns[array_key_first($columns)]['mobile'] = true;
}
@@ -179,28 +179,47 @@ return [
]);
},
],
'api' => function () {
return [
[
'pattern' => 'validate',
'method' => 'ALL',
'action' => function () {
return array_values($this->field()->form($this->requestBody())->errors());
}
]
];
},
'save' => function ($value) {
$data = [];
foreach ($value as $row) {
$data[] = $this->form($row)->content();
$row = $this->form($row)->content();
// remove frontend helper id
unset($row['_id']);
$data[] = $row;
}
return $data;
},
'validations' => [
'min',
'max'
'max',
'structure' => function ($value) {
if (empty($value) === true) {
return true;
}
$values = A::wrap($value);
foreach ($values as $index => $value) {
$form = $this->form($value);
foreach ($form->fields() as $field) {
$errors = $field->errors();
if (empty($errors) === false) {
throw new InvalidArgumentException([
'key' => 'structure.validation',
'data' => [
'field' => $field->label() ?? Str::ucfirst($field->name()),
'index' => $index + 1
]
]);
}
}
}
}
]
];

View File

@@ -46,12 +46,27 @@ return [
'max' => function (int $max = null) {
return $max;
},
/**
* Enable/disable the search in the dropdown
* Also limit displayed items (display: 20)
* and set minimum number of characters to search (min: 3)
*/
'search' => function (bool|array $search = true) {
return $search;
},
/**
* Custom tags separator, which will be used to store tags in the content file
*/
'separator' => function (string $separator = ',') {
return $separator;
},
/**
* If `true`, selected entries will be sorted
* according to their position in the dropdown
*/
'sort' => function (bool $sort = false) {
return $sort;
},
],
'computed' => [
'default' => function (): array {

View File

@@ -27,6 +27,13 @@ return [
return $counter;
},
/**
* Sets the font family (sans or monospace)
*/
'font' => function (string $font = null) {
return $font === 'monospace' ? 'monospace' : 'sans-serif';
},
/**
* Maximum number of allowed characters
*/

View File

@@ -24,18 +24,8 @@ return [
/**
* Default selected user(s) when a new page/file/user is created
*/
'default' => function ($default = null) {
if ($default === false) {
return [];
}
if ($default === null && $user = $this->kirby()->user()) {
return [
$this->userResponse($user)
];
}
return $this->toUsers($default);
'default' => function (string|array|bool|null $default = null) {
return $default;
},
'value' => function ($value = null) {
@@ -43,10 +33,22 @@ return [
},
],
'computed' => [
/**
* Unset inherited computed
*/
'default' => null
'default' => function (): array {
if ($this->default === false) {
return [];
}
if (
$this->default === true &&
$user = $this->kirby()->user()
) {
return [
$this->userResponse($user)
];
}
return $this->toUsers($this->default);
}
],
'methods' => [
'userResponse' => function ($user) {
@@ -57,7 +59,7 @@ return [
'text' => $this->text,
]);
},
'toUsers' => function ($value = null) {
'toUsers' => function ($value = null): array {
$users = [];
$kirby = App::instance();

View File

@@ -1,9 +1,23 @@
<?php
use Kirby\Exception\InvalidArgumentException;
use Kirby\Sane\Sane;
use Kirby\Toolkit\V;
return [
'props' => [
/**
* Enables/disables the character counter in the top right corner
*/
'counter' => function (bool $counter = true) {
return $counter;
},
/**
* Available heading levels
*/
'headings' => function (array|null $headings = null) {
return array_intersect($headings ?? range(1, 6), range(1, 6));
},
/**
* Enables inline mode, which will not wrap new lines in paragraphs and creates hard breaks instead.
*
@@ -13,18 +27,37 @@ return [
return $inline;
},
/**
* Sets the allowed HTML formats. Available formats: `bold`, `italic`, `underline`, `strike`, `code`, `link`, `email`. Activate them all by passing `true`. Deactivate them all by passing `false`
* Sets the allowed HTML formats. Available formats: `bold`, `italic`, `underline`, `strike`, `code`, `link`, `email`. Activate/deactivate them all by passing `true`/`false`. Default marks are `bold`, `italic`, `underline`, `strike`, `link`, `email`
* @param array|bool $marks
*/
'marks' => function ($marks = true) {
'marks' => function ($marks = null) {
return $marks;
},
/**
* Sets the allowed nodes. Available nodes: `paragraph`, `heading`, `bulletList`, `orderedList`. Activate/deactivate them all by passing `true`/`false`. Default nodes are `paragraph`, `heading`, `bulletList`, `orderedList`.
* Maximum number of allowed characters
*/
'maxlength' => function (int $maxlength = null) {
return $maxlength;
},
/**
* Minimum number of required characters
*/
'minlength' => function (int $minlength = null) {
return $minlength;
},
/**
* Sets the allowed nodes. Available nodes: `paragraph`, `heading`, `bulletList`, `orderedList`, `quote`. Activate/deactivate them all by passing `true`/`false`. Default nodes are `paragraph`, `heading`, `bulletList`, `orderedList`.
* @param array|bool|null $nodes
*/
'nodes' => function ($nodes = null) {
return $nodes;
},
/**
* Toolbar options, incl. `marks` (to narrow down which marks should have toolbar buttons), `nodes` (to narrow down which nodes should have toolbar dropdown entries) and `inline` to set the position of the toolbar (false = sticking on top of the field)
*/
'toolbar' => function ($toolbar = null) {
return $toolbar;
}
],
'computed' => [
@@ -33,4 +66,28 @@ return [
return Sane::sanitize($value, 'html');
}
],
'validations' => [
'minlength' => function ($value) {
if (
$this->minlength &&
V::minLength(strip_tags($value), $this->minlength) === false
) {
throw new InvalidArgumentException([
'key' => 'validation.minlength',
'data' => ['min' => $this->minlength]
]);
}
},
'maxlength' => function ($value) {
if (
$this->maxlength &&
V::maxLength(strip_tags($value), $this->maxlength) === false
) {
throw new InvalidArgumentException([
'key' => 'validation.maxlength',
'data' => ['max' => $this->maxlength]
]);
}
},
]
];

View File

@@ -4,6 +4,7 @@ use Kirby\Cms\App;
use Kirby\Cms\File;
use Kirby\Cms\Helpers;
use Kirby\Cms\Html;
use Kirby\Cms\ModelWithContent;
use Kirby\Cms\Page;
use Kirby\Cms\Pages;
use Kirby\Cms\Response;
@@ -12,6 +13,7 @@ use Kirby\Cms\Url;
use Kirby\Filesystem\Asset;
use Kirby\Filesystem\F;
use Kirby\Http\Router;
use Kirby\Image\QrCode;
use Kirby\Template\Slot;
use Kirby\Template\Snippet;
use Kirby\Toolkit\Date;
@@ -195,10 +197,8 @@ if (Helpers::hasOverride('go') === false) { // @codeCoverageIgnore
/**
* Redirects to the given Urls
* Urls can be relative or absolute.
*
* @todo Change return type to `never` once support for PHP 8.0 is dropped
*/
function go(string $url = '/', int $code = 302): void
function go(string $url = '/', int $code = 302): never
{
Response::go($url, $code);
}
@@ -434,6 +434,20 @@ if (Helpers::hasOverride('params') === false) { // @codeCoverageIgnore
}
}
if (Helpers::hasOverride('qr') === false) { // @codeCoverageIgnore
/**
* Creates a QR code object
*/
function qr(string|ModelWithContent $data): QrCode
{
if ($data instanceof ModelWithContent) {
$data = $data->url();
}
return new QrCode($data);
}
}
if (Helpers::hasOverride('r') === false) { // @codeCoverageIgnore
/**
* Smart version of return with an if condition as first argument
@@ -588,25 +602,6 @@ if (Helpers::hasOverride('tt') === false) { // @codeCoverageIgnore
}
}
if (Helpers::hasOverride('twitter') === false) { // @codeCoverageIgnore
/**
* Builds a Twitter link
*/
function twitter(
string $username,
string|null $text = null,
string|null $title = null,
string|null $class = null
): string {
return App::instance()->kirbytag([
'twitter' => $username,
'text' => $text,
'title' => $title,
'class' => $class
]);
}
}
if (Helpers::hasOverride('u') === false) { // @codeCoverageIgnore
/**
* Shortcut for url()
@@ -647,8 +642,11 @@ if (Helpers::hasOverride('video') === false) { // @codeCoverageIgnore
* videos. The embed Urls are automatically detected from
* the given Url.
*/
function video(string $url, array $options = [], array $attr = []): string|null
{
function video(
string $url,
array $options = [],
array $attr = []
): string|null {
return Html::video($url, $options, $attr);
}
}
@@ -657,8 +655,11 @@ if (Helpers::hasOverride('vimeo') === false) { // @codeCoverageIgnore
/**
* Embeds a Vimeo video by URL in an iframe
*/
function vimeo(string $url, array $options = [], array $attr = []): string|null
{
function vimeo(
string $url,
array $options = [],
array $attr = []
): string|null {
return Html::vimeo($url, $options, $attr);
}
}
@@ -679,8 +680,11 @@ if (Helpers::hasOverride('youtube') === false) { // @codeCoverageIgnore
/**
* Embeds a Youtube video by URL in an iframe
*/
function youtube(string $url, array $options = [], array $attr = []): string|null
{
function youtube(
string $url,
array $options = [],
array $attr = []
): string|null {
return Html::youtube($url, $options, $attr);
}
}

View File

@@ -2,19 +2,29 @@
use Kirby\Cms\App;
use Kirby\Cms\Blocks;
use Kirby\Cms\Content;
use Kirby\Cms\Field;
use Kirby\Cms\File;
use Kirby\Cms\Files;
use Kirby\Cms\Html;
use Kirby\Cms\Layouts;
use Kirby\Cms\Page;
use Kirby\Cms\Pages;
use Kirby\Cms\Structure;
use Kirby\Cms\Url;
use Kirby\Cms\User;
use Kirby\Cms\Users;
use Kirby\Content\Content;
use Kirby\Content\Field;
use Kirby\Data\Data;
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException;
use Kirby\Image\QrCode;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Dom;
use Kirby\Toolkit\Str;
use Kirby\Toolkit\V;
use Kirby\Toolkit\Xml;
use Kirby\Uuid\Uuid;
/**
* Field method setup
@@ -26,9 +36,6 @@ return function (App $app) {
/**
* Converts the field value into a proper boolean and inverts it
*
* @param \Kirby\Cms\Field $field
* @return bool
*/
'isFalse' => function (Field $field): bool {
return $field->toBool() === false;
@@ -36,9 +43,6 @@ return function (App $app) {
/**
* Converts the field value into a proper boolean
*
* @param \Kirby\Cms\Field $field
* @return bool
*/
'isTrue' => function (Field $field): bool {
return $field->toBool() === true;
@@ -47,22 +51,21 @@ return function (App $app) {
/**
* Validates the field content with the given validator and parameters
*
* @param string $validator
* @param mixed ...$arguments A list of optional validator arguments
* @return bool
*/
'isValid' => function (Field $field, string $validator, ...$arguments): bool {
'isValid' => function (
Field $field,
string $validator,
...$arguments
): bool {
return V::$validator($field->value, ...$arguments);
},
// converters
/**
* Converts a yaml or json field to a Blocks object
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Blocks
*/
'toBlocks' => function (Field $field) {
'toBlocks' => function (Field $field): Blocks {
try {
$blocks = Blocks::parse($field->value());
$blocks = Blocks::factory($blocks, [
@@ -84,11 +87,9 @@ return function (App $app) {
/**
* Converts the field value into a proper boolean
*
* @param \Kirby\Cms\Field $field
* @param bool $default Default value if the field is empty
* @return bool
*/
'toBool' => function (Field $field, $default = false): bool {
'toBool' => function (Field $field, bool $default = false): bool {
$value = $field->isEmpty() ? $default : $field->value;
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
},
@@ -96,11 +97,9 @@ return function (App $app) {
/**
* Parses the field value with the given method
*
* @param \Kirby\Cms\Field $field
* @param string $method [',', 'yaml', 'json']
* @return array
*/
'toData' => function (Field $field, string $method = ',') {
'toData' => function (Field $field, string $method = ','): array {
return match ($method) {
'yaml', 'json' => Data::decode($field->value, $method),
default => $field->split($method)
@@ -110,12 +109,14 @@ return function (App $app) {
/**
* Converts the field value to a timestamp or a formatted date
*
* @param \Kirby\Cms\Field $field
* @param string|\IntlDateFormatter|null $format PHP date formatting string
* @param string|null $fallback Fallback string for `strtotime` (since 3.2)
* @return string|int
* @param string|null $fallback Fallback string for `strtotime`
*/
'toDate' => function (Field $field, $format = null, string $fallback = null) use ($app) {
'toDate' => function (
Field $field,
string|IntlDateFormatter|null $format = null,
string $fallback = null
) use ($app): string|int|null {
if (empty($field->value) === true && $fallback === null) {
return null;
}
@@ -126,28 +127,23 @@ return function (App $app) {
$time = strtotime($fallback);
}
$handler = $app->option('date.handler', 'date');
return Str::date($time, $format, $handler);
return Str::date($time, $format);
},
/**
* Returns a file object from a filename in the field
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\File|null
*/
'toFile' => function (Field $field) {
'toFile' => function (Field $field): File|null {
return $field->toFiles()->first();
},
/**
* Returns a file collection from a yaml list of filenames in the field
*
* @param \Kirby\Cms\Field $field
* @param string $separator
* @return \Kirby\Cms\Files
*/
'toFiles' => function (Field $field, string $separator = 'yaml') {
'toFiles' => function (
Field $field,
string $separator = 'yaml'
): Files {
$parent = $field->parent();
$files = new Files([]);
@@ -163,11 +159,9 @@ return function (App $app) {
/**
* Converts the field value into a proper float
*
* @param \Kirby\Cms\Field $field
* @param float $default Default value if the field is empty
* @return float
*/
'toFloat' => function (Field $field, float $default = 0) {
'toFloat' => function (Field $field, float $default = 0): float {
$value = $field->isEmpty() ? $default : $field->value;
return (float)$value;
},
@@ -175,23 +169,17 @@ return function (App $app) {
/**
* Converts the field value into a proper integer
*
* @param \Kirby\Cms\Field $field
* @param int $default Default value if the field is empty
* @return int
*/
'toInt' => function (Field $field, int $default = 0) {
'toInt' => function (Field $field, int $default = 0): int {
$value = $field->isEmpty() ? $default : $field->value;
return (int)$value;
},
/**
* Parse layouts and turn them into
* Layout objects
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Layouts
* Parse layouts and turn them into Layout objects
*/
'toLayouts' => function (Field $field) {
'toLayouts' => function (Field $field): Layouts {
return Layouts::factory(Layouts::parse($field->value()), [
'parent' => $field->parent(),
'field' => $field,
@@ -201,12 +189,14 @@ return function (App $app) {
/**
* Wraps a link tag around the field value. The field value is used as the link text
*
* @param \Kirby\Cms\Field $field
* @param mixed $attr1 Can be an optional Url. If no Url is set, the Url of the Page, File or Site will be used. Can also be an array of link attributes
* @param mixed $attr2 If `$attr1` is used to set the Url, you can use `$attr2` to pass an array of additional attributes.
* @return string
*/
'toLink' => function (Field $field, $attr1 = null, $attr2 = null) {
'toLink' => function (
Field $field,
string|array|null $attr1 = null,
array|null $attr2 = null
): string {
if (is_string($attr1) === true) {
$href = $attr1;
$attr = $attr2;
@@ -225,49 +215,55 @@ return function (App $app) {
/**
* Parse yaml data and convert it to a
* content object
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Content
*/
'toObject' => function (Field $field) {
'toObject' => function (Field $field): Content {
return new Content($field->yaml(), $field->parent(), true);
},
/**
* Returns a page object from a page id in the field
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Page|null
*/
'toPage' => function (Field $field) {
'toPage' => function (Field $field): Page|null {
return $field->toPages()->first();
},
/**
* Returns a pages collection from a yaml list of page ids in the field
*
* @param \Kirby\Cms\Field $field
* @param string $separator Can be any other separator to split the field value by
* @return \Kirby\Cms\Pages
*/
'toPages' => function (Field $field, string $separator = 'yaml') use ($app) {
return $app->site()->find(false, false, ...$field->toData($separator));
'toPages' => function (
Field $field,
string $separator = 'yaml'
) use ($app): Pages {
return $app->site()->find(
false,
false,
...$field->toData($separator)
);
},
/**
* Turns the field value into an QR code object
*/
'toQrCode' => function (Field $field): QrCode|null {
return $field->isNotEmpty() ? new QrCode($field->value) : null;
},
/**
* Converts a yaml field to a Structure object
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Structure
*/
'toStructure' => function (Field $field) {
'toStructure' => function (Field $field): Structure {
try {
return new Structure(Data::decode($field->value, 'yaml'), $field->parent());
return Structure::factory(
Data::decode($field->value, 'yaml'),
['parent' => $field->parent()]
);
} catch (Exception) {
$message = 'Invalid structure data for "' . $field->key() . '" field';
if ($parent = $field->parent()) {
$message .= ' on parent "' . $parent->title() . '"';
$message .= ' on parent "' . $parent->id() . '"';
}
throw new InvalidArgumentException($message);
@@ -276,9 +272,6 @@ return function (App $app) {
/**
* Converts the field value to a Unix timestamp
*
* @param \Kirby\Cms\Field $field
* @return int|false
*/
'toTimestamp' => function (Field $field): int|false {
return strtotime($field->value ?? '');
@@ -286,33 +279,35 @@ return function (App $app) {
/**
* Turns the field value into an absolute Url
*
* @param \Kirby\Cms\Field $field
* @return string
*/
'toUrl' => function (Field $field): string {
return Url::to($field->value);
'toUrl' => function (Field $field): string|null {
try {
return $field->isNotEmpty() ? Url::to($field->value) : null;
} catch (NotFoundException) {
return null;
}
},
/**
* Converts a user email address to a user object
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\User|null
*/
'toUser' => function (Field $field) {
'toUser' => function (Field $field): User|null {
return $field->toUsers()->first();
},
/**
* Returns a users collection from a yaml list of user email addresses in the field
*
* @param \Kirby\Cms\Field $field
* @param string $separator
* @return \Kirby\Cms\Users
* Returns a users collection from a yaml list
* of user email addresses in the field
*/
'toUsers' => function (Field $field, string $separator = 'yaml') use ($app) {
return $app->users()->find(false, false, ...$field->toData($separator));
'toUsers' => function (
Field $field,
string $separator = 'yaml'
) use ($app): Users {
return $app->users()->find(
false,
false,
...$field->toData($separator)
);
},
// inspectors
@@ -320,14 +315,14 @@ return function (App $app) {
/**
* Returns the length of the field content
*/
'length' => function (Field $field) {
'length' => function (Field $field): int {
return Str::length($field->value);
},
/**
* Returns the number of words in the text
*/
'words' => function (Field $field) {
'words' => function (Field $field): int {
return str_word_count(strip_tags($field->value ?? ''));
},
@@ -336,11 +331,8 @@ return function (App $app) {
/**
* Applies the callback function to the field
* @since 3.4.0
*
* @param \Kirby\Cms\Field $field
* @param Closure $callback
*/
'callback' => function (Field $field, Closure $callback) {
'callback' => function (Field $field, Closure $callback): mixed {
return $callback($field);
},
@@ -348,10 +340,9 @@ return function (App $app) {
* Escapes the field value to be safely used in HTML
* templates without the risk of XSS attacks
*
* @param \Kirby\Cms\Field $field
* @param string $context Location of output (`html`, `attr`, `js`, `css`, `url` or `xml`)
*/
'escape' => function (Field $field, string $context = 'html') {
'escape' => function (Field $field, string $context = 'html'): Field {
$field->value = Str::esc($field->value ?? '', $context);
return $field;
},
@@ -359,25 +350,26 @@ return function (App $app) {
/**
* Creates an excerpt of the field value without html
* or any other formatting.
*
* @param \Kirby\Cms\Field $field
* @param int $cahrs
* @param bool $strip
* @param string $rep
* @return \Kirby\Cms\Field
*/
'excerpt' => function (Field $field, int $chars = 0, bool $strip = true, string $rep = ' …') {
$field->value = Str::excerpt($field->kirbytext()->value(), $chars, $strip, $rep);
'excerpt' => function (
Field $field,
int $chars = 0,
bool $strip = true,
string $rep = ' …'
): Field {
$field->value = Str::excerpt(
$field->kirbytext()->value(),
$chars,
$strip,
$rep
);
return $field;
},
/**
* Converts the field content to valid HTML
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'html' => function (Field $field) {
'html' => function (Field $field): Field {
$field->value = Html::encode($field->value);
return $field;
},
@@ -387,11 +379,8 @@ return function (App $app) {
* it can be safely placed inside of other inline elements
* without the risk of breaking the HTML structure.
* @since 3.3.0
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'inline' => function (Field $field) {
'inline' => function (Field $field): Field {
// List of valid inline elements taken from: https://developer.mozilla.org/de/docs/Web/HTML/Inline_elemente
// Obsolete elements, script tags, image maps and form elements have
// been excluded for safety reasons and as they are most likely not
@@ -402,12 +391,11 @@ return function (App $app) {
/**
* Converts the field content from Markdown/Kirbytext to valid HTML
*
* @param \Kirby\Cms\Field $field
* @param array $options
* @return \Kirby\Cms\Field
*/
'kirbytext' => function (Field $field, array $options = []) use ($app) {
'kirbytext' => function (
Field $field,
array $options = []
) use ($app): Field {
$field->value = $app->kirbytext($field->value, A::merge($options, [
'parent' => $field->parent(),
'field' => $field
@@ -420,12 +408,11 @@ return function (App $app) {
* Converts the field content from inline Markdown/Kirbytext
* to valid HTML
* @since 3.1.0
*
* @param \Kirby\Cms\Field $field
* @param array $options
* @return \Kirby\Cms\Field
*/
'kirbytextinline' => function (Field $field, array $options = []) use ($app) {
'kirbytextinline' => function (
Field $field,
array $options = []
) use ($app): Field {
$field->value = $app->kirbytext($field->value, A::merge($options, [
'parent' => $field->parent(),
'field' => $field,
@@ -439,11 +426,8 @@ return function (App $app) {
/**
* Parses all KirbyTags without also parsing Markdown
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'kirbytags' => function (Field $field) use ($app) {
'kirbytags' => function (Field $field) use ($app): Field {
$field->value = $app->kirbytags($field->value, [
'parent' => $field->parent(),
'field' => $field
@@ -454,23 +438,19 @@ return function (App $app) {
/**
* Converts the field content to lowercase
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'lower' => function (Field $field) {
'lower' => function (Field $field): Field {
$field->value = Str::lower($field->value);
return $field;
},
/**
* Converts markdown to valid HTML
*
* @param \Kirby\Cms\Field $field
* @param array $options
* @return \Kirby\Cms\Field
*/
'markdown' => function (Field $field, array $options = []) use ($app) {
'markdown' => function (
Field $field,
array $options = []
) use ($app): Field {
$field->value = $app->markdown($field->value, $options);
return $field;
},
@@ -478,23 +458,55 @@ return function (App $app) {
/**
* Converts all line breaks in the field content to `<br>` tags.
* @since 3.3.0
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'nl2br' => function (Field $field) {
'nl2br' => function (Field $field): Field {
$field->value = nl2br($field->value ?? '', false);
return $field;
},
/**
* Uses the field value as Kirby query
* Parses the field value as DOM and replaces
* any permalinks in href/src attributes with
* the regular url
*
* @param \Kirby\Cms\Field $field
* @param string|null $expect
* @return mixed
* This method is still experimental! You can use
* it to solve potential problems with permalinks
* already, but it might change in the future.
*/
'query' => function (Field $field, string $expect = null) use ($app) {
'permalinksToUrls' => function (Field $field): Field {
if ($field->isNotEmpty() === true) {
$dom = new Dom($field->value);
$attributes = ['href', 'src'];
$elements = $dom->query('//*[' . implode(' | ', A::map($attributes, fn ($attribute) => '@' . $attribute)) . ']');
foreach ($elements as $element) {
foreach ($attributes as $attribute) {
if ($element->hasAttribute($attribute) && $url = $element->getAttribute($attribute)) {
try {
if ($uuid = Uuid::for($url)) {
$url = $uuid->model()?->url();
$element->setAttribute($attribute, $url);
}
} catch (InvalidArgumentException) {
// ignore anything else than permalinks
}
}
}
}
$field->value = $dom->toString();
}
return $field;
},
/**
* Uses the field value as Kirby query
*/
'query' => function (
Field $field,
string $expect = null
) use ($app): mixed {
if ($parent = $field->parent()) {
return $parent->query($field->value, $expect);
}
@@ -509,13 +521,13 @@ return function (App $app) {
/**
* It parses any queries found in the field value.
*
* @param \Kirby\Cms\Field $field
* @param array $data
* @param string|null $fallback Fallback for tokens in the template that cannot be replaced
* (`null` to keep the original token)
* @return \Kirby\Cms\Field
* @param string|null $fallback Fallback for tokens in the template that cannot be replaced (`null` to keep the original token)
*/
'replace' => function (Field $field, array $data = [], string|null $fallback = '') use ($app) {
'replace' => function (
Field $field,
array $data = [],
string|null $fallback = ''
) use ($app): Field {
if ($parent = $field->parent()) {
// never pass `null` as the $template to avoid the fallback to the model ID
$field->value = $parent->toString($field->value ?? '', $data, $fallback);
@@ -534,55 +546,45 @@ return function (App $app) {
* Cuts the string after the given length and
* adds "…" if it is longer
*
* @param \Kirby\Cms\Field $field
* @param int $length The number of characters in the string
* @param string $appendix An optional replacement for the missing rest
* @return \Kirby\Cms\Field
*/
'short' => function (Field $field, int $length, string $appendix = '…') {
'short' => function (
Field $field,
int $length,
string $appendix = '…'
): Field {
$field->value = Str::short($field->value, $length, $appendix);
return $field;
},
/**
* Converts the field content to a slug
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'slug' => function (Field $field) {
'slug' => function (Field $field): Field {
$field->value = Str::slug($field->value);
return $field;
},
/**
* Applies SmartyPants to the field
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'smartypants' => function (Field $field) use ($app) {
'smartypants' => function (Field $field) use ($app): Field {
$field->value = $app->smartypants($field->value);
return $field;
},
/**
* Splits the field content into an array
*
* @param \Kirby\Cms\Field $field
* @return array
*/
'split' => function (Field $field, $separator = ',') {
'split' => function (Field $field, $separator = ','): array {
return Str::split((string)$field->value, $separator);
},
/**
* Converts the field content to uppercase
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'upper' => function (Field $field) {
'upper' => function (Field $field): Field {
$field->value = Str::upper($field->value);
return $field;
},
@@ -590,22 +592,16 @@ return function (App $app) {
/**
* Avoids typographical widows in strings by replacing
* the last space with `&nbsp;`
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'widont' => function (Field $field) {
'widont' => function (Field $field): Field {
$field->value = Str::widont($field->value);
return $field;
},
/**
* Converts the field content to valid XML
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'xml' => function (Field $field) {
'xml' => function (Field $field): Field {
$field->value = Xml::encode($field->value);
return $field;
},
@@ -614,9 +610,6 @@ return function (App $app) {
/**
* Parses yaml in the field content and returns an array
*
* @param \Kirby\Cms\Field $field
* @return array
*/
'yaml' => function (Field $field): array {
return $field->toData('yaml');

View File

@@ -1,5 +1,6 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\LanguageRoutes;
use Kirby\Cms\Media;
use Kirby\Cms\PluginAssets;
@@ -8,7 +9,7 @@ use Kirby\Panel\Plugins;
use Kirby\Toolkit\Str;
use Kirby\Uuid\Uuid;
return function ($kirby) {
return function (App $kirby) {
$api = $kirby->option('api.slug', 'api');
$panel = $kirby->option('panel.slug', 'panel');
$index = $kirby->url('index');
@@ -32,7 +33,7 @@ return function ($kirby) {
'pattern' => $api . '/(:all)',
'method' => 'ALL',
'env' => 'api',
'action' => function ($path = null) use ($kirby) {
'action' => function (string $path = null) use ($kirby) {
if ($kirby->option('api') === false) {
return null;
}
@@ -60,37 +61,63 @@ return function ($kirby) {
}
],
[
'pattern' => $media . '/plugins/(:any)/(:any)/(:all)\.(css|map|gif|js|mjs|jpg|png|svg|webp|avif|woff2|woff|json)',
// TODO: change to '/plugins/(:any)/(:any)/(:any)/(:all)' once
// the hash is made mandatory
'pattern' => $media . '/plugins/(:any)/(:any)/(?:(:any)/)?(:all)',
'env' => 'media',
'action' => function (string $provider, string $pluginName, string $filename, string $extension) {
return PluginAssets::resolve($provider . '/' . $pluginName, $filename . '.' . $extension);
'action' => function (
string $provider,
string $pluginName,
string $hash,
string $path
) {
return PluginAssets::resolve(
$provider . '/' . $pluginName,
$hash,
$path
);
}
],
[
'pattern' => $media . '/pages/(:all)/(:any)/(:any)',
'env' => 'media',
'action' => function ($path, $hash, $filename) use ($kirby) {
'action' => function (
string $path,
string $hash,
string $filename
) use ($kirby) {
return Media::link($kirby->page($path), $hash, $filename);
}
],
[
'pattern' => $media . '/site/(:any)/(:any)',
'env' => 'media',
'action' => function ($hash, $filename) use ($kirby) {
'action' => function (
string $hash,
string $filename
) use ($kirby) {
return Media::link($kirby->site(), $hash, $filename);
}
],
[
'pattern' => $media . '/users/(:any)/(:any)/(:any)',
'env' => 'media',
'action' => function ($id, $hash, $filename) use ($kirby) {
'action' => function (
string $id,
string $hash,
string $filename
) use ($kirby) {
return Media::link($kirby->user($id), $hash, $filename);
}
],
[
'pattern' => $media . '/assets/(:all)/(:any)/(:any)',
'env' => 'media',
'action' => function ($path, $hash, $filename) {
'action' => function (
string $path,
string $hash,
string $filename
) {
return Media::thumb($path, $hash, $filename);
}
],
@@ -98,7 +125,7 @@ return function ($kirby) {
'pattern' => $panel . '/(:all?)',
'method' => 'ALL',
'env' => 'panel',
'action' => function ($path = null) {
'action' => function (string $path = null) {
return Panel::router($path);
}
],

View File

@@ -1,6 +1,7 @@
<?php
use Kirby\Cms\File;
use Kirby\Cms\Files;
use Kirby\Toolkit\I18n;
return [
@@ -18,6 +19,12 @@ return [
'sort'
],
'props' => [
/**
* Filters pages by a query. Sorting will be disabled
*/
'query' => function (string|null $query = null) {
return $query;
},
/**
* Filters all files by template and also sets the template, which will be used for all uploads
*/
@@ -49,10 +56,17 @@ return [
return $this->parentModel();
},
'files' => function () {
$files = $this->parent->files()->template($this->template);
if ($this->query !== null) {
$files = $this->parent->query($this->query, Files::class) ?? new Files([]);
} else {
$files = $this->parent->files();
}
// filter out all protected files
$files = $files->filter('isReadable', true);
// filter files by template
$files = $files->template($this->template);
// filter out all protected and hidden files
$files = $files->filter('isListable', true);
// search
if ($this->search === true && empty($this->searchterm()) === false) {

View File

@@ -7,6 +7,9 @@ return [
'headline',
],
'props' => [
'icon' => function (string $icon = null) {
return $icon;
},
'text' => function ($text = null) {
return I18n::translate($text, $text);
},
@@ -25,6 +28,7 @@ return [
],
'toArray' => function () {
return [
'icon' => $this->icon,
'label' => $this->headline,
'text' => $this->text,
'theme' => $this->theme

View File

@@ -20,7 +20,7 @@ return [
return in_array($layout, $layouts) ? $layout : 'list';
},
/**
* The size option controls the size of cards. By default cards are auto-sized and the cards grid will always fill the full width. With a size you can disable auto-sizing. Available sizes: `tiny`, `small`, `medium`, `large`, `huge`
* The size option controls the size of cards. By default cards are auto-sized and the cards grid will always fill the full width. With a size you can disable auto-sizing. Available sizes: `tiny`, `small`, `medium`, `large`, `huge`, `full`
*/
'size' => function (string $size = 'auto') {
return $size;

View File

@@ -39,6 +39,10 @@ return [
return false;
}
if ($this->query !== null) {
return false;
}
if ($this->sortBy !== null) {
return false;
}

View File

@@ -2,6 +2,7 @@
use Kirby\Cms\Blueprint;
use Kirby\Cms\Page;
use Kirby\Cms\Pages;
use Kirby\Cms\Site;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\A;
@@ -29,6 +30,12 @@ return [
'create' => function ($create = null) {
return $create;
},
/**
* Filters pages by a query. Sorting will be disabled
*/
'query' => function (string|null $query = null) {
return $query;
},
/**
* Filters pages by their status. Available status settings: `draft`, `unlisted`, `listed`, `published`, `all`.
*/
@@ -43,11 +50,23 @@ return [
return $status;
},
/**
* Filters the list by single template.
*/
'template' => function (string|array $template = null) {
return $template;
},
/**
* Filters the list by templates and sets template options when adding new pages to the section.
*/
'templates' => function ($templates = null) {
return A::wrap($templates ?? $this->template);
},
/**
* Excludes the selected templates.
*/
'templatesIgnore' => function ($templates = null) {
return A::wrap($templates);
}
],
'computed' => [
@@ -64,13 +83,17 @@ return [
return $parent;
},
'pages' => function () {
$pages = match ($this->status) {
'draft' => $this->parent->drafts(),
'listed' => $this->parent->children()->listed(),
'published' => $this->parent->children(),
'unlisted' => $this->parent->children()->unlisted(),
default => $this->parent->childrenAndDrafts()
};
if ($this->query !== null) {
$pages = $this->parent->query($this->query, Pages::class) ?? new Pages([]);
} else {
$pages = match ($this->status) {
'draft' => $this->parent->drafts(),
'listed' => $this->parent->children()->listed(),
'published' => $this->parent->children(),
'unlisted' => $this->parent->children()->unlisted(),
default => $this->parent->childrenAndDrafts()
};
}
// filters pages that are protected and not in the templates list
// internal `filter()` method used instead of foreach loop that previously included `unset()`
@@ -78,13 +101,26 @@ return [
// also it has been tested that there is no performance difference
// even in 0.1 seconds on 100k virtual pages
$pages = $pages->filter(function ($page) {
// remove all protected pages
if ($page->isReadable() === false) {
// remove all protected and hidden pages
if ($page->isListable() === false) {
return false;
}
$intendedTemplate = $page->intendedTemplate()->name();
// filter by all set templates
if ($this->templates && in_array($page->intendedTemplate()->name(), $this->templates) === false) {
if (
$this->templates &&
in_array($intendedTemplate, $this->templates) === false
) {
return false;
}
// exclude by all ignored templates
if (
$this->templatesIgnore &&
in_array($intendedTemplate, $this->templatesIgnore) === true
) {
return false;
}
@@ -216,6 +252,11 @@ return [
$templates = $this->kirby()->blueprints();
}
// excludes ignored templates
if ($templatesIgnore = $this->templatesIgnore) {
$templates = array_diff($templates, $templatesIgnore);
}
// convert every template to a usable option array
// for the template select box
foreach ($templates as $template) {

View File

@@ -1,11 +1,5 @@
<?php
/**
* Constants
* @deprecated 3.8.0 Use `/` instead
*/
define('DS', '/');
/**
* Class aliases
*/

View File

@@ -2,6 +2,8 @@
use Kirby\Cms\Html;
use Kirby\Cms\Url;
use Kirby\Text\KirbyTag;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Str;
use Kirby\Uuid\Uuid;
@@ -15,8 +17,12 @@ return [
*/
'date' => [
'attr' => [],
'html' => function ($tag) {
return strtolower($tag->date) === 'year' ? date('Y') : date($tag->date);
'html' => function (KirbyTag $tag): string {
if (strtolower($tag->date) === 'year') {
return date('Y');
}
return date($tag->date);
}
],
@@ -31,7 +37,7 @@ return [
'text',
'title'
],
'html' => function ($tag) {
'html' => function (KirbyTag $tag): string {
return Html::email($tag->value, $tag->text, [
'class' => $tag->class,
'rel' => $tag->rel,
@@ -53,7 +59,7 @@ return [
'text',
'title'
],
'html' => function ($tag) {
'html' => function (KirbyTag $tag): string {
if (!$file = $tag->file($tag->value)) {
return $tag->text;
}
@@ -81,7 +87,7 @@ return [
'attr' => [
'file'
],
'html' => function ($tag) {
'html' => function (KirbyTag $tag): string {
return Html::gist($tag->value, $tag->file);
}
],
@@ -99,16 +105,29 @@ return [
'link',
'linkclass',
'rel',
'srcset',
'target',
'title',
'width'
],
'html' => function ($tag) {
'html' => function (KirbyTag $tag): string {
if ($tag->file = $tag->file($tag->value)) {
$tag->src = $tag->file->url();
$tag->alt = $tag->alt ?? $tag->file->alt()->or(' ')->value();
$tag->title = $tag->title ?? $tag->file->title()->value();
$tag->caption = $tag->caption ?? $tag->file->caption()->value();
$tag->src = $tag->file->url();
$tag->alt ??= $tag->file->alt()->or('')->value();
$tag->title ??= $tag->file->title()->value();
$tag->caption ??= $tag->file->caption()->value();
if ($srcset = $tag->srcset) {
$srcset = Str::split($srcset);
$srcset = match (count($srcset) > 1) {
// comma-separated list of sizes
true => A::map($srcset, fn ($size) => (int)trim($size)),
// srcset config name
default => $srcset[0]
};
$tag->srcset = $tag->file->srcset($srcset);
}
} else {
$tag->src = Url::to($tag->value);
}
@@ -129,11 +148,12 @@ return [
};
$image = Html::img($tag->src, [
'srcset' => $tag->srcset,
'width' => $tag->width,
'height' => $tag->height,
'class' => $tag->imgclass,
'title' => $tag->title,
'alt' => $tag->alt ?? ' '
'alt' => $tag->alt ?? ''
]);
if ($tag->kirby()->option('kirbytext.image.figure', true) === false) {
@@ -147,7 +167,7 @@ return [
$tag->caption = [$caption];
}
return Html::figure([ $link($image) ], $tag->caption, [
return Html::figure([$link($image)], $tag->caption, [
'class' => $tag->class
]);
}
@@ -166,7 +186,7 @@ return [
'title',
'text',
],
'html' => function ($tag) {
'html' => function (KirbyTag $tag): string {
if (empty($tag->lang) === false) {
$tag->value = Url::to($tag->value, $tag->lang);
}
@@ -200,7 +220,7 @@ return [
'text',
'title'
],
'html' => function ($tag) {
'html' => function (KirbyTag $tag): string {
return Html::tel($tag->value, $tag->text, [
'class' => $tag->class,
'rel' => $tag->rel,
@@ -209,37 +229,6 @@ return [
}
],
/**
* Twitter
*/
'twitter' => [
'attr' => [
'class',
'rel',
'target',
'text',
'title'
],
'html' => function ($tag) {
// get and sanitize the username
$username = str_replace('@', '', $tag->value);
// build the profile url
$url = 'https://twitter.com/' . $username;
// sanitize the link text
$text = $tag->text ?? '@' . $username;
// build the final link
return Html::a($url, $text, [
'class' => $tag->class,
'rel' => $tag->rel,
'target' => $tag->target,
'title' => $tag->title,
]);
}
],
/**
* Video
*/
@@ -258,7 +247,7 @@ return [
'style',
'width',
],
'html' => function ($tag) {
'html' => function (KirbyTag $tag): string {
// checks and gets if poster is local file
if (
empty($tag->poster) === false &&

View File

@@ -3,57 +3,73 @@
"account.delete": "Delete your account",
"account.delete.confirm": "Do you really want to delete your account? You will be logged out immediately. Your account cannot be recovered.",
"add": "Добави",
"activate": "Activate",
"add": "\u0414\u043e\u0431\u0430\u0432\u0438",
"alpha": "Alpha",
"author": "Author",
"avatar": "Профилна снимка",
"back": "Назад",
"cancel": "Откажи",
"change": "Промени",
"close": "Затвори",
"cancel": "\u041e\u0442\u043a\u0430\u0436\u0438",
"change": "\u041f\u0440\u043e\u043c\u0435\u043d\u0438",
"close": "\u0417\u0430\u0442\u0432\u043e\u0440\u0438",
"changes": "Changes",
"confirm": "Ок",
"collapse": "Collapse",
"collapse.all": "Collapse All",
"color": "Color",
"coordinates": "Coordinates",
"copy": "Копирай",
"copy.all": "Copy all",
"copy.success": "{count} copied!",
"create": "Създай",
"custom": "Custom",
"date": "Дата",
"date.select": "Select a date",
"day": "Day",
"days.fri": "Пт",
"days.mon": "Пн",
"days.sat": "Сб",
"days.sun": "Нд",
"days.thu": "Чт",
"days.tue": "Вт",
"days.wed": "Ср",
"days.fri": "\u041f\u0442",
"days.mon": "\u041f\u043d",
"days.sat": "\u0421\u0431",
"days.sun": "\u041d\u0434",
"days.thu": "\u0427\u0442",
"days.tue": "\u0412\u0442",
"days.wed": "\u0421\u0440",
"debugging": "Debugging",
"delete": "Изтрий",
"delete": "\u0418\u0437\u0442\u0440\u0438\u0439",
"delete.all": "Delete all",
"dialog.fields.empty": "This dialog has no fields",
"dialog.files.empty": "No files to select",
"dialog.pages.empty": "No pages to select",
"dialog.text.empty": "This dialog does not define any text",
"dialog.users.empty": "No users to select",
"dimensions": "Размери",
"disable": "Disable",
"disabled": "Disabled",
"discard": "Отмени",
"discard": "\u041e\u0442\u043c\u0435\u043d\u0438",
"drawer.fields.empty": "This drawer has no fields",
"domain": "Domain",
"download": "Download",
"duplicate": "Duplicate",
"edit": "Редактирай",
"edit": "\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0430\u0439",
"email": "Email",
"email.placeholder": "mail@example.com",
"enter": "Enter",
"entries": "Entries",
"entry": "Entry",
"environment": "Environment",
"error": "Error",
"error.access.code": "Invalid code",
"error.access.login": "Invalid login",
"error.access.panel": "Нямате права за достъп до панела",
@@ -81,6 +97,9 @@
"error.file.changeName.empty": "The name must not be empty",
"error.file.changeName.permission": "Не можете да смените името на \"{filename}\"",
"error.file.changeTemplate.invalid": "The template for the file \"{id}\" cannot be changed to \"{template}\" (valid: \"{blueprints}\")",
"error.file.changeTemplate.permission": "You are not allowed to change the template for the file \"{id}\"",
"error.file.duplicate": "Файл с име \"{filename}\" вече съществува",
"error.file.extension.forbidden": "Файловото разширение \"{extension}\" не е позволено",
"error.file.extension.invalid": "Invalid extension: {extension}",
@@ -95,12 +114,13 @@
"error.file.minheight": "The height of the image must be at least {height} pixels",
"error.file.minsize": "The file is too small",
"error.file.minwidth": "The width of the image must be at least {width} pixels",
"error.file.name.unique": "The filename must be unique",
"error.file.name.missing": "Името на файла е задължително",
"error.file.notFound": "Файлът \"{filename}\" не може да бъде намерен",
"error.file.orientation": "The orientation of the image must be \"{orientation}\"",
"error.file.type.forbidden": "Не е позволен ъплоуда на файлове от тип {type}",
"error.file.type.invalid": "Invalid file type: {type}",
"error.file.undefined": "Файлът не може да бъде намерен",
"error.file.undefined": "\u0424\u0430\u0439\u043b\u044a\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d",
"error.form.incomplete": "Моля коригирайте всички грешки във формата...",
"error.form.notSaved": "Формата не може да бъде запазена",
@@ -113,15 +133,20 @@
"error.layout.validation.block": "There's an error on the \"{field}\" field in block {blockIndex} using the \"{fieldset}\" block type in layout {layoutIndex}",
"error.layout.validation.settings": "There's an error in layout {index} settings",
"error.license.format": "Please enter a valid license key",
"error.license.domain": "The domain for the license is missing",
"error.license.email": "Моля въведете валиден email адрес",
"error.license.format": "Please enter a valid license code",
"error.license.verification": "The license could not be verified",
"error.login.totp.confirm.invalid": "Invalid code",
"error.login.totp.confirm.missing": "Please enter the current code",
"error.object.validation": "Theres an error in the \"{label}\" field:\n{message}",
"error.offline": "The Panel is currently offline",
"error.page.changeSlug.permission": "Не можете да смените URL на \"{slug}\"",
"error.page.changeSlug.reserved": "The path of top-level pages must not start with \"{path}\"",
"error.page.changeStatus.incomplete": "Страницата съдържа грешки и не може да бъде публикувана",
"error.page.changeStatus.permission": "Статусът на страницата не може да бъде променен",
"error.page.changeStatus.toDraft.invalid": "Страницата \"{slug}\" не може да бъде променена в чернова",
@@ -137,13 +162,19 @@
"error.page.draft.duplicate": "Вече съществува чернова с URL-добавка \"{slug}\"",
"error.page.duplicate": "Страница с URL-добавка \"{slug}\" вече съществува",
"error.page.duplicate.permission": "You are not allowed to duplicate \"{slug}\"",
"error.page.move.ancestor": "The page cannot be moved into itself",
"error.page.move.directory": "The page directory cannot be moved",
"error.page.move.duplicate": "A sub page with the URL appendix \"{slug}\" already exists",
"error.page.move.notFound": "The moved page could not be found",
"error.page.move.permission": "You are not allowed to move \"{slug}\"",
"error.page.move.template": "The \"{template}\" template is not accepted as a subpage of \"{parent}\"",
"error.page.notFound": "Страницата \"{slug}\" не може да бъде намерена",
"error.page.num.invalid": "Моля въведете валидно число за сортиране. Числата не трябва да са негативни.",
"error.page.slug.invalid": "Please enter a valid URL appendix",
"error.page.slug.maxlength": "Slug length must be less than \"{length}\" characters",
"error.page.sort.permission": "Страницата \"{slug}\" не може да бъде сортирана",
"error.page.status.invalid": "Моля изберете валиден статус на страницата",
"error.page.undefined": "Страницата не може да бъде намерена",
"error.page.undefined": "\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0430",
"error.page.update.permission": "Не можете да обновите \"{slug}\"",
"error.section.files.max.plural": "Не можете да добавяте повече от {max} файлa в секция \"{section}\"",
@@ -163,6 +194,8 @@
"error.site.changeTitle.permission": "Не може да променяте заглавието на сайта",
"error.site.update.permission": "Нямате права за да обновите сайта",
"error.structure.validation": "There's an error on the \"{field}\" field in row {index}",
"error.template.default.notFound": "Стандартният шаблон не съществува",
"error.unexpected": "An unexpected error occurred! Enable debug mode for more info: https://getkirby.com/docs/reference/system/options/debug",
@@ -175,17 +208,17 @@
"error.user.changeRole.permission": "Нямате права да промените ролята на този потребител \"{name}\"",
"error.user.changeRole.toAdmin": "You are not allowed to promote someone to the admin role",
"error.user.create.permission": "Нямате права да създадете този потребител",
"error.user.delete": "Потребителят не може да бъде изтрит",
"error.user.delete.lastAdmin": "Не можете да изтриете последния администратор",
"error.user.delete": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0438\u0437\u0442\u0440\u0438\u0442",
"error.user.delete.lastAdmin": "\u041d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u0442\u0440\u0438\u0435\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u044f \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440",
"error.user.delete.lastUser": "Последният потребител не може да бъде изтрит",
"error.user.delete.permission": "Не е позволено да изтривате този потребител",
"error.user.delete.permission": "\u041d\u0435 \u0435 \u043f\u043e\u0437\u0432\u043e\u043b\u0435\u043d\u043e \u0434\u0430 \u0438\u0437\u0442\u0440\u0438\u0432\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b",
"error.user.duplicate": "Потребител с имейл \"{email}\" вече съществува",
"error.user.email.invalid": "Моля въведете валиден email адрес",
"error.user.language.invalid": "Моля въведете валиден език",
"error.user.notFound": "Потребителят не може да бъде намерен.",
"error.user.notFound": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d.",
"error.user.password.excessive": "Please enter a valid password. Passwords must not be longer than 1000 characters.",
"error.user.password.invalid": "Моля въведете валидна парола. Тя трабва да съдържа поне 8 символа.",
"error.user.password.notSame": "Моля, потвърдете паролата",
"error.user.password.notSame": "\u041c\u043e\u043b\u044f, \u043f\u043e\u0442\u0432\u044a\u0440\u0434\u0435\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430",
"error.user.password.undefined": "Потребителят няма парола",
"error.user.password.wrong": "Wrong password",
"error.user.role.invalid": "Моля въведете валидна роля",
@@ -195,8 +228,10 @@
"error.validation.accepted": "Моля потвърдете",
"error.validation.alpha": "Моля въвдете символи измежду a-z",
"error.validation.alphanum": "Моля въвдете символи измежду a-z или цифри 0-9",
"error.validation.anchor": "Please enter a correct link anchor",
"error.validation.between": "Моля въведете стойност между \"{min}\" и \"{max}\"",
"error.validation.boolean": "Моля потвърдете или откажете",
"error.validation.color": "Please enter a valid color in the {format} format",
"error.validation.contains": "Моля въведете стойност, която съдържа \"{needle}\"",
"error.validation.date": "Моля въведете валидна дата",
"error.validation.date.after": "Please enter a date after {date}",
@@ -211,6 +246,7 @@
"error.validation.integer": "Моля въведете валидно цяло число",
"error.validation.ip": "Моля въведете валиден IP адрес",
"error.validation.less": "Моля въведете стойност по-ниска от {max}",
"error.validation.linkType": "The link type is not allowed",
"error.validation.match": "Стойността не съвпада с очаквания модел",
"error.validation.max": "Please enter a value equal to or lower than {max}",
"error.validation.maxlength": "Моля въведете по-къса стойност. (макс. {max} символа)",
@@ -227,15 +263,18 @@
"error.validation.same": "Моля въведете \"{other}\"",
"error.validation.size": "Размерът на стойността трябва да бъде \"{size}\"",
"error.validation.startswith": "Стойността трябва да започва с \"{start}\"",
"error.validation.tel": "Please enter an unformatted phone number",
"error.validation.time": "Моля въведете валидно време",
"error.validation.time.after": "Please enter a time after {time}",
"error.validation.time.before": "Please enter a time before {time}",
"error.validation.time.between": "Please enter a time between {min} and {max}",
"error.validation.uuid": "Please enter a valid UUID",
"error.validation.url": "Моля въведете валиден URL",
"expand": "Expand",
"expand.all": "Expand All",
"field.invalid": "The field is invalid",
"field.required": "The field is required",
"field.blocks.changeType": "Change type",
"field.blocks.code.name": "Код",
@@ -245,8 +284,9 @@
"field.blocks.delete.confirm.all": "Do you really want to delete all blocks?",
"field.blocks.delete.confirm.selected": "Do you really want to delete the selected blocks?",
"field.blocks.empty": "No blocks yet",
"field.blocks.fieldsets.empty": "No fieldsets yet",
"field.blocks.fieldsets.label": "Please select a block type …",
"field.blocks.fieldsets.paste": "Press <kbd>{{ shortcut }}</kbd> to paste/import blocks from your clipboard",
"field.blocks.fieldsets.paste": "Press <kbd>{{ shortcut }}</kbd> to import layouts/blocks from your clipboard <small>Only those allowed in the current field will get inserted.</small>",
"field.blocks.gallery.name": "Gallery",
"field.blocks.gallery.images.empty": "No images yet",
"field.blocks.gallery.images.label": "Images",
@@ -259,6 +299,8 @@
"field.blocks.image.crop": "Crop",
"field.blocks.image.link": "Връзка",
"field.blocks.image.location": "Location",
"field.blocks.image.location.internal": "This website",
"field.blocks.image.location.external": "External source",
"field.blocks.image.name": "Изображение",
"field.blocks.image.placeholder": "Select an image",
"field.blocks.image.ratio": "Ratio",
@@ -283,8 +325,10 @@
"field.files.empty": "Все още не са избрани файлове",
"field.layout.change": "Change layout",
"field.layout.delete": "Delete layout",
"field.layout.delete.confirm": "Do you really want to delete this layout?",
"field.layout.delete.confirm.all": "Do you really want to delete all layouts?",
"field.layout.empty": "No rows yet",
"field.layout.select": "Select a layout",
@@ -298,18 +342,29 @@
"field.users.empty": "Все още не са избрани потребители",
"fields.empty": "No fields yet",
"file": "Файл",
"file.blueprint": "This file has no blueprint yet. You can define the setup in <strong>/site/blueprints/files/{blueprint}.yml</strong>",
"file.changeTemplate": "Промени шаблон",
"file.changeTemplate.notice": "Changing the file's template will remove content for fields that don't match in type. If the new template defines certain rules, e.g. image dimensions, those will also be applied irreversibly. Use with caution.",
"file.delete.confirm": "Сигурни ли сте, че искате да изтриете <br><strong>{filename}</strong>?",
"file.focus.placeholder": "Set focal point",
"file.focus.reset": "Remove focal point",
"file.focus.title": "Focus",
"file.sort": "Change position",
"files": "Файлове",
"files.empty": "Няма файлове",
"filter": "Filter",
"hide": "Hide",
"hour": "Hour",
"hue": "Hue",
"import": "Import",
"info": "Info",
"insert": "Вмъкни",
"insert": "\u0412\u043c\u044a\u043a\u043d\u0438",
"insert.after": "Insert after",
"insert.before": "Insert before",
"install": "Инсталирай",
@@ -327,11 +382,12 @@
"installation.issues.server": "Kirby изисква <code>Apache</code>, <code>Nginx</code> или <code>Caddy</code>",
"installation.issues.sessions": "The <code>/site/sessions</code> folder does not exist or is not writable",
"language": "Език",
"language": "\u0415\u0437\u0438\u043a",
"language.code": "Код",
"language.convert": "Направи по подразбиране",
"language.convert.confirm": "<p>Сигурни ли сте, че искате да зададете <strong>{name}</strong> за език по подразбиране? Действието не може да бъде отменено.</p><p>В случай, че в <strong>{name}</strong> има непреведено съдържание, то части от сайта ви могат да останат празни.</p>",
"language.create": "Добавете нов език",
"language.default": "Език по подразбиране",
"language.delete.confirm": "Сигурни ли сте, че искате да изтриете език <strong>{name}</strong>, включително всички негови преводи? Действието не може да бъде отменено!",
"language.deleted": "Езикът беше изтрит",
"language.direction": "Посока на четене",
@@ -340,7 +396,16 @@
"language.locale": "PHP locale string",
"language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages",
"language.name": "Име",
"language.secondary": "Secondary language",
"language.settings": "Language settings",
"language.updated": "Езикът беше обновен",
"language.variables": "Language variables",
"language.variables.empty": "No translations yet",
"language.variable.delete.confirm": "Do you really want to delete the variable for {key}?",
"language.variable.key": "Key",
"language.variable.notFound": "The variable could not be found",
"language.variable.value": "Value",
"languages": "Езици",
"languages.default": "Език по подразбиране",
@@ -348,36 +413,52 @@
"languages.secondary": "Второстепенни езици",
"languages.secondary.empty": "Все още няма второстепенни езици",
"license": "Лиценз за Kirby",
"license": "\u041b\u0438\u0446\u0435\u043d\u0437 \u0437\u0430 Kirby",
"license.activate": "Activate it now",
"license.activate.label": "Please activate your license",
"license.activate.domain": "Your license will be activated for <strong>{host}</strong>.",
"license.activate.local": "You are about to activate your Kirby license for your local domain <strong>{host}</strong>. If this site will be deployed to a public domain, please activate it there instead. If {host} is the domain you want to use your license for, please continue.",
"license.activated": "Activated",
"license.buy": "Купи лиценз",
"license.register": "Регистрирай",
"license.code": "Код",
"license.code.help": "You received your license code after the purchase via email. Please copy and paste it here.",
"license.code.label": "Please enter your license code",
"license.status.active.info": "Includes new major versions until {date}",
"license.status.active.label": "Valid license",
"license.status.demo.info": "This is a demo installation",
"license.status.demo.label": "Demo",
"license.status.inactive.info": "Renew license to update to new major versions",
"license.status.inactive.label": "No new major versions",
"license.status.legacy.bubble": "Ready to renew your license?",
"license.status.legacy.info": "Your license does not cover this version",
"license.status.legacy.label": "Please renew your license",
"license.status.missing.bubble": "Ready to launch your site?",
"license.status.missing.info": "No valid license",
"license.status.missing.label": "Please activate your license",
"license.manage": "Manage your licenses",
"license.register.help": "You received your license code after the purchase via email. Please copy and paste it to register.",
"license.register.label": "Please enter your license code",
"license.register.domain": "Your license will be registered to <strong>{host}</strong>.",
"license.register.local": "You are about to register your license for your local domain <strong>{host}</strong>. If this site will be deployed to a public domain, please register it there instead. If {host} is the domain you want to license Kirby to, please continue.",
"license.register.success": "Thank you for supporting Kirby",
"license.unregistered": "Това е нерегистрирана демо версия на Kirby",
"license.purchased": "Purchased",
"license.success": "Thank you for supporting Kirby",
"license.unregistered.label": "Unregistered",
"link": "Връзка",
"link": "\u0412\u0440\u044a\u0437\u043a\u0430",
"link.text": "Текстова връзка",
"loading": "Зареждане",
"lock.unsaved": "Unsaved changes",
"lock.unsaved.empty": "There are no more unsaved changes",
"lock.isLocked": "Unsaved changes by <strong>{email}</strong>",
"lock.file.isLocked": "The file is currently being edited by {email} and cannot be changed.",
"lock.page.isLocked": "The page is currently being edited by {email} and cannot be changed.",
"lock.isLocked": "Unsaved changes by {email}",
"lock.unlock": "Unlock",
"lock.isUnlocked": "Your unsaved changes have been overwritten by another user. You can download your changes to merge them manually.",
"lock.unlock.submit": "Unlock and overwrite unsaved changes by <strong>{email}</strong>",
"lock.isUnlocked": "Was unlocked by another user",
"login": "Подписване",
"login.code.label.login": "Login code",
"login.code.label.password-reset": "Password reset code",
"login.code.placeholder.email": "000 000",
"login.code.placeholder.totp": "000000",
"login.code.text.email": "If your email address is registered, the requested code was sent via email.",
"login.code.text.totp": "Please enter the onetime code from your authenticator app.",
"login.email.login.body": "Hi {user.nameOrEmail},\n\nYou recently requested a login code for the Panel of {site}.\nThe following login code will be valid for {timeout} minutes:\n\n{code}\n\nIf you did not request a login code, please ignore this email or contact your administrator if you have questions.\nFor security, please DO NOT forward this email.",
"login.email.login.subject": "Your login code",
"login.email.password-reset.body": "Hi {user.nameOrEmail},\n\nYou recently requested a password reset code for the Panel of {site}.\nThe following password reset code will be valid for {timeout} minutes:\n\n{code}\n\nIf you did not request a password reset code, please ignore this email or contact your administrator if you have questions.\nFor security, please DO NOT forward this email.",
@@ -388,58 +469,80 @@
"login.toggleText.code.email-password": "Login with password",
"login.toggleText.password-reset.email": "Forgot your password?",
"login.toggleText.password-reset.email-password": "← Back to login",
"login.totp.enable.option": "Set up onetime codes",
"login.totp.enable.intro": "Authenticator apps can generate onetime codes that are used as a second factor when signing into your account.",
"login.totp.enable.qr.label": "1. Scan this QR code",
"login.totp.enable.qr.help": "Unable to scan? Add the setup key <code>{secret}</code> manually to your authenticator app.",
"login.totp.enable.confirm.headline": "2. Confirm with generated code",
"login.totp.enable.confirm.text": "Your app generates a new onetime code every 30 seconds. Enter the current code to complete the setup:",
"login.totp.enable.confirm.label": "Current code",
"login.totp.enable.confirm.help": "After this setup, we will ask you for a onetime code every time you log in.",
"login.totp.enable.success": "Onetime codes enabled",
"login.totp.disable.option": "Disable onetime codes",
"login.totp.disable.label": "Enter your password to disable onetime codes",
"login.totp.disable.help": "In the future, a different second factor like a login code sent via email will be requested when you log in. You can always set up onetime codes again later.",
"login.totp.disable.admin": "<p>This will disable onetime codes for <strong>{user}</strong>.</p><p>In the future, a different second factor like a login code sent via email will be requested when they log in. {user} can set up onetime codes again after their next login.</p>",
"login.totp.disable.success": "Onetime codes disabled",
"logout": "Изход",
"merge": "Merge",
"menu": "Меню",
"meridiem": "AM/PM",
"mime": "Media Type",
"minutes": "Minutes",
"month": "Month",
"months.april": "Април",
"months.august": "Август",
"months.december": "Декември",
"months.april": "\u0410\u043f\u0440\u0438\u043b",
"months.august": "\u0410\u0432\u0433\u0443\u0441\u0442",
"months.december": "\u0414\u0435\u043a\u0435\u043c\u0432\u0440\u0438",
"months.february": "Февруари",
"months.january": "Януари",
"months.july": "Юли",
"months.june": "Юни",
"months.march": "Март",
"months.may": "Май",
"months.november": "Ноември",
"months.october": "Октомври",
"months.september": "Септември",
"months.january": "\u042f\u043d\u0443\u0430\u0440\u0438",
"months.july": "\u042e\u043b\u0438",
"months.june": "\u042e\u043d\u0438",
"months.march": "\u041c\u0430\u0440\u0442",
"months.may": "\u041c\u0430\u0439",
"months.november": "\u041d\u043e\u0435\u043c\u0432\u0440\u0438",
"months.october": "\u041e\u043a\u0442\u043e\u043c\u0432\u0440\u0438",
"months.september": "\u0421\u0435\u043f\u0442\u0435\u043c\u0432\u0440\u0438",
"more": "Още",
"move": "Move",
"name": "Име",
"next": "Next",
"night": "Night",
"no": "no",
"off": "off",
"on": "on",
"open": "Отвори",
"open.newWindow": "Open in new window",
"option": "Option",
"options": "Options",
"options.none": "No options",
"options.all": "Show all {count} options",
"orientation": "Ориентация",
"orientation.landscape": "Пейзаж",
"orientation.portrait": "Портрет",
"orientation.square": "Квадрат",
"page": "Страница",
"page.blueprint": "This page has no blueprint yet. You can define the setup in <strong>/site/blueprints/pages/{blueprint}.yml</strong>",
"page.changeSlug": "Промени URL",
"page.changeSlug.fromTitle": "Създайте от заглавието",
"page.changeSlug": "\u041f\u0440\u043e\u043c\u0435\u043d\u0438 URL",
"page.changeSlug.fromTitle": "\u0421\u044a\u0437\u0434\u0430\u0439\u0442\u0435 \u043e\u0442 \u0437\u0430\u0433\u043b\u0430\u0432\u0438\u0435\u0442\u043e",
"page.changeStatus": "Промени статус",
"page.changeStatus.position": "Моля изберете позиция",
"page.changeStatus.select": "Изберете нов статус",
"page.changeTemplate": "Промени шаблон",
"page.changeTemplate.notice": "Changing the page's template will remove content for fields that don't match in type. Use with caution.",
"page.create": "Create as {status}",
"page.delete.confirm": "Сигурни ли сте, че искате да изтриете <strong>{title}</strong>?",
"page.delete.confirm.subpages": "<strong>Тази страница има подстраници</strong>. <br>Всички подстраници също ще бъдат изтрити.",
"page.delete.confirm.title": "Въведи заглавие на страница за да потвърдиш",
"page.draft.create": "Създай чернова",
"page.duplicate.appendix": "Копирай",
"page.duplicate.files": "Copy files",
"page.duplicate.pages": "Copy pages",
"page.move": "Move page",
"page.sort": "Change position",
"page.status": "Status",
"page.status.draft": "Чернова",
@@ -457,9 +560,10 @@
"pagination.page": "Страница",
"password": "Парола",
"password": "\u041f\u0430\u0440\u043e\u043b\u0430",
"paste": "Paste",
"paste.after": "Paste after",
"paste.success": "{count} pasted!",
"pixel": "Пиксел",
"plugin": "Plugin",
"plugins": "Plugins",
@@ -467,12 +571,14 @@
"preview": "Preview",
"remove": "Премахни",
"rename": "Преименувай",
"replace": "Замести",
"retry": "Опитай пак",
"revert": "Отмени",
"renew": "Renew",
"replace": "\u0417\u0430\u043c\u0435\u0441\u0442\u0438",
"replace.with": "Replace with",
"retry": "\u041e\u043f\u0438\u0442\u0430\u0439 \u043f\u0430\u043a",
"revert": "\u041e\u0442\u043c\u0435\u043d\u0438",
"revert.confirm": "Do you really want to <strong>delete all unsaved changes</strong>?",
"role": "Роля",
"role": "\u0420\u043e\u043b\u044f",
"role.admin.description": "The admin has all rights",
"role.admin.title": "Admin",
"role.all": "Всички",
@@ -481,12 +587,13 @@
"role.nobody.description": "This is a fallback role without any permissions",
"role.nobody.title": "Nobody",
"save": "Запиши",
"save": "\u0417\u0430\u043f\u0438\u0448\u0438",
"search": "Търси",
"search.min": "Enter {min} characters to search",
"search.all": "Show all",
"search.all": "Show all {count} results",
"search.results.none": "No results",
"section.invalid": "The section is invalid",
"section.required": "The section is required",
"security": "Security",
@@ -496,13 +603,18 @@
"show": "Show",
"site.blueprint": "The site has no blueprint yet. You can define the setup in <strong>/site/blueprints/site.yml</strong>",
"size": "Размер",
"slug": "URL-добавка",
"slug": "URL-\u0434\u043e\u0431\u0430\u0432\u043a\u0430",
"sort": "Сортирай",
"sort.drag": "Drag to sort …",
"split": "Split",
"stats.empty": "No reports",
"status": "Status",
"system.issues.content": "The content folder seems to be exposed",
"system.issues.eol.kirby": "Your installed Kirby version has reached end-of-life and will not receive further security updates",
"system.issues.eol.plugin": "Your installed version of the { plugin } plugin is has reached end-of-life and will not receive further security updates",
"system.issues.eol.php": "Your installed PHP release { release } has reached end-of-life and will not receive further security updates",
"system.issues.debug": "Debugging must be turned off in production",
"system.issues.git": "The .git folder seems to be exposed",
"system.issues.https": "We recommend HTTPS for all your sites",
@@ -520,12 +632,15 @@
"system.updateStatus.update": "Free update { version } available",
"system.updateStatus.upgrade": "Upgrade { version } available",
"title": "Заглавие",
"tel": "Phone",
"tel.placeholder": "+49123456789",
"template": "Образец",
"title": "Заглавие",
"today": "Днес",
"toolbar.button.clear": "Clear formatting",
"toolbar.button.code": "Код",
"toolbar.button.bold": "Получер шрифт",
"toolbar.button.bold": "\u041f\u043e\u043b\u0443\u0447\u0435\u0440 \u0448\u0440\u0438\u0444\u0442",
"toolbar.button.email": "Email",
"toolbar.button.headings": "Заглавия",
"toolbar.button.heading.1": "Заглавие 1",
@@ -534,13 +649,15 @@
"toolbar.button.heading.4": "Heading 4",
"toolbar.button.heading.5": "Heading 5",
"toolbar.button.heading.6": "Heading 6",
"toolbar.button.italic": "Наклонен шрифт",
"toolbar.button.italic": "\u041d\u0430\u043a\u043b\u043e\u043d\u0435\u043d \u0448\u0440\u0438\u0444\u0442",
"toolbar.button.file": "Файл",
"toolbar.button.file.select": "Select a file",
"toolbar.button.file.upload": "Upload a file",
"toolbar.button.link": "Връзка",
"toolbar.button.link": "\u0412\u0440\u044a\u0437\u043a\u0430",
"toolbar.button.paragraph": "Paragraph",
"toolbar.button.strike": "Strike-through",
"toolbar.button.sub": "Subscript",
"toolbar.button.sup": "Superscript",
"toolbar.button.ol": "Подреден списък",
"toolbar.button.underline": "Underline",
"toolbar.button.ul": "Списък",
@@ -550,6 +667,8 @@
"translation.name": "Български",
"translation.locale": "bg_BG",
"type": "Type",
"upload": "Прикачи",
"upload.error.cantMove": "The uploaded file could not be moved",
"upload.error.cantWrite": "Failed to write file to disk",
@@ -562,7 +681,7 @@
"upload.error.noFiles": "No files were uploaded",
"upload.error.partial": "The uploaded file was only partially uploaded",
"upload.error.tmpDir": "Missing a temporary folder",
"upload.errors": "Error",
"upload.errors": "Грешка",
"upload.progress": "Uploading…",
"url": "Url",
@@ -584,18 +703,18 @@
"users": "Потребители",
"version": "Версия на Kirby",
"version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Kirby",
"version.current": "Current version",
"version.latest": "Latest version",
"versionInformation": "Version information",
"view.account": "Вашия акаунт",
"view.installation": "Инсталация",
"view.account": "\u0412\u0430\u0448\u0438\u044f \u0430\u043a\u0430\u0443\u043d\u0442",
"view.installation": "\u0418\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f",
"view.languages": "Езици",
"view.resetPassword": "Reset password",
"view.site": "Сайт",
"view.system": "System",
"view.users": "Потребители",
"view.users": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438",
"welcome": "Добре дошли",
"year": "Year",

View File

@@ -3,19 +3,26 @@
"account.delete": "Delete your account",
"account.delete.confirm": "Do you really want to delete your account? You will be logged out immediately. Your account cannot be recovered.",
"activate": "Activate",
"add": "Afegir",
"alpha": "Alpha",
"author": "Author",
"avatar": "Imatge del perfil",
"back": "Tornar",
"cancel": "Cancel·lar",
"cancel": "Cancel\u00b7lar",
"change": "Canviar",
"close": "Tancar",
"changes": "Changes",
"confirm": "Ok",
"collapse": "Col·lapsar",
"collapse.all": "Col·lapsar tot",
"color": "Color",
"coordinates": "Coordinates",
"copy": "Copiar",
"copy.all": "Copy all",
"copy.success": "{count} copied!",
"create": "Crear",
"custom": "Custom",
"date": "Data",
"date.select": "Selecciona una data",
@@ -34,13 +41,20 @@
"delete": "Eliminar",
"delete.all": "Eliminar tot",
"dialog.fields.empty": "This dialog has no fields",
"dialog.files.empty": "No hi ha cap fitxer per seleccionar",
"dialog.pages.empty": "No hi ha cap pàgina per seleccionar",
"dialog.text.empty": "This dialog does not define any text",
"dialog.users.empty": "No hi ha cap usuari per seleccionar",
"dimensions": "Dimensions",
"disable": "Disable",
"disabled": "Desactivat",
"discard": "Descartar",
"drawer.fields.empty": "This drawer has no fields",
"domain": "Domain",
"download": "Descarregar",
"duplicate": "Duplicar",
@@ -49,11 +63,13 @@
"email": "Email",
"email.placeholder": "mail@exemple.com",
"enter": "Enter",
"entries": "Entries",
"entry": "Entry",
"environment": "Environment",
"error": "Error",
"error.access.code": "Codi invàlid",
"error.access.login": "Inici de sessió no vàlid",
"error.access.panel": "No tens permís per accedir al panell",
@@ -81,6 +97,9 @@
"error.file.changeName.empty": "El nom no pot estar buit",
"error.file.changeName.permission": "No tens permís per canviar el nom de \"{filename}\"",
"error.file.changeTemplate.invalid": "The template for the file \"{id}\" cannot be changed to \"{template}\" (valid: \"{blueprints}\")",
"error.file.changeTemplate.permission": "You are not allowed to change the template for the file \"{id}\"",
"error.file.duplicate": "Ja existeix un fitxer amb el nom \"{filename}\"",
"error.file.extension.forbidden": "L'extensió de l'arxiu \"{extension}\" no està permesa",
"error.file.extension.invalid": "Invalid extension: {extension}",
@@ -95,6 +114,7 @@
"error.file.minheight": "L'alçada de la imatge ha de ser com a mínim de {height} píxels",
"error.file.minsize": "El fitxer és massa petit",
"error.file.minwidth": "L'amplada de la imatge ha de ser com a mínim de {width} píxels",
"error.file.name.unique": "The filename must be unique",
"error.file.name.missing": "El nom del fitxer no pot estar buit",
"error.file.notFound": "L'arxiu \"{filename}\" no s'ha trobat",
"error.file.orientation": "Lorientació de la imatge ha de ser \"{orientation}\"",
@@ -113,15 +133,20 @@
"error.layout.validation.block": "There's an error on the \"{field}\" field in block {blockIndex} using the \"{fieldset}\" block type in layout {layoutIndex}",
"error.layout.validation.settings": "There's an error in layout {index} settings",
"error.license.format": "Introduïu una clau de llicència vàlida",
"error.license.domain": "The domain for the license is missing",
"error.license.email": "Si us plau, introdueix una adreça de correu electrònic vàlida",
"error.license.format": "Please enter a valid license code",
"error.license.verification": "No sha pogut verificar la llicència",
"error.login.totp.confirm.invalid": "Codi invàlid",
"error.login.totp.confirm.missing": "Please enter the current code",
"error.object.validation": "Theres an error in the \"{label}\" field:\n{message}",
"error.offline": "The Panel is currently offline",
"error.page.changeSlug.permission": "No teniu permís per canviar l'apèndix d'URL per a \"{slug}\"",
"error.page.changeSlug.reserved": "The path of top-level pages must not start with \"{path}\"",
"error.page.changeStatus.incomplete": "La pàgina té errors i no es pot publicar",
"error.page.changeStatus.permission": "No es pot canviar l'estat d'aquesta pàgina",
"error.page.changeStatus.toDraft.invalid": "La pàgina \"{slug}\" no es pot convertir en un esborrany",
@@ -137,13 +162,19 @@
"error.page.draft.duplicate": "Ja existeix un esborrany de pàgina amb l'apèndix d'URL \"{slug}\"",
"error.page.duplicate": "Ja existeix una pàgina amb l'apèndix d'URL \"{slug}\"",
"error.page.duplicate.permission": "No tens permís per duplicar \"{slug}\"",
"error.page.move.ancestor": "The page cannot be moved into itself",
"error.page.move.directory": "The page directory cannot be moved",
"error.page.move.duplicate": "A sub page with the URL appendix \"{slug}\" already exists",
"error.page.move.notFound": "The moved page could not be found",
"error.page.move.permission": "You are not allowed to move \"{slug}\"",
"error.page.move.template": "The \"{template}\" template is not accepted as a subpage of \"{parent}\"",
"error.page.notFound": "La pàgina \"{slug}\" no s'ha trobat",
"error.page.num.invalid": "Si us plau, introdueix un número d 'ordenació vàlid. Els números no poden ser negatius.",
"error.page.slug.invalid": "Please enter a valid URL appendix",
"error.page.slug.maxlength": "La longitud del nom ha de tenir menys de caràcters \"{length}\"",
"error.page.sort.permission": "La pàgina \"{slug}\" no es pot ordenar",
"error.page.status.invalid": "Si us plau, estableix un estat de pàgina vàlid",
"error.page.undefined": "La pàgina no s'ha trobat",
"error.page.undefined": "La p\u00e0gina no s'ha trobat",
"error.page.update.permission": "No tens permís per actualitzar \"{slug}\"",
"error.section.files.max.plural": "No has d'afegir més de {max} fitxers a la secció \"{section}\"",
@@ -163,6 +194,8 @@
"error.site.changeTitle.permission": "No tens permís per canviar el títol del lloc web",
"error.site.update.permission": "No tens permís per actualitzar el lloc web",
"error.structure.validation": "There's an error on the \"{field}\" field in row {index}",
"error.template.default.notFound": "La plantilla predeterminada no existeix",
"error.unexpected": "An unexpected error occurred! Enable debug mode for more info: https://getkirby.com/docs/reference/system/options/debug",
@@ -176,7 +209,7 @@
"error.user.changeRole.toAdmin": "No tens permís per promocionar algú al rol dadministrador",
"error.user.create.permission": "No tens permís per crear aquest usuari",
"error.user.delete": "L'usuari \"{name}\" no es pot eliminar",
"error.user.delete.lastAdmin": "No es pot eliminar l'últim administrador",
"error.user.delete.lastAdmin": "No es pot eliminar l'\u00faltim administrador",
"error.user.delete.lastUser": "El darrer usuari no es pot eliminar",
"error.user.delete.permission": "No pots eliminar l'usuari \"{name}\"",
"error.user.duplicate": "Ja existeix un usuari amb l'adreça electrònica \"{email}\"",
@@ -195,8 +228,10 @@
"error.validation.accepted": "Si us plau confirma",
"error.validation.alpha": "Si us plau, introdueix únicament caràcters entre a-z",
"error.validation.alphanum": "Si us plau, introdueix únicament caràcters entre a-z o números de 0-9",
"error.validation.anchor": "Please enter a correct link anchor",
"error.validation.between": "Introdueix un valor entre \"{min}\" i \"{max}\"",
"error.validation.boolean": "Si us plau confirma o denega",
"error.validation.color": "Please enter a valid color in the {format} format",
"error.validation.contains": "Si us plau, introduïu un valor que contingui \"{needle}\"",
"error.validation.date": "Si us plau, introdueix una data vàlida",
"error.validation.date.after": "Introdueix una data posterior {date}",
@@ -211,6 +246,7 @@
"error.validation.integer": "Si us plau, introduïu un nombre enter vàlid",
"error.validation.ip": "Si us plau, introduïu una adreça IP vàlida",
"error.validation.less": "Si us plau, introduïu un valor inferior a {max}",
"error.validation.linkType": "The link type is not allowed",
"error.validation.match": "El valor no coincideix amb el patró esperat",
"error.validation.max": "Si us plau, introduïu un valor igual o inferior a {max}",
"error.validation.maxlength": "Si us plau, introduïu un valor més curt. (màxim {max} caràcters)",
@@ -227,15 +263,18 @@
"error.validation.same": "Si us plau, introduïu \"{other}\"",
"error.validation.size": "La mida del valor ha de ser \"{size}\"",
"error.validation.startswith": "El valor ha de començar amb \"{start}\"",
"error.validation.tel": "Please enter an unformatted phone number",
"error.validation.time": "Si us plau, introduïu una hora vàlida",
"error.validation.time.after": "Please enter a time after {time}",
"error.validation.time.before": "Please enter a time before {time}",
"error.validation.time.between": "Please enter a time between {min} and {max}",
"error.validation.uuid": "Please enter a valid UUID",
"error.validation.url": "Si us plau, introduïu una URL vàlida",
"expand": "Expandir",
"expand.all": "Expandir tot",
"field.invalid": "The field is invalid",
"field.required": "El camp és obligatori",
"field.blocks.changeType": "Change type",
"field.blocks.code.name": "Codi",
@@ -245,8 +284,9 @@
"field.blocks.delete.confirm.all": "Do you really want to delete all blocks?",
"field.blocks.delete.confirm.selected": "Do you really want to delete the selected blocks?",
"field.blocks.empty": "No blocks yet",
"field.blocks.fieldsets.empty": "No fieldsets yet",
"field.blocks.fieldsets.label": "Please select a block type …",
"field.blocks.fieldsets.paste": "Press <kbd>{{ shortcut }}</kbd> to paste/import blocks from your clipboard",
"field.blocks.fieldsets.paste": "Press <kbd>{{ shortcut }}</kbd> to import layouts/blocks from your clipboard <small>Only those allowed in the current field will get inserted.</small>",
"field.blocks.gallery.name": "Gallery",
"field.blocks.gallery.images.empty": "No images yet",
"field.blocks.gallery.images.label": "Images",
@@ -259,6 +299,8 @@
"field.blocks.image.crop": "Crop",
"field.blocks.image.link": "Enllaç",
"field.blocks.image.location": "Location",
"field.blocks.image.location.internal": "This website",
"field.blocks.image.location.external": "External source",
"field.blocks.image.name": "Imatge",
"field.blocks.image.placeholder": "Select an image",
"field.blocks.image.ratio": "Ratio",
@@ -283,8 +325,10 @@
"field.files.empty": "Encara no hi ha cap fitxer seleccionat",
"field.layout.change": "Change layout",
"field.layout.delete": "Delete layout",
"field.layout.delete.confirm": "Do you really want to delete this layout?",
"field.layout.delete.confirm.all": "Do you really want to delete all layouts?",
"field.layout.empty": "No rows yet",
"field.layout.select": "Select a layout",
@@ -298,15 +342,26 @@
"field.users.empty": "Encara no s'ha seleccionat cap usuari",
"fields.empty": "No fields yet",
"file": "Arxiu",
"file.blueprint": "This file has no blueprint yet. You can define the setup in <strong>/site/blueprints/files/{blueprint}.yml</strong>",
"file.changeTemplate": "Canviar la plantilla",
"file.changeTemplate.notice": "Changing the file's template will remove content for fields that don't match in type. If the new template defines certain rules, e.g. image dimensions, those will also be applied irreversibly. Use with caution.",
"file.delete.confirm": "Esteu segurs d'eliminar <br><strong>{filename}</strong>?",
"file.focus.placeholder": "Set focal point",
"file.focus.reset": "Remove focal point",
"file.focus.title": "Focus",
"file.sort": "Change position",
"files": "Arxius",
"files.empty": "Encara no hi ha fitxers",
"filter": "Filter",
"hide": "Hide",
"hour": "Hora",
"hue": "Hue",
"import": "Import",
"info": "Info",
"insert": "Insertar",
@@ -332,6 +387,7 @@
"language.convert": "Fer per defecte",
"language.convert.confirm": "<p>Segur que voleu convertir <strong>{name}</strong> a l'idioma predeterminat? Això no es pot desfer.</p> <p> Si <strong>{name}</strong> té contingut no traduït, ja no podreu tornar enrere i algunes parts del vostre lloc poden quedar buides.</p>",
"language.create": "Afegir un nou idioma",
"language.default": "Idioma per defecte",
"language.delete.confirm": "Segur que voleu eliminar l'idioma <strong>{name}</strong> incloent totes les traduccions? Això no es pot desfer!",
"language.deleted": "S'ha suprimit l'idioma",
"language.direction": "Direcció de lectura",
@@ -340,7 +396,16 @@
"language.locale": "Cadena local de PHP",
"language.locale.warning": "S'està fent servir una configuració regional personalitzada. Modifica el fitxer d'idioma a /site/languages",
"language.name": "Nom",
"language.secondary": "Secondary language",
"language.settings": "Language settings",
"language.updated": "S'ha actualitzat l'idioma",
"language.variables": "Language variables",
"language.variables.empty": "No translations yet",
"language.variable.delete.confirm": "Do you really want to delete the variable for {key}?",
"language.variable.key": "Key",
"language.variable.notFound": "The variable could not be found",
"language.variable.value": "Value",
"languages": "Idiomes",
"languages.default": "Idioma per defecte",
@@ -348,36 +413,52 @@
"languages.secondary": "Idiomes secundaris",
"languages.secondary.empty": "Encara no hi ha idiomes secundaris",
"license": "Llicència Kirby",
"license": "Llic\u00e8ncia Kirby",
"license.activate": "Activate it now",
"license.activate.label": "Please activate your license",
"license.activate.domain": "Your license will be activated for <strong>{host}</strong>.",
"license.activate.local": "You are about to activate your Kirby license for your local domain <strong>{host}</strong>. If this site will be deployed to a public domain, please activate it there instead. If {host} is the domain you want to use your license for, please continue.",
"license.activated": "Activated",
"license.buy": "Comprar una llicència",
"license.register": "Registrar",
"license.code": "Codi",
"license.code.help": "You received your license code after the purchase via email. Please copy and paste it here.",
"license.code.label": "Si us plau, introdueixi el seu codi de llicència",
"license.status.active.info": "Includes new major versions until {date}",
"license.status.active.label": "Valid license",
"license.status.demo.info": "This is a demo installation",
"license.status.demo.label": "Demo",
"license.status.inactive.info": "Renew license to update to new major versions",
"license.status.inactive.label": "No new major versions",
"license.status.legacy.bubble": "Ready to renew your license?",
"license.status.legacy.info": "Your license does not cover this version",
"license.status.legacy.label": "Please renew your license",
"license.status.missing.bubble": "Ready to launch your site?",
"license.status.missing.info": "No valid license",
"license.status.missing.label": "Please activate your license",
"license.manage": "Manage your licenses",
"license.register.help": "Heu rebut el codi de la vostra llicència després de la compra, per correu electrònic. Copieu-lo i enganxeu-lo per registrar-vos.",
"license.register.label": "Si us plau, introdueixi el seu codi de llicència",
"license.register.domain": "Your license will be registered to <strong>{host}</strong>.",
"license.register.local": "You are about to register your license for your local domain <strong>{host}</strong>. If this site will be deployed to a public domain, please register it there instead. If {host} is the domain you want to license Kirby to, please continue.",
"license.register.success": "Gràcies per donar suport a Kirby",
"license.unregistered": "Aquesta és una demo no registrada de Kirby",
"license.purchased": "Purchased",
"license.success": "Gràcies per donar suport a Kirby",
"license.unregistered.label": "Unregistered",
"link": "Enllaç",
"link": "Enlla\u00e7",
"link.text": "Enllaç de text",
"loading": "Carregant",
"lock.unsaved": "Canvis no guardats",
"lock.unsaved.empty": "Ja no hi ha canvis no guardats",
"lock.isLocked": "Canvis no guardats per <strong>{email}</strong>",
"lock.file.isLocked": "El fitxer està sent editat actualment per {email} i no pot ser modificat.",
"lock.page.isLocked": "La pàgina està sent editat actualment per {email} i no pot ser modificat.",
"lock.isLocked": "Unsaved changes by {email}",
"lock.unlock": "Desbloquejar",
"lock.isUnlocked": "Els teus canvis sense guardar han estat sobreescrits per a un altra usuario. Pots descarregar els teus canvis per combinar-los manualment.",
"lock.unlock.submit": "Unlock and overwrite unsaved changes by <strong>{email}</strong>",
"lock.isUnlocked": "Was unlocked by another user",
"login": "Entrar",
"login.code.label.login": "Login code",
"login.code.label.password-reset": "Password reset code",
"login.code.placeholder.email": "000 000",
"login.code.placeholder.totp": "000000",
"login.code.text.email": "If your email address is registered, the requested code was sent via email.",
"login.code.text.totp": "Please enter the onetime code from your authenticator app.",
"login.email.login.body": "Hi {user.nameOrEmail},\n\nYou recently requested a login code for the Panel of {site}.\nThe following login code will be valid for {timeout} minutes:\n\n{code}\n\nIf you did not request a login code, please ignore this email or contact your administrator if you have questions.\nFor security, please DO NOT forward this email.",
"login.email.login.subject": "Your login code",
"login.email.password-reset.body": "Hi {user.nameOrEmail},\n\nYou recently requested a password reset code for the Panel of {site}.\nThe following password reset code will be valid for {timeout} minutes:\n\n{code}\n\nIf you did not request a password reset code, please ignore this email or contact your administrator if you have questions.\nFor security, please DO NOT forward this email.",
@@ -388,9 +469,24 @@
"login.toggleText.code.email-password": "Login with password",
"login.toggleText.password-reset.email": "Forgot your password?",
"login.toggleText.password-reset.email-password": "← Back to login",
"login.totp.enable.option": "Set up onetime codes",
"login.totp.enable.intro": "Authenticator apps can generate onetime codes that are used as a second factor when signing into your account.",
"login.totp.enable.qr.label": "1. Scan this QR code",
"login.totp.enable.qr.help": "Unable to scan? Add the setup key <code>{secret}</code> manually to your authenticator app.",
"login.totp.enable.confirm.headline": "2. Confirm with generated code",
"login.totp.enable.confirm.text": "Your app generates a new onetime code every 30 seconds. Enter the current code to complete the setup:",
"login.totp.enable.confirm.label": "Current code",
"login.totp.enable.confirm.help": "After this setup, we will ask you for a onetime code every time you log in.",
"login.totp.enable.success": "Onetime codes enabled",
"login.totp.disable.option": "Disable onetime codes",
"login.totp.disable.label": "Enter your password to disable onetime codes",
"login.totp.disable.help": "In the future, a different second factor like a login code sent via email will be requested when you log in. You can always set up onetime codes again later.",
"login.totp.disable.admin": "<p>This will disable onetime codes for <strong>{user}</strong>.</p><p>In the future, a different second factor like a login code sent via email will be requested when they log in. {user} can set up onetime codes again after their next login.</p>",
"login.totp.disable.success": "Onetime codes disabled",
"logout": "Tancar sessió",
"merge": "Merge",
"menu": "Menú",
"meridiem": "AM/PM",
"mime": "Tipus de mitjà",
@@ -404,42 +500,49 @@
"months.january": "Gener",
"months.july": "Juliol",
"months.june": "Juny",
"months.march": "Març",
"months.march": "Mar\u00e7",
"months.may": "Maig",
"months.november": "Novembre",
"months.october": "Octubre",
"months.september": "Setembre",
"more": "Més",
"move": "Move",
"name": "Nom",
"next": "Següent",
"night": "Night",
"no": "no",
"off": "apagat",
"on": "encès",
"open": "Obrir",
"open.newWindow": "Open in new window",
"option": "Option",
"options": "Opcions",
"options.none": "Sense opcions",
"options.all": "Show all {count} options",
"orientation": "Orientació",
"orientation.landscape": "Horitzontal",
"orientation.portrait": "Vertical",
"orientation.square": "Quadrat",
"page": "Page",
"page.blueprint": "This page has no blueprint yet. You can define the setup in <strong>/site/blueprints/pages/{blueprint}.yml</strong>",
"page.changeSlug": "Canviar URL",
"page.changeSlug.fromTitle": "Crear a partir del títol",
"page.changeSlug.fromTitle": "Crear a partir del t\u00edtol",
"page.changeStatus": "Canviar l'estat",
"page.changeStatus.position": "Si us plau, seleccioneu una posició",
"page.changeStatus.select": "Seleccioneu un nou estat",
"page.changeTemplate": "Canviar la plantilla",
"page.changeTemplate.notice": "Changing the page's template will remove content for fields that don't match in type. Use with caution.",
"page.create": "Create as {status}",
"page.delete.confirm": "Segur que voleu eliminar <strong>{title}</strong>?",
"page.delete.confirm.subpages": "<strong>Aquesta pàgina té subpàgines</strong>. <br>Totes les subpàgines també s'eliminaran.",
"page.delete.confirm.title": "Introduïu el títol de la pàgina per confirmar",
"page.draft.create": "Crear un esborrany",
"page.duplicate.appendix": "Copiar",
"page.duplicate.files": "Copiar fitxers",
"page.duplicate.pages": "Copiar pàgines",
"page.move": "Move page",
"page.sort": "Change position",
"page.status": "Estat",
"page.status.draft": "Esborrany",
@@ -455,11 +558,12 @@
"pages.status.listed": "Publicat",
"pages.status.unlisted": "Sense classificar",
"pagination.page": "Page",
"pagination.page": "Pàgina",
"password": "Contrasenya",
"paste": "Paste",
"paste.after": "Paste after",
"paste.success": "{count} pasted!",
"pixel": "Pixel",
"plugin": "Plugin",
"plugins": "Plugins",
@@ -467,7 +571,9 @@
"preview": "Preview",
"remove": "Eliminar",
"rename": "Canviar el nom",
"replace": "Reemplaçar",
"renew": "Renew",
"replace": "Reempla\u00e7ar",
"replace.with": "Replace with",
"retry": "Reintentar",
"revert": "Revertir",
"revert.confirm": "Segur que voleu eliminar <strong>tots els canvis pendents desar</strong>?",
@@ -484,9 +590,10 @@
"save": "Desar",
"search": "Cercar",
"search.min": "Introduïu {min} caràcters per cercar",
"search.all": "Mostrar tots",
"search.all": "Show all {count} results",
"search.results.none": "Sense resultats",
"section.invalid": "The section is invalid",
"section.required": "La secció és obligatòria",
"security": "Security",
@@ -496,13 +603,18 @@
"show": "Show",
"site.blueprint": "The site has no blueprint yet. You can define the setup in <strong>/site/blueprints/site.yml</strong>",
"size": "Tamany",
"slug": "URL-apèndix",
"slug": "URL-ap\u00e8ndix",
"sort": "Ordenar",
"sort.drag": "Drag to sort …",
"split": "Split",
"stats.empty": "No reports",
"status": "Estat",
"system.issues.content": "The content folder seems to be exposed",
"system.issues.eol.kirby": "Your installed Kirby version has reached end-of-life and will not receive further security updates",
"system.issues.eol.plugin": "Your installed version of the { plugin } plugin is has reached end-of-life and will not receive further security updates",
"system.issues.eol.php": "Your installed PHP release { release } has reached end-of-life and will not receive further security updates",
"system.issues.debug": "Debugging must be turned off in production",
"system.issues.git": "The .git folder seems to be exposed",
"system.issues.https": "We recommend HTTPS for all your sites",
@@ -520,10 +632,13 @@
"system.updateStatus.update": "Free update { version } available",
"system.updateStatus.upgrade": "Upgrade { version } available",
"title": "Títol",
"tel": "Phone",
"tel.placeholder": "+49123456789",
"template": "Plantilla",
"title": "Títol",
"today": "Avui",
"toolbar.button.clear": "Clear formatting",
"toolbar.button.code": "Codi",
"toolbar.button.bold": "Negreta",
"toolbar.button.email": "Email",
@@ -538,9 +653,11 @@
"toolbar.button.file": "Arxiu",
"toolbar.button.file.select": "Selecciona un fitxer",
"toolbar.button.file.upload": "Carrega un fitxer",
"toolbar.button.link": "Enllaç",
"toolbar.button.link": "Enlla\u00e7",
"toolbar.button.paragraph": "Paragraph",
"toolbar.button.strike": "Strike-through",
"toolbar.button.sub": "Subscript",
"toolbar.button.sup": "Superscript",
"toolbar.button.ol": "Llista ordenada",
"toolbar.button.underline": "Underline",
"toolbar.button.ul": "Llista de vinyetes",
@@ -550,6 +667,8 @@
"translation.name": "Catalan",
"translation.locale": "ca_ES",
"type": "Type",
"upload": "Carregar",
"upload.error.cantMove": "El fitxer carregat no s'ha pogut moure",
"upload.error.cantWrite": "No s'ha pogut escriure el fitxer al disc",
@@ -584,7 +703,7 @@
"users": "Usuaris",
"version": "Versió de Kirby",
"version": "Versi\u00f3 de Kirby",
"version.current": "Current version",
"version.latest": "Latest version",
"versionInformation": "Version information",

Some files were not shown because too many files have changed in this diff Show More