#164 fix comment update

This commit is contained in:
isUnknown 2025-09-23 08:20:17 +02:00
parent 13ac9eb6bb
commit 2e0f28a13f
48 changed files with 3732 additions and 428 deletions

109
public/composer.lock generated
View file

@ -272,16 +272,16 @@
},
{
"name": "getkirby/cms",
"version": "4.7.0",
"version": "4.8.0",
"source": {
"type": "git",
"url": "https://github.com/getkirby/kirby.git",
"reference": "938fe98951cace6c77aab744779bf4e0799ad705"
"reference": "5292c17832dd34b0e5f3e98dea837a357ef037b6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/getkirby/kirby/zipball/938fe98951cace6c77aab744779bf4e0799ad705",
"reference": "938fe98951cace6c77aab744779bf4e0799ad705",
"url": "https://api.github.com/repos/getkirby/kirby/zipball/5292c17832dd34b0e5f3e98dea837a357ef037b6",
"reference": "5292c17832dd34b0e5f3e98dea837a357ef037b6",
"shasum": ""
},
"require": {
@ -301,13 +301,13 @@
"ext-simplexml": "*",
"filp/whoops": "2.18.0",
"getkirby/composer-installer": "^1.2.1",
"laminas/laminas-escaper": "2.16.0",
"laminas/laminas-escaper": "2.17.0",
"michelf/php-smartypants": "1.8.1",
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
"phpmailer/phpmailer": "6.9.3",
"symfony/polyfill-intl-idn": "1.31.0",
"symfony/polyfill-mbstring": "1.31.0",
"symfony/yaml": "6.4.18"
"phpmailer/phpmailer": "6.10.0",
"symfony/polyfill-intl-idn": "1.32.0",
"symfony/polyfill-mbstring": "1.32.0",
"symfony/yaml": "6.4.21"
},
"replace": {
"symfony/polyfill-php72": "*"
@ -371,7 +371,7 @@
"type": "custom"
}
],
"time": "2025-03-25T11:15:09+00:00"
"time": "2025-06-03T09:52:03+00:00"
},
{
"name": "getkirby/composer-installer",
@ -477,16 +477,16 @@
},
{
"name": "laminas/laminas-escaper",
"version": "2.16.0",
"version": "2.17.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-escaper.git",
"reference": "9cf1f5317ca65b4fd5c6a3c2855e24a187b288c8"
"reference": "df1ef9503299a8e3920079a16263b578eaf7c3ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/9cf1f5317ca65b4fd5c6a3c2855e24a187b288c8",
"reference": "9cf1f5317ca65b4fd5c6a3c2855e24a187b288c8",
"url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/df1ef9503299a8e3920079a16263b578eaf7c3ba",
"reference": "df1ef9503299a8e3920079a16263b578eaf7c3ba",
"shasum": ""
},
"require": {
@ -534,7 +534,7 @@
"type": "community_bridge"
}
],
"time": "2025-02-17T12:40:19+00:00"
"time": "2025-05-06T19:29:36+00:00"
},
{
"name": "league/color-extractor",
@ -653,16 +653,16 @@
},
{
"name": "phpmailer/phpmailer",
"version": "v6.9.3",
"version": "v6.10.0",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e"
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e",
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
"shasum": ""
},
"require": {
@ -722,7 +722,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.9.3"
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0"
},
"funding": [
{
@ -730,7 +730,7 @@
"type": "github"
}
],
"time": "2024-11-24T18:04:13+00:00"
"time": "2025-04-24T15:19:31+00:00"
},
{
"name": "psr/log",
@ -784,16 +784,16 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.5.1",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
"shasum": ""
},
"require": {
@ -806,7 +806,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.5-dev"
"dev-main": "3.6-dev"
}
},
"autoload": {
@ -831,7 +831,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
},
"funding": [
{
@ -847,11 +847,11 @@
"type": "tidelift"
}
],
"time": "2024-09-25T14:20:29+00:00"
"time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.31.0",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@ -910,7 +910,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
@ -921,6 +921,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@ -930,16 +934,16 @@
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.31.0",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "c36586dcf89a12315939e00ec9b4474adcb1d773"
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773",
"reference": "c36586dcf89a12315939e00ec9b4474adcb1d773",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3",
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3",
"shasum": ""
},
"require": {
@ -993,7 +997,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0"
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0"
},
"funding": [
{
@ -1009,11 +1013,11 @@
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
"time": "2024-09-10T14:38:51+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.31.0",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@ -1074,7 +1078,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
},
"funding": [
{
@ -1085,6 +1089,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@ -1094,19 +1102,20 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.31.0",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
@ -1154,7 +1163,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
},
"funding": [
{
@ -1170,20 +1179,20 @@
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
"time": "2024-12-23T08:48:59+00:00"
},
{
"name": "symfony/yaml",
"version": "v6.4.18",
"version": "v6.4.21",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5"
"reference": "f01987f45676778b474468aa266fe2eda1f2bc7e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5",
"reference": "bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5",
"url": "https://api.github.com/repos/symfony/yaml/zipball/f01987f45676778b474468aa266fe2eda1f2bc7e",
"reference": "f01987f45676778b474468aa266fe2eda1f2bc7e",
"shasum": ""
},
"require": {
@ -1226,7 +1235,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v6.4.18"
"source": "https://github.com/symfony/yaml/tree/v6.4.21"
},
"funding": [
{
@ -1242,7 +1251,7 @@
"type": "tidelift"
}
],
"time": "2025-01-07T09:44:41+00:00"
"time": "2025-04-04T09:48:44+00:00"
}
],
"packages-dev": [],

View file

@ -1,14 +1,14 @@
##
## Bundle of CA Root Certificates
##
## Certificate data from Mozilla as of: Tue Feb 25 04:12:03 2025 GMT
## Certificate data from Mozilla as of: Tue May 20 03:12:02 2025 GMT
##
## Find updated versions here: https://curl.se/docs/caextract.html
##
## This is a bundle of X.509 certificates of public Certificate Authorities
## (CA). These were automatically extracted from Mozilla's root certificates
## file (certdata.txt). This file can be found in the mozilla source tree:
## https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt
## https://raw.githubusercontent.com/mozilla-firefox/firefox/refs/heads/release/security/nss/lib/ckfw/builtins/certdata.txt
##
## It contains the certificates in PEM format and therefore
## can be directly used with curl / libcurl / php_curl, or with
@ -16,76 +16,10 @@
## Just configure this file as the SSLCACertificateFile.
##
## Conversion done with mk-ca-bundle.pl version 1.29.
## SHA256: 620fd89c02acb0019f1899dab7907db5d20735904f5a9a0d3a8771a5857ac482
## SHA256: 8944ec6b572b577daee4fc681a425881f841ec2660e4cb5f0eee727f84620697
##
GlobalSign Root CA
==================
-----BEGIN CERTIFICATE-----
MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx
GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds
b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV
BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD
VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa
DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc
THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb
Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP
c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX
gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF
AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj
Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG
j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH
hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC
X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
-----END CERTIFICATE-----
Entrust.net Premium 2048 Secure Server CA
=========================================
-----BEGIN CERTIFICATE-----
MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u
ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp
bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV
BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx
NzUwNTFaFw0yOTA3MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3
d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl
MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u
ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL
Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr
hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW
nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi
VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo0IwQDAOBgNVHQ8BAf8E
BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJ
KoZIhvcNAQEFBQADggEBADubj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPy
T/4xmf3IDExoU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf
zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5bu/8j72gZyxKT
J1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+bYQLCIt+jerXmCHG8+c8eS9e
nNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/ErfF6adulZkMV8gzURZVE=
-----END CERTIFICATE-----
Baltimore CyberTrust Root
=========================
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJRTESMBAGA1UE
ChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3li
ZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoXDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMC
SUUxEjAQBgNVBAoTCUJhbHRpbW9yZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFs
dGltb3JlIEN5YmVyVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKME
uyKrmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjrIZ3AQSsB
UnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeKmpYcqWe4PwzV9/lSEy/C
G9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSuXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9
XbIGevOF6uvUA65ehD5f/xXtabz5OTZydc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjpr
l3RjM71oGDHweI12v/yejl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoI
VDaGezq1BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB
BQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT929hkTI7gQCvlYpNRh
cL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3WgxjkzSswF07r51XgdIGn9w/xZchMB5
hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsa
Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H
RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
-----END CERTIFICATE-----
Entrust Root Certification Authority
====================================
-----BEGIN CERTIFICATE-----
@ -112,30 +46,6 @@ W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0
tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8
-----END CERTIFICATE-----
Comodo AAA Services root
========================
-----BEGIN CERTIFICATE-----
MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS
R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg
TGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAw
MFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hl
c3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQuaBtDFcCLNSS1UY8y2bmhG
C1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe3M/vg4aijJRPn2jymJBGhCfHdr/jzDUs
i14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszW
Y19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjH
Ypy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEK
Iz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0f
BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNl
cy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2Vz
LmNybDANBgkqhkiG9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm
7l3sAg9g1o1QGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z
8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C
12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
-----END CERTIFICATE-----
QuoVadis Root CA 2
==================
-----BEGIN CERTIFICATE-----
@ -202,78 +112,6 @@ vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr
qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto=
-----END CERTIFICATE-----
XRamp Global CA Root
====================
-----BEGIN CERTIFICATE-----
MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UE
BhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2Vj
dXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB
dXRob3JpdHkwHhcNMDQxMTAxMTcxNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMx
HjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkg
U2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
dHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS638eMpSe2OAtp87ZOqCwu
IR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCPKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMx
foArtYzAQDsRhtDLooY2YKTVMIJt2W7QDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FE
zG+gSqmUsE3a56k0enI4qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqs
AxcZZPRaJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNViPvry
xS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud
EwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASsjVy16bYbMDYGA1UdHwQvMC0wK6Ap
oCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMC
AQEwDQYJKoZIhvcNAQEFBQADggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc
/Kh4ZzXxHfARvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt
qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLaIR9NmXmd4c8n
nxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSyi6mx5O+aGtA9aZnuqCij4Tyz
8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQO+7ETPTsJ3xCwnR8gooJybQDJbw=
-----END CERTIFICATE-----
Go Daddy Class 2 CA
===================
-----BEGIN CERTIFICATE-----
MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMY
VGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRp
ZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkG
A1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g
RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQAD
ggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv
2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32
qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6j
YGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmY
vLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0O
BBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2o
atTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMu
MTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwG
A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wim
PQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKt
I3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ
HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VI
Ls9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/b
vZ8=
-----END CERTIFICATE-----
Starfield Class 2 CA
====================
-----BEGIN CERTIFICATE-----
MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzElMCMGA1UEChMc
U3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZpZWxkIENsYXNzIDIg
Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQwNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBo
MQswCQYDVQQGEwJVUzElMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAG
A1UECxMpU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqG
SIb3DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf8MOh2tTY
bitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN+lq2cwQlZut3f+dZxkqZ
JRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVm
epsZGD3/cVE8MC5fvj13c7JdBmzDI1aaK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSN
F4Azbl5KXZnJHoe0nRrA1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HF
MIHCMB0GA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fRzt0f
hvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNo
bm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24g
QXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGs
afPzWdqbAYcaT1epoXkJKtv3L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLM
PUxA2IGvd56Deruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl
xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynpVSJYACPq4xJD
KVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEYWQPJIrSPnNVeKtelttQKbfi3
QBFGmh95DmK/D5fs4C8fF5Q=
-----END CERTIFICATE-----
DigiCert Assured ID Root CA
===========================
-----BEGIN CERTIFICATE-----

View file

@ -3,7 +3,7 @@
"description": "The Kirby core",
"license": "proprietary",
"type": "kirby-cms",
"version": "4.7.0",
"version": "4.8.0",
"keywords": [
"kirby",
"cms",
@ -41,12 +41,12 @@
"composer/semver": "3.4.3",
"filp/whoops": "2.18.0",
"getkirby/composer-installer": "^1.2.1",
"laminas/laminas-escaper": "2.16.0",
"laminas/laminas-escaper": "2.17.0",
"michelf/php-smartypants": "1.8.1",
"phpmailer/phpmailer": "6.9.3",
"symfony/polyfill-intl-idn": "1.31.0",
"symfony/polyfill-mbstring": "1.31.0",
"symfony/yaml": "6.4.18"
"phpmailer/phpmailer": "6.10.0",
"symfony/polyfill-intl-idn": "1.32.0",
"symfony/polyfill-mbstring": "1.32.0",
"symfony/yaml": "6.4.21"
},
"replace": {
"symfony/polyfill-php72": "*"

View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "fa52461c55dc4e276cd5853b57c95c60",
"content-hash": "e70567909c74a3864f445abea10a85cc",
"packages": [
{
"name": "christian-riesen/base32",
@ -319,16 +319,16 @@
},
{
"name": "laminas/laminas-escaper",
"version": "2.16.0",
"version": "2.17.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-escaper.git",
"reference": "9cf1f5317ca65b4fd5c6a3c2855e24a187b288c8"
"reference": "df1ef9503299a8e3920079a16263b578eaf7c3ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/9cf1f5317ca65b4fd5c6a3c2855e24a187b288c8",
"reference": "9cf1f5317ca65b4fd5c6a3c2855e24a187b288c8",
"url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/df1ef9503299a8e3920079a16263b578eaf7c3ba",
"reference": "df1ef9503299a8e3920079a16263b578eaf7c3ba",
"shasum": ""
},
"require": {
@ -376,7 +376,7 @@
"type": "community_bridge"
}
],
"time": "2025-02-17T12:40:19+00:00"
"time": "2025-05-06T19:29:36+00:00"
},
{
"name": "league/color-extractor",
@ -495,16 +495,16 @@
},
{
"name": "phpmailer/phpmailer",
"version": "v6.9.3",
"version": "v6.10.0",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e"
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e",
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
"shasum": ""
},
"require": {
@ -564,7 +564,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.9.3"
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0"
},
"funding": [
{
@ -572,7 +572,7 @@
"type": "github"
}
],
"time": "2024-11-24T18:04:13+00:00"
"time": "2025-04-24T15:19:31+00:00"
},
{
"name": "psr/log",
@ -626,16 +626,16 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.5.1",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
"shasum": ""
},
"require": {
@ -648,7 +648,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.5-dev"
"dev-main": "3.6-dev"
}
},
"autoload": {
@ -673,7 +673,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
},
"funding": [
{
@ -689,11 +689,11 @@
"type": "tidelift"
}
],
"time": "2024-09-25T14:20:29+00:00"
"time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.31.0",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@ -752,7 +752,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0"
},
"funding": [
{
@ -772,16 +772,16 @@
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.31.0",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "c36586dcf89a12315939e00ec9b4474adcb1d773"
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773",
"reference": "c36586dcf89a12315939e00ec9b4474adcb1d773",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3",
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3",
"shasum": ""
},
"require": {
@ -835,7 +835,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0"
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0"
},
"funding": [
{
@ -851,11 +851,11 @@
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
"time": "2024-09-10T14:38:51+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.31.0",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@ -916,7 +916,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0"
},
"funding": [
{
@ -936,19 +936,20 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.31.0",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
@ -996,7 +997,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
},
"funding": [
{
@ -1012,20 +1013,20 @@
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
"time": "2024-12-23T08:48:59+00:00"
},
{
"name": "symfony/yaml",
"version": "v6.4.18",
"version": "v6.4.21",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5"
"reference": "f01987f45676778b474468aa266fe2eda1f2bc7e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5",
"reference": "bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5",
"url": "https://api.github.com/repos/symfony/yaml/zipball/f01987f45676778b474468aa266fe2eda1f2bc7e",
"reference": "f01987f45676778b474468aa266fe2eda1f2bc7e",
"shasum": ""
},
"require": {
@ -1068,7 +1069,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v6.4.18"
"source": "https://github.com/symfony/yaml/tree/v6.4.21"
},
"funding": [
{
@ -1084,7 +1085,7 @@
"type": "tidelift"
}
],
"time": "2025-01-07T09:44:41+00:00"
"time": "2025-04-04T09:48:44+00:00"
}
],
"packages-dev": [],

View file

@ -24,16 +24,20 @@ return [
},
/**
* Allowed incremental steps between numbers (i.e `0.5`)
* Use `any` to allow any decimal value.
*/
'step' => function ($step = null) {
return $this->toNumber($step) ?? '';
'step' => function ($step = null): float|string {
return match ($step) {
'any' => 'any',
default => $this->toNumber($step) ?? ''
};
},
'value' => function ($value = null) {
return $this->toNumber($value) ?? '';
}
],
'methods' => [
'toNumber' => function ($value) {
'toNumber' => function ($value): float|null {
if ($this->isEmpty($value) === true) {
return null;
}

View file

@ -1,6 +1,7 @@
<?php
use Kirby\Field\FieldOptions;
use Kirby\Toolkit\I18n;
return [
'extends' => 'radio',
@ -19,8 +20,8 @@ return [
/**
* Custom placeholder string for empty option.
*/
'placeholder' => function (string $placeholder = '—') {
return $placeholder;
'placeholder' => function (string|array $placeholder = '—') {
return I18n::translate($placeholder, $placeholder);
},
],
'methods' => [

View file

@ -366,7 +366,7 @@
"field.layout.delete": "Smazat rozvržení",
"field.layout.delete.confirm": "Opravdu chcete smazat toto rozvržení?",
"field.layout.delete.confirm.all": "Opravdu chcete smazat všechna rozvržení?",
"field.layout.empty": "Zatím žádné řádky",
"field.layout.empty": "Zatím žádné rozvržení",
"field.layout.select": "Vyberte rozvržení",
"field.object.empty": "Zatím žádná informace",

View file

@ -98,17 +98,17 @@
"error.content.lock.replace": "Þessi útfáfa er læst og það verður ekki skipt út",
"error.content.lock.update": "Þessi útgáfa er læst og hún verður ekki uppfærð",
"error.entries.max.plural": "You must not add more than {max} entries",
"error.entries.max.singular": "You must not add more than one entry",
"error.entries.min.plural": "You must add at least {min} entries",
"error.entries.min.singular": "You must add at least one entry",
"error.entries.supports": "\"{type}\" field type is not supported for the entries field",
"error.entries.max.plural": "Ekki fleiri en {max} færslur",
"error.entries.max.singular": "Hámark ein færsla",
"error.entries.min.plural": "Að minnsta kosti {min} færslur",
"error.entries.min.singular": "Þú skalt setja inn a.m.k. eina færslu",
"error.entries.supports": "\"{type}\" sviðsgerðin er ekki studd fyrir færslu svið",
"error.entries.validation": "Það er villa í \"{field}\" sviðinu í röð {index}",
"error.email.preset.notFound": "Netfangstillingarnar: \"{name}\" fundust ekki",
"error.field.converter.invalid": "Ógildur umbreytari \"{converter}\"",
"error.field.link.options": "Invalid options: {options}",
"error.field.link.options": "Ógildar stillingar: {options}",
"error.field.type.missing": "Sviðið \"{ name }\": Sviðgerðin er \"{type}\" er alls ekki til.",
"error.file.changeName.empty": "Nafn skal fylla út",
@ -116,7 +116,7 @@
"error.file.changeTemplate.invalid": "Sniðmátinu fyrir skránna \"{id}\" er ekki hægt að breyta í \"{template}\" (gild: \"{blueprints}\")",
"error.file.changeTemplate.permission": "Þú mátt ekkert breyta sniðmátinu fyrir skránna \"{id}\"",
"error.file.delete.multiple": "Not all files could be deleted. Try each remaining file individually to see the specific error that prevents deletion.",
"error.file.delete.multiple": "Ekki var unnt að eyða öllum skrám. Reyndu að eyða hverri og einni til þess að sjá hvort þessi villa hindri eyðingu.",
"error.file.duplicate": "Skrá með nafninu \"{filename}\" er nú þegar til",
"error.file.extension.forbidden": "Skrárendingin \"{extension}\" er ekki leyfð",
"error.file.extension.invalid": "Óleyfilegt skrársnið hér: {extension}",
@ -179,7 +179,7 @@
"error.page.delete": "Síðunni \"{slug}\" er ekki hægt að eyða",
"error.page.delete.confirm": "Ritaðu titil síðunnar til að staðfesta",
"error.page.delete.hasChildren": "Síðan hefur undirsíður og er því ekki hægt að eyða",
"error.page.delete.multiple": "Not all pages could be deleted. Try each remaining page individually to see the specific error that prevents deletion.",
"error.page.delete.multiple": "Ekki var unnt að eyða öllum síðum. Reyndu að eyða hverri og einni til þess að sjá hvort þessi villa hindri eyðingu.",
"error.page.delete.permission": "Þú mátt ekkert eyða \"{slug}\"",
"error.page.draft.duplicate": "Uppkast með slóðinni \"{slug}\" er þegar til",
"error.page.duplicate": "Síða með slóðinni \"{slug}\" er þegar til",
@ -394,13 +394,13 @@
"file.sort": "Breyta röðun",
"files": "Skrár",
"files.delete.confirm.selected": "Do you really want to delete the selected files? This action cannot be undone.",
"files.delete.confirm.selected": "Viltu virkilega eyða völdum skrám? Hér verður ekki aftur snúið.",
"files.empty": "Engar skrár enn",
"filter": "Sigta",
"form.discard": "Hunsa breytingar",
"form.discard.confirm": "Ætlarðu virkilega að <strong>hunsa alla breytingar</strong>?",
"form.discard.confirm": "Ætlarðu virkilega að <strong>hunsa allar breytingar</strong>?",
"form.locked": "Efnið er þér ekki aðgengilegt þar sem annar notandi er nú þegar að vinna í því",
"form.unsaved": "Þessar breytingar hafa ekki verið vistaðar",
"form.preview": "Skoða breytingar",
@ -482,7 +482,7 @@
"license.status.missing.info": "Ekkert gilt skráningarleyfi",
"license.status.missing.label": "Vinsamlegast virkjaðu leyfið þitt",
"license.status.unknown.info": "Staða leyfis fyrir hugbúnaðinn er óþekkt",
"license.status.unknown.label": "Unknown",
"license.status.unknown.label": "Óþekkt",
"license.manage": "Sýslaðu með leyfin þín",
"license.purchased": "Verslað",
"license.success": "Þakka þér fyrir að velja Kirby",
@ -604,7 +604,7 @@
"page.status.unlisted.description": "Síðan er útgefin en þó ólistuð.",
"pages": "Síður",
"pages.delete.confirm.selected": "Do you really want to delete the selected pages? This action cannot be undone.",
"pages.delete.confirm.selected": "Viltu virkilega eyða völdum síðum? Hér verður ekki aftur snúið.",
"pages.empty": "Engar síður enn",
"pages.status.draft": "Uppköst",
"pages.status.listed": "Útgefnar og listaðar",
@ -679,9 +679,9 @@
"system.issues.git": ".git mappan virðist vera berskjölduð",
"system.issues.https": "Við mælum harðlega með því að þú notir HTTPS fyrir öll þín vefsvæði",
"system.issues.kirby": "Kirby mappan virðist vera berskjölduð",
"system.issues.local": "The site is running locally with relaxed security checks",
"system.issues.local": "Vefsvæðið keyrir staðbundið (e. local) með ákaflega lágum öryggiskröfum.",
"system.issues.site": "Mappa vefsvæðisins virðist vera berskjölduð",
"system.issues.vue.compiler": "The Vue template compiler is enabled",
"system.issues.vue.compiler": "Vue sniðmátsþýðandinn er virkur",
"system.issues.vulnerability.kirby": "Uppsetningin þín gæti verið berskjölduð gagnvart eftirfarandi veikleika: ({ severity } veikleikinn): { description }",
"system.issues.vulnerability.plugin": "Uppsetningin þín gæti verið berskjölduð gagnvart eftirfarandi veikleika í viðbótinni { plugin }: ({ severity } veikleikinn): { description }",
"system.updateStatus": "Uppfærslustaða",
@ -761,7 +761,7 @@
"user.changeLanguage": "Breyta tungumáli",
"user.changeName": "Endurnefna þennan notanda",
"user.changePassword": "Breyta lykilorð",
"user.changePassword.current": "Your current password",
"user.changePassword.current": "Þitt núverandi lykilorð",
"user.changePassword.new": "Nýtt lykilorð",
"user.changePassword.new.confirm": "Staðfestu nýtt lykilorð…",
"user.changeRole": "Breyta hlutverki",

View file

@ -20,9 +20,9 @@
"coordinates": "Coordinates",
"copy": "Copia",
"copy.all": "Copia tutto",
"copy.success": "{count} copied!",
"copy.success": "Copiato",
"copy.success.multiple": "{count} copied!",
"copy.url": "Copy URL",
"copy.url": "Copia URL",
"create": "Crea",
"custom": "Custom",
@ -92,23 +92,23 @@
"error.cache.type.invalid": "Tipo di cache \"{type}\" non valido",
"error.content.lock.delete": "The version is locked and cannot be deleted",
"error.content.lock.move": "The source version is locked and cannot be moved",
"error.content.lock.publish": "This version is already published",
"error.content.lock.replace": "The version is locked and cannot be replaced",
"error.content.lock.update": "The version is locked and cannot be updated",
"error.content.lock.delete": "La versione è bloccata e non può essere eliminata",
"error.content.lock.move": "La versione di origine è bloccata e non può essere spostata",
"error.content.lock.publish": "Questa versione è già pubblica",
"error.content.lock.replace": "La versione è bloccata e non può essere rimpiazzata",
"error.content.lock.update": "La versione è bloccata e non può essere aggiornata",
"error.entries.max.plural": "You must not add more than {max} entries",
"error.entries.max.singular": "You must not add more than one entry",
"error.entries.min.plural": "You must add at least {min} entries",
"error.entries.min.singular": "You must add at least one entry",
"error.entries.supports": "\"{type}\" field type is not supported for the entries field",
"error.entries.max.plural": "Non puoi aggiungere più di {max} voci",
"error.entries.max.singular": "Non puoi aggiungere più di una voce ",
"error.entries.min.plural": "Devi aggiungere almeno {min} voci",
"error.entries.min.singular": "Devi aggiungere almeno una voce",
"error.entries.supports": "Il tipo di campo \"{type}\" non è supportato per il campo \"entries\"",
"error.entries.validation": "C'è un errore nel campo \"{field}\" nella riga {index}",
"error.email.preset.notFound": "Non è stato possibile trovare il preset email \"{name}\"",
"error.field.converter.invalid": "Convertitore \"{converter}\" non valido",
"error.field.link.options": "Invalid options: {options}",
"error.field.link.options": "Opzioni non valide: {options}",
"error.field.type.missing": "Campo \"{ name }\": il tipo di campo \"{ type }\" non esiste",
"error.file.changeName.empty": "Il nome non dev'essere vuoto",
@ -116,7 +116,7 @@
"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.delete.multiple": "Not all files could be deleted. Try each remaining file individually to see the specific error that prevents deletion.",
"error.file.delete.multiple": "Non tutti i file possono essere eliminati. Prova a rimuovere i file uno per uno per vedere lerrore specifico che ne impedisce leliminazione.",
"error.file.duplicate": "Un file con il nome \"{filename}\" esiste già",
"error.file.extension.forbidden": "L'estensione \"{extension}\" non è consentita",
"error.file.extension.invalid": "Estensione non valida: {extension}",
@ -135,7 +135,7 @@
"error.file.name.missing": "Il nome del file non può essere vuoto",
"error.file.notFound": "Il file non \u00e8 stato trovato",
"error.file.orientation": "L'imaggine dev'essere orientata in \"{orientation}\"",
"error.file.sort.permission": "You are not allowed to change the sorting of \"{filename}\"",
"error.file.sort.permission": "Non ti è permesso riordinare \"{filename}\"",
"error.file.type.forbidden": "Non ti è permesso caricare file {type}",
"error.file.type.invalid": "Tipo di file non valido: {type}",
"error.file.undefined": "Il file non \u00e8 stato trovato",
@ -179,7 +179,7 @@
"error.page.delete": "La pagina \"{slug}\" non può essere eliminata",
"error.page.delete.confirm": "Inserisci il titolo della pagina per confermare",
"error.page.delete.hasChildren": "La pagina ha sottopagine e non può essere eliminata",
"error.page.delete.multiple": "Not all pages could be deleted. Try each remaining page individually to see the specific error that prevents deletion.",
"error.page.delete.multiple": "Non è stato possibile eliminare tutte le pagine. Prova ad eliminare le pagine restanti una ad una per vedere l'errore specifico che ne impedisce l'eliminazione. ",
"error.page.delete.permission": "Non ti è permesso eliminare \"{slug}\"",
"error.page.draft.duplicate": "Una bozza di pagina con l'URL \"{slug}\" esiste già",
"error.page.duplicate": "Una pagina con l'URL \"{slug}\" esiste già",
@ -187,7 +187,7 @@
"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.noSections": "The page \"{parent}\" cannot be a parent of any page because it lacks any pages sections in its blueprint",
"error.page.move.noSections": "La pagina \"{parent}\" non può contenere sottopagine perché il suo \"blueprint\" non definisce alcuna sezione di tipo \"pages\". ",
"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}\"",
@ -317,9 +317,9 @@
"field.blocks.heading.name": "Titolo",
"field.blocks.heading.text": "Testo",
"field.blocks.heading.placeholder": "Titolo …",
"field.blocks.figure.back.plain": "Plain",
"field.blocks.figure.back.pattern.light": "Pattern (light)",
"field.blocks.figure.back.pattern.dark": "Pattern (dark)",
"field.blocks.figure.back.plain": "Semplice",
"field.blocks.figure.back.pattern.light": "Pattern (chiaro)",
"field.blocks.figure.back.pattern.dark": "Pattern (scuro)",
"field.blocks.image.alt": "Testo alternativo",
"field.blocks.image.caption": "Didascalia",
"field.blocks.image.crop": "Ritaglio",
@ -357,10 +357,10 @@
"field.blocks.video.url.placeholder": "https://youtube.com/?v=",
"field.entries.delete.confirm.all": "Vuoi davvero cancellare tutte le voci?",
"field.entries.empty": "Non ci sono ancora elementi.",
"field.entries.empty": "Non contiene ancora voci",
"field.files.empty": "Nessun file selezionato",
"field.files.empty.single": "No file selected yet",
"field.files.empty.single": "Nessun file selezionato",
"field.layout.change": "Change layout",
"field.layout.delete": "Elimina layout",
@ -372,14 +372,14 @@
"field.object.empty": "Ancora nessuna informazione",
"field.pages.empty": "Nessuna pagina selezionata",
"field.pages.empty.single": "No page selected yet",
"field.pages.empty.single": "Nessuna pagina selezionata",
"field.structure.delete.confirm": "Vuoi veramente eliminare questo elemento?",
"field.structure.delete.confirm.all": "Vuoi davvero cancellare tutte le voci?",
"field.structure.empty": "Non ci sono ancora elementi.",
"field.structure.empty": "Non contiene ancora voci",
"field.users.empty": "Nessun utente selezionato",
"field.users.empty.single": "No user selected yet",
"field.users.empty.single": "Nessun utente selezionato",
"fields.empty": "No fields yet",
@ -394,17 +394,17 @@
"file.sort": "Cambia posizione",
"files": "Files",
"files.delete.confirm.selected": "Do you really want to delete the selected files? This action cannot be undone.",
"files.delete.confirm.selected": "Vuoi davvero eliminare i file selezionati? Questa azione è irreversibile.",
"files.empty": "Nessun file caricato",
"filter": "Filter",
"form.discard": "Discard changes",
"form.discard.confirm": "Do you really want to <strong>discard all your changes</strong>?",
"form.locked": "This content is disabled for you as it is currently edited by another user",
"form.unsaved": "The current changes have not yet been saved",
"form.preview": "Preview changes",
"form.preview.draft": "Preview draft",
"form.discard": "Annulla modifiche",
"form.discard.confirm": "Vuoi davvero <strong>scartare tutte le tue modifiche</strong>?",
"form.locked": "Questo contenuto è disabilitato perché è attualmente modificato da un altro utente",
"form.unsaved": "Le modifiche non sono ancora state salvate",
"form.preview": "Anteprima modifiche",
"form.preview.draft": "Anteprima della bozza",
"hide": "Nascondi",
"hour": "Ora",
@ -481,8 +481,8 @@
"license.status.missing.bubble": "Pronto a lanciare il tuo sito?",
"license.status.missing.info": "Nessuna licenza valida",
"license.status.missing.label": "Attiva la tua licenza ora",
"license.status.unknown.info": "The license status is unknown",
"license.status.unknown.label": "Unknown",
"license.status.unknown.info": "Lo stato della licenza è sconosciuto",
"license.status.unknown.label": "Sconosciuto",
"license.manage": "Gestisci le tue licenze",
"license.purchased": "Acquistata",
"license.success": "Ti ringraziamo per aver supportato Kirby",
@ -495,9 +495,9 @@
"lock.unsaved": "Modifiche non salvate",
"lock.unsaved.empty": "Non ci sono altre modifiche non salvate",
"lock.unsaved.files": "Unsaved files",
"lock.unsaved.pages": "Unsaved pages",
"lock.unsaved.users": "Unsaved accounts",
"lock.unsaved.files": "File non salvati",
"lock.unsaved.pages": "Pagine non salvate",
"lock.unsaved.users": "Account non salvati",
"lock.isLocked": "Unsaved changes by {email}",
"lock.unlock": "Sblocca",
"lock.unlock.submit": "Unlock and overwrite unsaved changes by <strong>{email}</strong>",
@ -604,7 +604,7 @@
"page.status.unlisted.description": "La pagina è accessibile soltanto tramite URL",
"pages": "Pagine",
"pages.delete.confirm.selected": "Do you really want to delete the selected pages? This action cannot be undone.",
"pages.delete.confirm.selected": "Vuoi davvero eliminare le pagine selezionate? Questa azione è irreversibile.",
"pages.empty": "Nessuna pagina",
"pages.status.draft": "Bozza",
"pages.status.listed": "Pubblicato",
@ -622,7 +622,7 @@
"prev": "Precedente",
"preview": "Anteprima",
"publish": "Publish",
"publish": "Pubblica",
"published": "Pubblicato",
"remove": "Rimuovi",
@ -644,9 +644,9 @@
"role.nobody.title": "Nessuno",
"save": "Salva",
"saved": "Saved",
"saved": "Salvato",
"search": "Cerca",
"searching": "Searching",
"searching": "Ricerca in corso",
"search.min": "Inserisci almeno {min} caratteri per la ricerca",
"search.all": "Mostra tutti i {count} risultati",
"search.results.none": "Nessun risultato",
@ -679,9 +679,9 @@
"system.issues.git": "La cartella .git sembra essere esposta",
"system.issues.https": "Raccomandiamo l'utilizzo di HTTPS per tutti i siti",
"system.issues.kirby": "La cartella kirby sembra essere esposta",
"system.issues.local": "The site is running locally with relaxed security checks",
"system.issues.local": "Il sito è in esecuzione in locale con controlli di sicurezza meno severi",
"system.issues.site": "La cartella site sembra essere esposta",
"system.issues.vue.compiler": "The Vue template compiler is enabled",
"system.issues.vue.compiler": "Il compilatore di template di Vue è abilitato",
"system.issues.vulnerability.kirby": "La tua installazione potrebbe essere colpita dalla seguente vulnerabilità ({ severity } gravità): { description }",
"system.issues.vulnerability.plugin": "La tua installazione potrebbe essere colpita dalla seguente vulnerabilità nel plugin { plugin } ({ severity } gravità): { description }",
"system.updateStatus": "Aggiorna lo stato",
@ -698,10 +698,10 @@
"tel.placeholder": "+49123456789",
"template": "Template",
"theme": "Theme",
"theme.light": "Lights on",
"theme.dark": "Lights off",
"theme.automatic": "Match system default",
"theme": "Tema",
"theme.light": "Luci accese",
"theme.dark": "Luci spente",
"theme.automatic": "Usa impostazione predefinita del sistema",
"title": "Titolo",
"today": "Oggi",
@ -753,7 +753,7 @@
"upload.progress": "Caricamento...",
"url": "URL",
"url.placeholder": "https://esempio.com",
"url.placeholder": "https://example.com",
"user": "Utente",
"user.blueprint": "Puoi definire ulteriori sezioni e campi del form aggiuntivi per questo ruolo in <strong>/site/blueprints/users/{blueprint}.yml</strong>",
@ -761,7 +761,7 @@
"user.changeLanguage": "Cambia lingua",
"user.changeName": "Rinomina questo utente",
"user.changePassword": "Cambia password",
"user.changePassword.current": "Your current password",
"user.changePassword.current": "La password attuale",
"user.changePassword.new": "Nuova password",
"user.changePassword.new.confirm": "Conferma la nuova password...",
"user.changeRole": "Cambia ruolo",
@ -773,13 +773,13 @@
"users": "Utenti",
"version": "Versione di Kirby",
"version.changes": "Changed version",
"version.compare": "Compare versions",
"version.changes": "Versione modificata",
"version.compare": "Confronta le versioni",
"version.current": "Versione corrente",
"version.latest": "Ultima versione",
"versionInformation": "Informazioni sulla versione",
"view": "View",
"view": "Visualizza",
"view.account": "Il tuo account",
"view.installation": "Installazione",
"view.languages": "Lingue",

View file

@ -20,9 +20,9 @@
"coordinates": "Coördinaten ",
"copy": "Kopiëren",
"copy.all": "Kopieer alles",
"copy.success": "{count} gekopieerd!",
"copy.success": "Gekopieerd",
"copy.success.multiple": "{count} gekopieerd!",
"copy.url": "Copy URL",
"copy.url": "Kopieer URL",
"create": "Aanmaken",
"custom": "Custom",
@ -92,23 +92,23 @@
"error.cache.type.invalid": "Ongeldig cache type \"{type}\"",
"error.content.lock.delete": "The version is locked and cannot be deleted",
"error.content.lock.move": "The source version is locked and cannot be moved",
"error.content.lock.publish": "This version is already published",
"error.content.lock.replace": "The version is locked and cannot be replaced",
"error.content.lock.update": "The version is locked and cannot be updated",
"error.content.lock.delete": "Deze versie is vergrendeld en kan niet worden verwijderd",
"error.content.lock.move": "De bronversie is vergrendeld en kan niet worden verplaatst",
"error.content.lock.publish": "Deze versie is al gepubliceerd",
"error.content.lock.replace": "Deze versie is vergrendeld en kan niet worden vervangen",
"error.content.lock.update": "Deze versie is vergrendeld en kan niet worden geüpdatet",
"error.entries.max.plural": "You must not add more than {max} entries",
"error.entries.max.singular": "You must not add more than one entry",
"error.entries.min.plural": "You must add at least {min} entries",
"error.entries.min.singular": "You must add at least one entry",
"error.entries.supports": "\"{type}\" field type is not supported for the entries field",
"error.entries.max.plural": "Je kunt niet meer dan {max} items toevoegen",
"error.entries.max.singular": "Je kunt niet meer dan één item toevoegen",
"error.entries.min.plural": "Je moet ten minste {min} items toevoegen",
"error.entries.min.singular": "Je moet ten minste één item toevoegen",
"error.entries.supports": "\"{type}\" veld type is niet ondersteund in het items veld",
"error.entries.validation": "Er is een fout opgetreden in veld \"{field}\" in rij {index}",
"error.email.preset.notFound": "De e-mailvoorinstelling \"{name}\" kan niet worden gevonden",
"error.field.converter.invalid": "Ongeldige converter \"{converter}\"",
"error.field.link.options": "Invalid options: {options}",
"error.field.link.options": "Ongeldige opties: {options}",
"error.field.type.missing": "Veld \"{ name }\": Het veldtype \"{ type }\" bestaat niet",
"error.file.changeName.empty": "De naam mag niet leeg zijn",
@ -116,7 +116,7 @@
"error.file.changeTemplate.invalid": "Het template voor het bestand \"{id}\" kan niet worden gewijzigd in \"{template}\" (geldig: \"{blueprints}\")",
"error.file.changeTemplate.permission": "Je hebt geen rechten om het template te wijzigen voor bestand \"{id}\"",
"error.file.delete.multiple": "Not all files could be deleted. Try each remaining file individually to see the specific error that prevents deletion.",
"error.file.delete.multiple": "Niet alle bestanden kunnen worden verwijderd. Probeer de overgebleven bestanden individueel te verwijderen om het probleem te achterhalen.",
"error.file.duplicate": "Er bestaat al een bestand met de naam \"{filename}\"",
"error.file.extension.forbidden": "Bestandsextensie \"{extension}\" is niet toegestaan",
"error.file.extension.invalid": "Ongeldige extensie: {extension}",
@ -135,7 +135,7 @@
"error.file.name.missing": "De bestandsnaam mag niet leeg zijn",
"error.file.notFound": "Het bestand kan niet worden gevonden",
"error.file.orientation": "De oriëntatie van de afbeelding moet \"{orientation}\" zijn",
"error.file.sort.permission": "You are not allowed to change the sorting of \"{filename}\"",
"error.file.sort.permission": "Je hebt geen rechten om de sortering te wijzigen van \"{filename}\"",
"error.file.type.forbidden": "Je hebt geen rechten om {type} bestanden up te loaden",
"error.file.type.invalid": "Ongeldig bestands type: {type}",
"error.file.undefined": "Het bestand kan niet worden gevonden",
@ -179,7 +179,7 @@
"error.page.delete": "De pagina \"{slug}\" kan niet worden verwijderd",
"error.page.delete.confirm": "Voer de paginatitel in om te bevestigen",
"error.page.delete.hasChildren": "Deze pagina heeft subpagina's en kan niet worden verwijderd",
"error.page.delete.multiple": "Not all pages could be deleted. Try each remaining page individually to see the specific error that prevents deletion.",
"error.page.delete.multiple": "Niet alle pagina's kunnen worden verwijderd. Probeer de overgebleven pagina's individueel te verwijderen om het probleem te achterhalen.",
"error.page.delete.permission": "Je hebt geen rechten om \"{slug}\" te verwijderen",
"error.page.draft.duplicate": "Er bestaat al een conceptpagina met de URL-appendix \"{slug}\"",
"error.page.duplicate": "Er bestaat al een pagina met de URL-appendix \"{slug}\"",
@ -187,7 +187,7 @@
"error.page.move.ancestor": "De pagina kan niet in zichzelf worden verplaatst",
"error.page.move.directory": "De page map kan niet worden verplaatst",
"error.page.move.duplicate": "Er bestaat al een subpagina met de URL-appendix \"{slug}\"",
"error.page.move.noSections": "The page \"{parent}\" cannot be a parent of any page because it lacks any pages sections in its blueprint",
"error.page.move.noSections": "De pagina \"{parent}\" kan geen bovenliggende pagina zijn omdat het een pages sectie mist in de blueprint.",
"error.page.move.notFound": "De verplaatste pagina kan niet gevonden worden",
"error.page.move.permission": "Je hebt geen rechten om \"{slug}\" te verplaatsen",
"error.page.move.template": "De \"{template}\" template is niet toegestaan als een subpagina van \"{parent}\"",
@ -317,9 +317,9 @@
"field.blocks.heading.name": "Koptekst",
"field.blocks.heading.text": "Tekst",
"field.blocks.heading.placeholder": "Koptekst ...",
"field.blocks.figure.back.plain": "Plain",
"field.blocks.figure.back.pattern.light": "Pattern (light)",
"field.blocks.figure.back.pattern.dark": "Pattern (dark)",
"field.blocks.figure.back.plain": "Neutraal",
"field.blocks.figure.back.pattern.light": "Patroon (licht)",
"field.blocks.figure.back.pattern.dark": "Patroon (donker)",
"field.blocks.image.alt": "Alternatieve tekst",
"field.blocks.image.caption": "Beschrijving",
"field.blocks.image.crop": "Uitsnede",
@ -360,7 +360,7 @@
"field.entries.empty": "Nog geen items",
"field.files.empty": "Nog geen bestanden geselecteerd",
"field.files.empty.single": "No file selected yet",
"field.files.empty.single": "Nog geen bestand geselecteerd",
"field.layout.change": "Verander layout",
"field.layout.delete": "Verwijder indeling",
@ -372,14 +372,14 @@
"field.object.empty": "Nog geen informatie",
"field.pages.empty": "Nog geen pagina's geselecteerd",
"field.pages.empty.single": "No page selected yet",
"field.pages.empty.single": "Nog geen pagina geselecteerd",
"field.structure.delete.confirm": "Wil je deze rij verwijderen?",
"field.structure.delete.confirm.all": "Weet je zeker dat je alle items wil verwijderen?",
"field.structure.empty": "Nog geen items",
"field.users.empty": "Nog geen gebruikers geselecteerd",
"field.users.empty.single": "No user selected yet",
"field.users.empty.single": "Nog geen gebruiker geselecteerd",
"fields.empty": "Nog geen velden",
@ -394,17 +394,17 @@
"file.sort": "Verander positie",
"files": "Bestanden",
"files.delete.confirm.selected": "Do you really want to delete the selected files? This action cannot be undone.",
"files.delete.confirm.selected": "Weet je zeker dat je de geselecteerde bestanden wil verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
"files.empty": "Nog geen bestanden",
"filter": "Filter",
"form.discard": "Discard changes",
"form.discard.confirm": "Do you really want to <strong>discard all your changes</strong>?",
"form.locked": "This content is disabled for you as it is currently edited by another user",
"form.unsaved": "The current changes have not yet been saved",
"form.preview": "Preview changes",
"form.preview.draft": "Preview draft",
"form.discard": "Wijzigingen annuleren",
"form.discard.confirm": "Weet je zeker dat je <strong>alle wijzigingen ongedaan wil maken</strong>?",
"form.locked": "Deze inhoud is uitgeschakeld voor jou omdat deze momenteel door een andere gebruiker wordt bewerkt",
"form.unsaved": "De huidige wijzigingen zijn nog niet opgeslagen",
"form.preview": "Bekijk wijzigingen",
"form.preview.draft": "Bekijk concept",
"hide": "Verberg",
"hour": "Uur",
@ -481,8 +481,8 @@
"license.status.missing.bubble": "Klaar om je website te lanceren?",
"license.status.missing.info": "Geen geldige licentie",
"license.status.missing.label": "Activeer je licentie",
"license.status.unknown.info": "The license status is unknown",
"license.status.unknown.label": "Unknown",
"license.status.unknown.info": "De licentiestatus is onbekend",
"license.status.unknown.label": "Onbekend",
"license.manage": "Beheer je licenties",
"license.purchased": "Gekocht",
"license.success": "Bedankt dat je Kirby ondersteunt",
@ -495,9 +495,9 @@
"lock.unsaved": "Niet opgeslagen wijzigingen",
"lock.unsaved.empty": "Er zijn geen niet opgeslagen wijzigingen meer",
"lock.unsaved.files": "Unsaved files",
"lock.unsaved.pages": "Unsaved pages",
"lock.unsaved.users": "Unsaved accounts",
"lock.unsaved.files": "Niet opgeslagen bestanden",
"lock.unsaved.pages": "Niet opgeslagen pagina's",
"lock.unsaved.users": "Niet opgeslagen accounts",
"lock.isLocked": "Niet opgeslagen wijzigingen door {email}",
"lock.unlock": "Ontgrendelen",
"lock.unlock.submit": "Niet-opgeslagen wijzigingen ontgrendelen en overschrijven met <strong>{email}</strong>",
@ -589,7 +589,7 @@
"page.create": "Maak aan als {status}",
"page.delete.confirm": "Weet je zeker dat je pagina <strong>{title}</strong> wilt verwijderen?",
"page.delete.confirm.subpages": "<strong>Deze pagina heeft subpagina's</strong>. <br>Alle subpagina's zullen ook worden verwijderd.",
"page.delete.confirm.title": "Voeg een paginatitel in om te bevestigen",
"page.delete.confirm.title": "Voer de paginatitel in om te bevestigen",
"page.duplicate.appendix": "Kopiëren",
"page.duplicate.files": "Kopieer bestanden",
"page.duplicate.pages": "Kopieer pagina's",
@ -604,7 +604,7 @@
"page.status.unlisted.description": "Deze pagina is alleen bereikbaar via URL",
"pages": "Paginas",
"pages.delete.confirm.selected": "Do you really want to delete the selected pages? This action cannot be undone.",
"pages.delete.confirm.selected": "Weet je zeker dat je de geselecteerde pagina's wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
"pages.empty": "Nog geen pagina's",
"pages.status.draft": "Concepten",
"pages.status.listed": "Gepubliceerd",
@ -622,7 +622,7 @@
"prev": "Vorige",
"preview": "Voorbeeld",
"publish": "Publish",
"publish": "Publiceren",
"published": "Gepubliceerd",
"remove": "Verwijder",
@ -644,9 +644,9 @@
"role.nobody.title": "Niemand",
"save": "Opslaan",
"saved": "Saved",
"saved": "Opgeslagen",
"search": "Zoeken",
"searching": "Searching",
"searching": "Zoeken",
"search.min": "Voer {min} tekens in om te zoeken",
"search.all": "Laat alle {count} resultaten zien",
"search.results.none": "Geen resultaten",
@ -679,9 +679,9 @@
"system.issues.git": "De .git map lijkt zichtbaar te zijn",
"system.issues.https": "We raden HTTPS aan voor al je sites",
"system.issues.kirby": "De kirby map lijkt zichtbaar te zijn",
"system.issues.local": "The site is running locally with relaxed security checks",
"system.issues.local": "De site draait lokaal met versoepelde beveiligingscontroles",
"system.issues.site": "De site map lijkt zichtbaar te zijn",
"system.issues.vue.compiler": "The Vue template compiler is enabled",
"system.issues.vue.compiler": "De Vue template compiler is ingeschakeld",
"system.issues.vulnerability.kirby": "De installatie is mogelijk getroffen door de volgende kwetsbaarheid ({ severity } ernst): { description }",
"system.issues.vulnerability.plugin": "De installatie is mogelijk getroffen door de volgende kwetsbaarheid in plugin { plugin } ({ severity } ernst): { description }",
"system.updateStatus": "Update status",
@ -698,10 +698,10 @@
"tel.placeholder": "+49123456789",
"template": "Template",
"theme": "Theme",
"theme.light": "Lights on",
"theme.dark": "Lights off",
"theme.automatic": "Match system default",
"theme": "Thema",
"theme.light": "Lichten aan",
"theme.dark": "Lichten uit",
"theme.automatic": "Systeemstandaard gebruiken",
"title": "Titel",
"today": "Vandaag",
@ -761,7 +761,7 @@
"user.changeLanguage": "Taal veranderen",
"user.changeName": "Gebruiker hernoemen",
"user.changePassword": "Wachtwoord wijzigen",
"user.changePassword.current": "Your current password",
"user.changePassword.current": "Je huidige wachtwoord",
"user.changePassword.new": "Nieuw wachtwoord",
"user.changePassword.new.confirm": "Bevestig het nieuwe wachtwoord...",
"user.changeRole": "Verander rol",
@ -773,13 +773,13 @@
"users": "Gebruikers",
"version": "Kirby-versie",
"version.changes": "Changed version",
"version.compare": "Compare versions",
"version.changes": "Gewijzigde versie",
"version.compare": "Vergelijk versies",
"version.current": "Huidige versie",
"version.latest": "Laatste versie",
"versionInformation": "Versie informatie",
"view": "View",
"view": "Bekijk",
"view.account": "Jouw account",
"view.installation": "Installatie",
"view.languages": "Talen",

View file

@ -20,7 +20,7 @@
"coordinates": "Coordenadas",
"copy": "Copiar",
"copy.all": "Copiar todos",
"copy.success": "{count} copiados!",
"copy.success": "Copiado",
"copy.success.multiple": "{count} copiados!",
"copy.url": "Copiar URL",
"create": "Criar",
@ -38,7 +38,7 @@
"days.tue": "Ter",
"days.wed": "Qua",
"debugging": "Depuração ",
"debugging": "Debugging ",
"delete": "Eliminar",
"delete.all": "Eliminar todos",

View file

@ -5,11 +5,17 @@
$uri = parse_url('https://getkirby.com/' . ltrim($_SERVER['REQUEST_URI'], '/'), PHP_URL_PATH) ?? '/';
$uri = urldecode($uri);
// Emulate Apache's `mod_rewrite` functionality
if ($uri !== '/' && file_exists($_SERVER['DOCUMENT_ROOT'] . '/' . ltrim($uri, '/'))) {
// emulate Apache's `mod_rewrite` functionality, but prevent
// disclosure of the existence of files outside the document root
$path = $_SERVER['DOCUMENT_ROOT'] . '/' . ltrim($uri, '/');
if (
$uri !== '/' &&
file_exists($path) === true &&
substr(realpath($path), 0, strlen($_SERVER['DOCUMENT_ROOT'])) === $_SERVER['DOCUMENT_ROOT']
) {
return false;
}
$_SERVER['SCRIPT_NAME'] = '/index.php';
require $_SERVER['DOCUMENT_ROOT'] . '/' . $_SERVER['SCRIPT_NAME'];
require $_SERVER['DOCUMENT_ROOT'] . $_SERVER['SCRIPT_NAME'];

View file

@ -3,6 +3,7 @@
namespace Kirby\Cms;
use Closure;
use Exception as GlobalException;
use Generator;
use Kirby\Data\Data;
use Kirby\Email\Email as BaseEmail;
@ -318,9 +319,18 @@ class App
}
}
foreach (glob($this->root('blueprints') . '/' . $type . '/*.yml') as $blueprint) {
$name = F::name($blueprint);
$blueprints[$name] = $name;
try {
// protect against path traversal attacks
$root = $this->root('blueprints') . '/' . $type;
$realpath = Dir::realpath($root, $this->root('blueprints'));
foreach (glob($realpath . '/*.yml') as $blueprint) {
$name = F::name($blueprint);
$blueprints[$name] = $name;
}
} catch (GlobalException) {
// if the realpath operation failed, the following glob was skipped,
// keeping just the blueprints from extensions
}
ksort($blueprints);
@ -478,7 +488,7 @@ class App
}
// controller from site root
$controller = Controller::load($this->root('controllers') . '/' . $name . '.php');
$controller = Controller::load($this->root('controllers') . '/' . $name . '.php', $this->root('controllers'));
// controller from extension
$controller ??= $this->extension('controllers', $name);
@ -1184,7 +1194,7 @@ class App
string|null $path = null,
string|null $method = null
): Response|null {
if (($_ENV['KIRBY_RENDER'] ?? true) === false) {
if ((filter_var($_ENV['KIRBY_RENDER'] ?? true, FILTER_VALIDATE_BOOLEAN)) === false) {
return null;
}
@ -1292,11 +1302,36 @@ class App
// try to resolve image urls for pages and drafts
if ($page = $site->findPageOrDraft($id)) {
return $page->file($filename);
return $this->resolveFile($page->file($filename));
}
// try to resolve site files at least
return $site->file($filename);
return $this->resolveFile($site->file($filename));
}
/**
* Filters a resolved file object using the configuration
* @internal
*/
public function resolveFile(File|null $file): File|null
{
// shortcut for files that don't exist
if ($file === null) {
return null;
}
$option = $this->option('content.fileRedirects', true);
if ($option === true) {
return $file;
}
if ($option instanceof Closure) {
return $option($file) === true ? $file : null;
}
// option was set to `false` or an invalid value
return null;
}
/**

View file

@ -79,7 +79,7 @@ trait AppCaches
$prefix =
str_replace(['/', ':'], '_', $this->system()->indexUrl()) .
'/' .
str_replace('.', '/', $key);
str_replace(['/', '.'], ['_', '/'], $key);
$defaults = [
'active' => true,

View file

@ -105,10 +105,11 @@ class Collections
{
$kirby = App::instance();
// first check for collection file
$file = $kirby->root('collections') . '/' . $name . '.php';
// first check for collection file in the `collections` root
$root = $kirby->root('collections');
$file = $root . '/' . $name . '.php';
if (is_file($file) === true) {
if (F::exists($file, $root) === true) {
$collection = F::load($file, allowOutput: false);
if ($collection instanceof Closure) {

View file

@ -617,12 +617,20 @@ class File extends ModelWithContent
}
/**
* Simplified File URL that uses the parent
* Page URL and the filename as a more stable
* alternative for the media URLs.
* Clean file URL that uses the parent page URL
* and the filename as a more stable alternative
* for the media URLs if available. The `content.fileRedirects`
* option is used to disable this behavior or enable it
* on a per-file basis.
*/
public function previewUrl(): string|null
{
// check if the clean file URL is accessible,
// otherwise we need to fall back to the media URL
if ($this->kirby()->resolveFile($this) === null) {
return $this->url();
}
$parent = $this->parent();
$url = Url::to($this->id());
@ -651,6 +659,7 @@ class File extends ModelWithContent
return $url;
case 'user':
// there are no clean URL routes for user files
return $this->url();
default:
return $url;

View file

@ -57,7 +57,7 @@ class Language
}
static::$kirby = $props['kirby'] ?? null;
$this->code = trim($props['code']);
$this->code = basename(trim($props['code'])); // prevent path traversal
$this->default = ($props['default'] ?? false) === true;
$this->direction = ($props['direction'] ?? null) === 'rtl' ? 'rtl' : 'ltr';
$this->name = trim($props['name'] ?? $this->code);
@ -325,6 +325,7 @@ class Language
public static function loadRules(string $code): array
{
$kirby = App::instance();
$code = basename($code); // prevent path traversal
$code = Str::contains($code, '.') ? Str::before($code, '.') : $code;
$file = $kirby->root('i18n:rules') . '/' . $code . '.json';

View file

@ -95,11 +95,13 @@ class Media
string $filename
): Response|false {
$kirby = App::instance();
$index = $kirby->root('index');
$media = $kirby->root('media');
$root = match (true) {
// assets
is_string($model)
=> $kirby->root('media') . '/assets/' . $model . '/' . $hash,
=> $media . '/assets/' . $model . '/' . $hash,
// parent files for file model that already included hash
$model instanceof File
=> dirname($model->mediaRoot()),
@ -108,10 +110,13 @@ class Media
=> $model->mediaRoot() . '/' . $hash
};
$thumb = $root . '/' . $filename;
$job = $root . '/.jobs/' . $filename . '.json';
try {
// prevent path traversal
$root = Dir::realpath($root, $media);
$thumb = $root . '/' . $filename;
$job = $root . '/.jobs/' . $filename . '.json';
$options = Data::read($job);
} catch (Throwable) {
// send a customized error message to make clearer what happened here
@ -127,7 +132,12 @@ class Media
// this adds support for custom assets
$source = match (true) {
is_string($model) === true
=> $kirby->root('index') . '/' . $model . '/' . $options['filename'],
=> F::realpath(
$index . '/' . $model . '/' . $options['filename'],
$index
),
$model instanceof File
=> $model->root(),
default
=> $model->file($options['filename'])->root()
};

View file

@ -361,7 +361,7 @@ class Page extends ModelWithContent
}
/**
* Sorting number + Slug
* Returns the directory name (UID with optional sorting number)
*/
public function dirname(): string
{
@ -377,7 +377,8 @@ class Page extends ModelWithContent
}
/**
* Sorting number + Slug
* Returns the directory path relative to the `content` root
* (including optional sorting numbers and draft directories)
*/
public function diruri(): string
{

View file

@ -2,6 +2,7 @@
namespace Kirby\Cms;
use Kirby\Filesystem\F;
use Kirby\Http\Url as BaseUrl;
use Kirby\Toolkit\Str;
@ -63,10 +64,11 @@ class Url extends BaseUrl
$kirby = App::instance();
$page = $kirby->site()->page();
$path = $assetPath . '/' . $page->template() . '.' . $extension;
$file = $kirby->root('assets') . '/' . $path;
$root = $kirby->root('assets');
$file = $root . '/' . $path;
$url = $kirby->url('assets') . '/' . $path;
return file_exists($file) === true ? $url : null;
return F::exists($file, $root) === true ? $url : null;
}
/**

View file

@ -114,9 +114,14 @@ class Dir
/**
* Checks if the directory exists on disk
*/
public static function exists(string $dir): bool
public static function exists(string $dir, string|null $in = null): bool
{
return is_dir($dir) === true;
try {
static::realpath($dir, $in);
return true;
} catch (Exception) {
return false;
}
}
/**
@ -523,6 +528,33 @@ class Dir
return $result;
}
/**
* Returns the absolute path to the directory if the directory can be found.
* @since 4.7.1
*/
public static function realpath(string $dir, string|null $in = null): string
{
$realpath = realpath($dir);
if ($realpath === false || is_dir($realpath) === false) {
throw new Exception(sprintf('The directory does not exist at the given path: "%s"', $dir));
}
if ($in !== null) {
$parent = realpath($in);
if ($parent === false || is_dir($parent) === false) {
throw new Exception(sprintf('The parent directory does not exist: "%s"', $in));
}
if (substr($realpath, 0, strlen($parent)) !== $parent) {
throw new Exception('The directory is not within the parent directory');
}
}
return $realpath;
}
/**
* Removes a folder including all containing files and folders
*/

View file

@ -810,18 +810,24 @@ class Environment
}
// load the config for the host
if (empty($host) === false) {
if (
empty($host) === false &&
F::exists($path = $root . '/config.' . $host . '.php', $root) === true
) {
$configHost = F::load(
file: $root . '/config.' . $host . '.php',
file: $path,
fallback: [],
allowOutput: false
);
}
// load the config for the server IP
if (empty($addr) === false) {
if (
empty($addr) === false &&
F::exists($path = $root . '/config.' . $addr . '.php', $root) === true
) {
$configAddr = F::load(
file: $root . '/config.' . $addr . '.php',
file: $path,
fallback: [],
allowOutput: false
);

View file

@ -4,6 +4,7 @@ namespace Kirby\Panel\Lab;
use Kirby\Cms\App;
use Kirby\Filesystem\Dir;
use Kirby\Filesystem\F;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Str;
@ -32,7 +33,7 @@ class Category
) {
$this->root = $root ?? static::base() . '/' . $this->id;
if (file_exists($this->root . '/index.php') === true) {
if (F::exists($this->root . '/index.php', static::base()) === true) {
$this->props = array_merge(
require $this->root . '/index.php',
$this->props

View file

@ -30,6 +30,8 @@ class Docs
public function __construct(
protected string $name
) {
// protect against path traversal
$this->name = basename($name);
$this->kirby = App::instance();
$this->json = $this->read();
}

View file

@ -71,7 +71,7 @@ class Example
public function exists(): bool
{
return is_dir($this->root) === true;
return Dir::exists($this->root, $this->parent->root()) === true;
}
public function file(string $filename): string

View file

@ -231,8 +231,12 @@ abstract class Model
// for card layouts with `cover: true` provide
// crops based on the card ratio
if ($layout === 'cards') {
$ratio = explode('/', $settings['ratio'] ?? '1/1');
$ratio = $ratio[0] / $ratio[1];
$ratio = $settings['ratio'] ?? '1/1';
if (is_numeric($ratio) === false) {
$ratio = explode('/', $ratio);
$ratio = $ratio[0] / $ratio[1];
}
return $image->srcset([
$sizes[0] . 'w' => [

View file

@ -389,7 +389,8 @@ class FileSessionStore extends SessionStore
*/
protected function name(int $expiryTime, string $id): string
{
return $expiryTime . '.' . $id;
// protect against path traversal
return $expiryTime . '.' . basename($id);
}
/**

View file

@ -5,6 +5,7 @@ namespace Kirby\Template;
use Kirby\Cms\App;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\LogicException;
use Kirby\Filesystem\F;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Tpl;
@ -187,7 +188,7 @@ class Snippet extends Tpl
$name = (string)$name;
$file = $root . '/' . $name . '.php';
if (file_exists($file) === false) {
if (F::exists($file, $root) === false) {
$file = $kirby->extensions('snippets')[$name] ?? null;
}

View file

@ -3,6 +3,7 @@
namespace Kirby\Toolkit;
use Closure;
use Exception;
use Kirby\Filesystem\F;
use ReflectionFunction;
@ -60,12 +61,24 @@ class Controller
return $this->function->call($bind, ...$args);
}
public static function load(string $file): static|null
public static function load(string $file, string|null $in = null): static|null
{
if (is_file($file) === false) {
return null;
}
// restrict file paths to the provided root
// to prevent path traversal
if ($in !== null) {
try {
$file = F::realpath($file, $in);
} catch (Exception) {
// don't expose whether the file exists
// (which would have returned `null` above)
return null;
}
}
$function = F::load($file);
if ($function instanceof Closure === false) {

View file

@ -8,7 +8,6 @@ return [
$data = json_decode($json);
$page = page($data->location->page->uri);
$project = page($data->location->project->uri);
$file = $page->file($data->location->file->uuid);
$isReply = $data->location->parent->id ?? false;

View file

@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit37a8e61308b9b6f49cb9835f477f0c64::getLoader();

View file

@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View file

@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}

View file

@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,10 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View file

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

View file

@ -0,0 +1,10 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Kirby\\' => array($vendorDir . '/getkirby/composer-installer/src'),
);

View file

@ -0,0 +1,36 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit37a8e61308b9b6f49cb9835f477f0c64
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit37a8e61308b9b6f49cb9835f477f0c64', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit37a8e61308b9b6f49cb9835f477f0c64', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit37a8e61308b9b6f49cb9835f477f0c64::getInitializer($loader));
$loader->register(true);
return $loader;
}
}

View file

@ -0,0 +1,36 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit37a8e61308b9b6f49cb9835f477f0c64
{
public static $prefixLengthsPsr4 = array (
'K' =>
array (
'Kirby\\' => 6,
),
);
public static $prefixDirsPsr4 = array (
'Kirby\\' =>
array (
0 => __DIR__ . '/..' . '/getkirby/composer-installer/src',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit37a8e61308b9b6f49cb9835f477f0c64::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit37a8e61308b9b6f49cb9835f477f0c64::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit37a8e61308b9b6f49cb9835f477f0c64::$classMap;
}, null, ClassLoader::class);
}
}

View file

@ -0,0 +1,56 @@
{
"packages": [
{
"name": "getkirby/composer-installer",
"version": "1.2.1",
"version_normalized": "1.2.1.0",
"source": {
"type": "git",
"url": "https://github.com/getkirby/composer-installer.git",
"reference": "c98ece30bfba45be7ce457e1102d1b169d922f3d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/getkirby/composer-installer/zipball/c98ece30bfba45be7ce457e1102d1b169d922f3d",
"reference": "c98ece30bfba45be7ce457e1102d1b169d922f3d",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.0 || ^2.0"
},
"require-dev": {
"composer/composer": "^1.8 || ^2.0"
},
"time": "2020-12-28T12:54:39+00:00",
"type": "composer-plugin",
"extra": {
"class": "Kirby\\ComposerInstaller\\Plugin"
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Kirby\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Kirby's custom Composer installer for the Kirby CMS and for Kirby plugins",
"homepage": "https://getkirby.com",
"support": {
"issues": "https://github.com/getkirby/composer-installer/issues",
"source": "https://github.com/getkirby/composer-installer/tree/1.2.1"
},
"funding": [
{
"url": "https://getkirby.com/buy",
"type": "custom"
}
],
"install-path": "../getkirby/composer-installer"
}
],
"dev": true,
"dev-package-names": []
}

View file

@ -0,0 +1,32 @@
<?php return array(
'root' => array(
'name' => 'getkirby/pluginkit',
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'kirby-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'getkirby/composer-installer' => array(
'pretty_version' => '1.2.1',
'version' => '1.2.1.0',
'reference' => 'c98ece30bfba45be7ce457e1102d1b169d922f3d',
'type' => 'composer-plugin',
'install_path' => __DIR__ . '/../getkirby/composer-installer',
'aliases' => array(),
'dev_requirement' => false,
),
'getkirby/pluginkit' => array(
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'kirby-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

View file

@ -0,0 +1,30 @@
{
"name": "getkirby/composer-installer",
"description": "Kirby's custom Composer installer for the Kirby CMS and for Kirby plugins",
"type": "composer-plugin",
"license": "MIT",
"homepage": "https://getkirby.com",
"require": {
"composer-plugin-api": "^1.0 || ^2.0"
},
"require-dev": {
"composer/composer": "^1.8 || ^2.0"
},
"autoload": {
"psr-4": {
"Kirby\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Kirby\\": "tests/"
}
},
"scripts": {
"fix": "php-cs-fixer fix --config .php_cs",
"test": "--stderr --coverage-html=tests/coverage"
},
"extra": {
"class": "Kirby\\ComposerInstaller\\Plugin"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,104 @@
# Kirby Composer Installer
[![CI Status](https://flat.badgen.net/github/checks/getkirby/composer-installer/master)](https://github.com/getkirby/composer-installer/actions?query=workflow%3ACI)
[![Coverage Status](https://flat.badgen.net/coveralls/c/github/getkirby/composer-installer)](https://coveralls.io/github/getkirby/composer-installer)
This is Kirby's custom [Composer installer](https://getcomposer.org/doc/articles/custom-installers.md) for the Kirby CMS.
It is responsible for automatically choosing the correct installation paths if you install the CMS via Composer.
It can also be used to automatically install Kirby plugins to the `site/plugins` directory.
## Installing the CMS
### Default configuration
If you `require` the `getkirby/cms` package in your own `composer.json`, there is nothing else you need to do:
```js
{
"require": {
"getkirby/cms": "^3.0"
}
}
```
Kirby's Composer installer (this repo) will run automatically and will install the CMS to the `kirby` directory.
### Custom installation path
You might want to use a different installation path. The path can be configured like this in your `composer.json`:
```js
{
"require": {
"getkirby/cms": "^3.0"
},
"extra": {
"kirby-cms-path": "kirby" // change this to your custom path
}
}
```
### Disable the installer for the CMS
If you prefer to have the CMS installed to the `vendor` directory, you can disable the custom path entirely:
```js
{
"require": {
"getkirby/cms": "^3.0"
},
"extra": {
"kirby-cms-path": false
}
}
```
Please note that you will need to modify your site's `index.php` to load the `vendor/autoload.php` file instead of Kirby's `bootstrap.php`.
## Installing plugins
### Support in published plugins
Plugins need to require this installer as a Composer dependency to make use of the automatic installation to the `site/plugins` directory.
You can find out more about this in our [plugin documentation](https://getkirby.com/docs/guide/plugins/plugin-setup-basic).
### Usage for plugin users
As a user of Kirby plugins that support this installer, you only need to `require` the plugins in your site's `composer.json`:
```js
{
"require": {
"getkirby/cms": "^3.0",
"superwoman/superplugin": "^1.0"
}
}
```
The installer (this repo) will run automatically, as the plugin dev added it to the plugin's `composer.json`.
### Custom installation path
If your `site/plugins` directory is at a custom path, you can configure the installation path like this in your `composer.json`:
```js
{
"require": {
"getkirby/cms": "^3.0",
"superwoman/superplugin": "^1.0"
},
"extra": {
"kirby-plugin-path": "site/plugins" // change this to your custom path
}
}
```
## License
<http://www.opensource.org/licenses/mit-license.php>
## Author
Lukas Bestle <https://getkirby.com>

View file

@ -0,0 +1,64 @@
<?php
namespace Kirby\ComposerInstaller;
use Composer\Config;
use Composer\Package\PackageInterface;
use InvalidArgumentException;
/**
* @package Kirby Composer Installer
* @author Lukas Bestle <lukas@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://opensource.org/licenses/MIT
*/
class CmsInstaller extends Installer
{
/**
* Decides if the installer supports the given type
*
* @param string $packageType
* @return bool
*/
public function supports($packageType): bool
{
return $packageType === 'kirby-cms';
}
/**
* Returns the installation path of a package
*
* @param \Composer\Package\PackageInterface $package
* @return string
*/
public function getInstallPath(PackageInterface $package): string
{
// get the extra configuration of the top-level package
if ($rootPackage = $this->composer->getPackage()) {
$extra = $rootPackage->getExtra();
} else {
$extra = [];
}
// use path from configuration, otherwise fall back to default
if (isset($extra['kirby-cms-path']) === true) {
$path = $extra['kirby-cms-path'];
} else {
$path = 'kirby';
}
// if explicitly set to something invalid (e.g. `false`), install to vendor dir
if (is_string($path) !== true) {
return parent::getInstallPath($package);
}
// don't allow unsafe directories
$vendorDir = $this->composer->getConfig()->get('vendor-dir', Config::RELATIVE_PATHS) ?? 'vendor';
if ($path === $vendorDir || $path === '.') {
throw new InvalidArgumentException('The path ' . $path . ' is an unsafe installation directory for ' . $package->getPrettyName() . '.');
}
return $path;
}
}

View file

@ -0,0 +1,105 @@
<?php
namespace Kirby\ComposerInstaller;
use Composer\Installer\LibraryInstaller;
use Composer\Package\PackageInterface;
use Composer\Repository\InstalledRepositoryInterface;
use React\Promise\PromiseInterface;
use RuntimeException;
/**
* @package Kirby Composer Installer
* @author Lukas Bestle <lukas@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://opensource.org/licenses/MIT
*/
class Installer extends LibraryInstaller
{
/**
* Decides if the installer supports the given type
*
* @param string $packageType
* @return bool
*/
public function supports($packageType): bool
{
throw new RuntimeException('This method needs to be overridden.'); // @codeCoverageIgnore
}
/**
* Installs a specific package
*
* @param \Composer\Repository\InstalledRepositoryInterface $repo Repository in which to check
* @param \Composer\Package\PackageInterface $package Package instance to install
* @return \React\Promise\PromiseInterface|null
*/
public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
{
// first install the package normally...
$promise = parent::install($repo, $package);
// ...then run custom code
$postInstall = function () use ($package): void {
$this->postInstall($package);
};
// Composer 2 in async mode
if ($promise instanceof PromiseInterface) {
return $promise->then($postInstall);
}
// Composer 1 or Composer 2 without async
$postInstall();
}
/**
* Updates a specific package
*
* @param \Composer\Repository\InstalledRepositoryInterface $repo Repository in which to check
* @param \Composer\Package\PackageInterface $initial Already installed package version
* @param \Composer\Package\PackageInterface $target Updated version
* @return \React\Promise\PromiseInterface|null
*
* @throws \InvalidArgumentException if $initial package is not installed
*/
public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target)
{
// first update the package normally...
$promise = parent::update($repo, $initial, $target);
// ...then run custom code
$postInstall = function () use ($target): void {
$this->postInstall($target);
};
// Composer 2 in async mode
if ($promise instanceof PromiseInterface) {
return $promise->then($postInstall);
}
// Composer 1 or Composer 2 without async
$postInstall();
}
/**
* Custom handler that will be called after each package
* installation or update
*
* @param \Composer\Package\PackageInterface $package
* @return void
*/
protected function postInstall(PackageInterface $package)
{
// remove the package's `vendor` directory to avoid duplicated autoloader and vendor code
$packageVendorDir = $this->getInstallPath($package) . '/vendor';
if (is_dir($packageVendorDir) === true) {
$success = $this->filesystem->removeDirectory($packageVendorDir);
if ($success !== true) {
throw new RuntimeException('Could not completely delete ' . $packageVendorDir . ', aborting.'); // @codeCoverageIgnore
}
}
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Kirby\ComposerInstaller;
use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;
/**
* @package Kirby Composer Installer
* @author Lukas Bestle <lukas@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://opensource.org/licenses/MIT
*/
class Plugin implements PluginInterface
{
/**
* Apply plugin modifications to Composer
*
* @param \Composer\Composer $composer
* @param \Composer\IO\IOInterface $io
* @return void
*/
public function activate(Composer $composer, IOInterface $io): void
{
$installationManager = $composer->getInstallationManager();
$installationManager->addInstaller(new CmsInstaller($io, $composer));
$installationManager->addInstaller(new PluginInstaller($io, $composer));
}
/**
* Remove any hooks from Composer
*
* @codeCoverageIgnore
*
* @param \Composer\Composer $composer
* @param \Composer\IO\IOInterface $io
* @return void
*/
public function deactivate(Composer $composer, IOInterface $io): void
{
// nothing to do
}
/**
* Prepare the plugin to be uninstalled
*
* @codeCoverageIgnore
*
* @param Composer $composer
* @param IOInterface $io
* @return void
*/
public function uninstall(Composer $composer, IOInterface $io): void
{
// nothing to do
}
}

View file

@ -0,0 +1,112 @@
<?php
namespace Kirby\ComposerInstaller;
use Composer\Package\PackageInterface;
use InvalidArgumentException;
/**
* @package Kirby Composer Installer
* @author Lukas Bestle <lukas@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://opensource.org/licenses/MIT
*/
class PluginInstaller extends Installer
{
/**
* Decides if the installer supports the given type
*
* @param string $packageType
* @return bool
*/
public function supports($packageType): bool
{
return $packageType === 'kirby-plugin';
}
/**
* Returns the installation path of a package
*
* @param \Composer\Package\PackageInterface $package
* @return string path
*/
public function getInstallPath(PackageInterface $package): string
{
// place into `vendor` directory as usual if Pluginkit is not supported
if ($this->supportsPluginkit($package) !== true) {
return parent::getInstallPath($package);
}
// get the extra configuration of the top-level package
if ($rootPackage = $this->composer->getPackage()) {
$extra = $rootPackage->getExtra();
} else {
$extra = [];
}
// use base path from configuration, otherwise fall back to default
$basePath = $extra['kirby-plugin-path'] ?? 'site/plugins';
if (is_string($basePath) !== true) {
throw new InvalidArgumentException('Invalid "kirby-plugin-path" option');
}
// determine the plugin name from its package name;
// can be overridden in the plugin's `composer.json`
$prettyName = $package->getPrettyName();
$pluginExtra = $package->getExtra();
if (empty($pluginExtra['installer-name']) === false) {
$name = $pluginExtra['installer-name'];
if (is_string($name) !== true) {
throw new InvalidArgumentException('Invalid "installer-name" option in plugin ' . $prettyName);
}
} elseif (strpos($prettyName, '/') !== false) {
// use name after the slash
$name = explode('/', $prettyName)[1];
} else {
$name = $prettyName;
}
// build destination path from base path and plugin name
return $basePath . '/' . $name;
}
/**
* Custom handler that will be called after each package
* installation or update
*
* @param \Composer\Package\PackageInterface $package
* @return void
*/
protected function postInstall(PackageInterface $package): void
{
// only continue if Pluginkit is supported
if ($this->supportsPluginkit($package) !== true) {
return;
}
parent::postInstall($package);
}
/**
* Checks if the package has explicitly required this installer;
* otherwise (if the Pluginkit is not yet supported by the plugin)
* the installer will fall back to the behavior of the LibraryInstaller
*
* @param \Composer\Package\PackageInterface $package
* @return bool
*/
protected function supportsPluginkit(PackageInterface $package): bool
{
foreach ($package->getRequires() as $link) {
if ($link->getTarget() === 'getkirby/composer-installer') {
return true;
}
}
// no required package is the installer
return false;
}
}