diff --git a/core/composer.lock b/core/composer.lock
index 0b61a4be80..389b5e52dd 100644
--- a/core/composer.lock
+++ b/core/composer.lock
@@ -868,16 +868,16 @@
},
{
"name": "doctrine/dbal",
- "version": "4.4.3",
+ "version": "4.4.4",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
- "reference": "61e730f1658814821a85f2402c945f3883407dec"
+ "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/dbal/zipball/61e730f1658814821a85f2402c945f3883407dec",
- "reference": "61e730f1658814821a85f2402c945f3883407dec",
+ "url": "https://api.github.com/repos/doctrine/dbal/zipball/fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce",
+ "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce",
"shasum": ""
},
"require": {
@@ -954,7 +954,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
- "source": "https://github.com/doctrine/dbal/tree/4.4.3"
+ "source": "https://github.com/doctrine/dbal/tree/4.4.4"
},
"funding": [
{
@@ -970,7 +970,7 @@
"type": "tidelift"
}
],
- "time": "2026-03-20T08:52:12+00:00"
+ "time": "2026-07-21T14:34:40+00:00"
},
{
"name": "doctrine/deprecations",
@@ -1525,21 +1525,21 @@
},
{
"name": "guzzlehttp/guzzle",
- "version": "7.15.1",
+ "version": "7.15.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "61443dfb33c62f308ee8add20f45b4d6e4bf8d2f"
+ "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/61443dfb33c62f308ee8add20f45b4d6e4bf8d2f",
- "reference": "61443dfb33c62f308ee8add20f45b4d6e4bf8d2f",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc",
+ "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc",
"shasum": ""
},
"require": {
"ext-json": "*",
- "guzzlehttp/promises": "^2.5.1",
+ "guzzlehttp/promises": "^2.5.2",
"guzzlehttp/psr7": "^2.13",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
@@ -1633,7 +1633,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
- "source": "https://github.com/guzzle/guzzle/tree/7.15.1"
+ "source": "https://github.com/guzzle/guzzle/tree/7.15.3"
},
"funding": [
{
@@ -1649,20 +1649,20 @@
"type": "tidelift"
}
],
- "time": "2026-07-18T11:23:11+00:00"
+ "time": "2026-08-05T19:48:21+00:00"
},
{
"name": "guzzlehttp/promises",
- "version": "2.5.1",
+ "version": "2.5.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
- "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29"
+ "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/promises/zipball/9ad1e4fc607446a055b95870c7f668e93b5cff29",
- "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/2823687acff28b2dbe67b2508a6b300e2c3fa4ce",
+ "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce",
"shasum": ""
},
"require": {
@@ -1717,7 +1717,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
- "source": "https://github.com/guzzle/promises/tree/2.5.1"
+ "source": "https://github.com/guzzle/promises/tree/2.5.2"
},
"funding": [
{
@@ -1733,7 +1733,7 @@
"type": "tidelift"
}
],
- "time": "2026-07-08T15:48:39+00:00"
+ "time": "2026-08-05T19:30:54+00:00"
},
{
"name": "guzzlehttp/psr7",
@@ -1942,7 +1942,7 @@
},
{
"name": "illuminate/bus",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/bus.git",
@@ -1995,7 +1995,7 @@
},
{
"name": "illuminate/cache",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/cache.git",
@@ -2057,7 +2057,7 @@
},
{
"name": "illuminate/collections",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/collections.git",
@@ -2117,7 +2117,7 @@
},
{
"name": "illuminate/conditionable",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/conditionable.git",
@@ -2163,7 +2163,7 @@
},
{
"name": "illuminate/config",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/config.git",
@@ -2211,16 +2211,16 @@
},
{
"name": "illuminate/console",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/console.git",
- "reference": "91732d7ae739c86dd9055c4dc850c2b4efccb47f"
+ "reference": "ce37b63fb90f036dbd34263d84725f79c6d6f221"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/console/zipball/91732d7ae739c86dd9055c4dc850c2b4efccb47f",
- "reference": "91732d7ae739c86dd9055c4dc850c2b4efccb47f",
+ "url": "https://api.github.com/repos/illuminate/console/zipball/ce37b63fb90f036dbd34263d84725f79c6d6f221",
+ "reference": "ce37b63fb90f036dbd34263d84725f79c6d6f221",
"shasum": ""
},
"require": {
@@ -2273,11 +2273,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2026-03-21T11:33:57+00:00"
+ "time": "2026-07-29T14:35:23+00:00"
},
{
"name": "illuminate/container",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/container.git",
@@ -2339,7 +2339,7 @@
},
{
"name": "illuminate/contracts",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
@@ -2387,16 +2387,16 @@
},
{
"name": "illuminate/database",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/database.git",
- "reference": "13ea191cbbee8cc615188c990ec26428df89fd29"
+ "reference": "6d539459fb55732a13e7f962afeffa9dca2a1cce"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/database/zipball/13ea191cbbee8cc615188c990ec26428df89fd29",
- "reference": "13ea191cbbee8cc615188c990ec26428df89fd29",
+ "url": "https://api.github.com/repos/illuminate/database/zipball/6d539459fb55732a13e7f962afeffa9dca2a1cce",
+ "reference": "6d539459fb55732a13e7f962afeffa9dca2a1cce",
"shasum": ""
},
"require": {
@@ -2456,11 +2456,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2026-06-19T13:46:51+00:00"
+ "time": "2026-08-14T15:00:48+00:00"
},
{
"name": "illuminate/events",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/events.git",
@@ -2515,16 +2515,16 @@
},
{
"name": "illuminate/filesystem",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/filesystem.git",
- "reference": "3b5ad9b385595d735689ebc2bfe701567cc4f1c3"
+ "reference": "dc0b7440f12753060b574fa8c20c2db259fa37c9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/filesystem/zipball/3b5ad9b385595d735689ebc2bfe701567cc4f1c3",
- "reference": "3b5ad9b385595d735689ebc2bfe701567cc4f1c3",
+ "url": "https://api.github.com/repos/illuminate/filesystem/zipball/dc0b7440f12753060b574fa8c20c2db259fa37c9",
+ "reference": "dc0b7440f12753060b574fa8c20c2db259fa37c9",
"shasum": ""
},
"require": {
@@ -2578,20 +2578,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2026-06-02T13:54:37+00:00"
+ "time": "2026-08-12T13:55:25+00:00"
},
{
"name": "illuminate/http",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/http.git",
- "reference": "15f6ff6d3cc237500575827c3d106c63ceb48984"
+ "reference": "5f4f82ed0ed550a01f5d95c4ddbbad409fb0d560"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/http/zipball/15f6ff6d3cc237500575827c3d106c63ceb48984",
- "reference": "15f6ff6d3cc237500575827c3d106c63ceb48984",
+ "url": "https://api.github.com/repos/illuminate/http/zipball/5f4f82ed0ed550a01f5d95c4ddbbad409fb0d560",
+ "reference": "5f4f82ed0ed550a01f5d95c4ddbbad409fb0d560",
"shasum": ""
},
"require": {
@@ -2640,11 +2640,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2026-05-27T23:28:12+00:00"
+ "time": "2026-08-06T14:28:07+00:00"
},
{
"name": "illuminate/log",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/log.git",
@@ -2697,7 +2697,7 @@
},
{
"name": "illuminate/macroable",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/macroable.git",
@@ -2743,7 +2743,7 @@
},
{
"name": "illuminate/pagination",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/pagination.git",
@@ -2793,7 +2793,7 @@
},
{
"name": "illuminate/pipeline",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/pipeline.git",
@@ -2845,7 +2845,7 @@
},
{
"name": "illuminate/queue",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/queue.git",
@@ -2912,7 +2912,7 @@
},
{
"name": "illuminate/redis",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/redis.git",
@@ -2966,7 +2966,7 @@
},
{
"name": "illuminate/reflection",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/reflection.git",
@@ -3017,16 +3017,16 @@
},
{
"name": "illuminate/routing",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/routing.git",
- "reference": "f6c107d75cefff875aff53e0aa6a32c2341432a9"
+ "reference": "5d3cc9beca82c359f16e6c1d072903087e428ca1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/routing/zipball/f6c107d75cefff875aff53e0aa6a32c2341432a9",
- "reference": "f6c107d75cefff875aff53e0aa6a32c2341432a9",
+ "url": "https://api.github.com/repos/illuminate/routing/zipball/5d3cc9beca82c359f16e6c1d072903087e428ca1",
+ "reference": "5d3cc9beca82c359f16e6c1d072903087e428ca1",
"shasum": ""
},
"require": {
@@ -3079,11 +3079,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2026-05-05T20:47:39+00:00"
+ "time": "2026-08-05T14:58:10+00:00"
},
{
"name": "illuminate/session",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/session.git",
@@ -3140,16 +3140,16 @@
},
{
"name": "illuminate/support",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
- "reference": "bc7bc2db3e635697f81ea123511ae25b014a63fb"
+ "reference": "7d7a7d2dc5084aa4b1fd8b0a42094e1d3fb51e00"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/support/zipball/bc7bc2db3e635697f81ea123511ae25b014a63fb",
- "reference": "bc7bc2db3e635697f81ea123511ae25b014a63fb",
+ "url": "https://api.github.com/repos/illuminate/support/zipball/7d7a7d2dc5084aa4b1fd8b0a42094e1d3fb51e00",
+ "reference": "7d7a7d2dc5084aa4b1fd8b0a42094e1d3fb51e00",
"shasum": ""
},
"require": {
@@ -3216,11 +3216,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2026-07-13T16:08:36+00:00"
+ "time": "2026-08-06T14:28:07+00:00"
},
{
"name": "illuminate/translation",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/translation.git",
@@ -3271,16 +3271,16 @@
},
{
"name": "illuminate/validation",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/validation.git",
- "reference": "297cff9c69885b1a9125ee4f26f77ef570abb2b6"
+ "reference": "ce2bb8d019e2510d1892b0c87f9086ea802c28e4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/validation/zipball/297cff9c69885b1a9125ee4f26f77ef570abb2b6",
- "reference": "297cff9c69885b1a9125ee4f26f77ef570abb2b6",
+ "url": "https://api.github.com/repos/illuminate/validation/zipball/ce2bb8d019e2510d1892b0c87f9086ea802c28e4",
+ "reference": "ce2bb8d019e2510d1892b0c87f9086ea802c28e4",
"shasum": ""
},
"require": {
@@ -3330,11 +3330,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2026-05-17T14:42:21+00:00"
+ "time": "2026-08-18T12:21:48+00:00"
},
{
"name": "illuminate/view",
- "version": "v12.64.0",
+ "version": "v12.67.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/view.git",
@@ -3520,16 +3520,16 @@
},
{
"name": "laravel/prompts",
- "version": "v0.3.21",
+ "version": "v0.3.23",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
- "reference": "7753c65c281c2550c7c183f14e18062073b7d821"
+ "reference": "b7b4c35e5bc47450f6b6238c6cc9c47ba19b2221"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/prompts/zipball/7753c65c281c2550c7c183f14e18062073b7d821",
- "reference": "7753c65c281c2550c7c183f14e18062073b7d821",
+ "url": "https://api.github.com/repos/laravel/prompts/zipball/b7b4c35e5bc47450f6b6238c6cc9c47ba19b2221",
+ "reference": "b7b4c35e5bc47450f6b6238c6cc9c47ba19b2221",
"shasum": ""
},
"require": {
@@ -3573,22 +3573,22 @@
"description": "Add beautiful and user-friendly forms to your command-line applications.",
"support": {
"issues": "https://github.com/laravel/prompts/issues",
- "source": "https://github.com/laravel/prompts/tree/v0.3.21"
+ "source": "https://github.com/laravel/prompts/tree/v0.3.23"
},
- "time": "2026-06-26T00:11:25+00:00"
+ "time": "2026-08-11T18:58:24+00:00"
},
{
"name": "laravel/serializable-closure",
- "version": "v2.0.13",
+ "version": "v2.0.15",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
- "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce"
+ "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce",
- "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce",
+ "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/dccd8bcb851bb03fcc005df650b708b57cc52661",
+ "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661",
"shasum": ""
},
"require": {
@@ -3636,7 +3636,7 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
- "time": "2026-04-16T14:03:50+00:00"
+ "time": "2026-07-21T16:49:22+00:00"
},
{
"name": "league/flysystem",
@@ -4004,16 +4004,16 @@
},
{
"name": "nesbot/carbon",
- "version": "3.13.1",
+ "version": "3.13.2",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon.git",
- "reference": "2937ad3d1d2c506fd2bc97d571438a95641f44e2"
+ "reference": "a1c54919f5fff9800cd03c32bd01defd5a4061cb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/2937ad3d1d2c506fd2bc97d571438a95641f44e2",
- "reference": "2937ad3d1d2c506fd2bc97d571438a95641f44e2",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/a1c54919f5fff9800cd03c32bd01defd5a4061cb",
+ "reference": "a1c54919f5fff9800cd03c32bd01defd5a4061cb",
"shasum": ""
},
"require": {
@@ -4105,7 +4105,7 @@
"type": "tidelift"
}
],
- "time": "2026-07-09T18:23:49+00:00"
+ "time": "2026-08-08T11:40:35+00:00"
},
{
"name": "nunomaduro/termwind",
@@ -4353,16 +4353,16 @@
},
{
"name": "predis/predis",
- "version": "v3.5.1",
+ "version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/predis/predis.git",
- "reference": "5c996db191ee2d9bafe651f454b1fca16754271b"
+ "reference": "2ff20c08bb63697245ffee3f198d1673086c302d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/predis/predis/zipball/5c996db191ee2d9bafe651f454b1fca16754271b",
- "reference": "5c996db191ee2d9bafe651f454b1fca16754271b",
+ "url": "https://api.github.com/repos/predis/predis/zipball/2ff20c08bb63697245ffee3f198d1673086c302d",
+ "reference": "2ff20c08bb63697245ffee3f198d1673086c302d",
"shasum": ""
},
"require": {
@@ -4404,7 +4404,7 @@
],
"support": {
"issues": "https://github.com/predis/predis/issues",
- "source": "https://github.com/predis/predis/tree/v3.5.1"
+ "source": "https://github.com/predis/predis/tree/v3.6.0"
},
"funding": [
{
@@ -4412,7 +4412,7 @@
"type": "github"
}
],
- "time": "2026-06-11T16:56:53+00:00"
+ "time": "2026-08-14T23:07:56+00:00"
},
{
"name": "psr/cache",
@@ -5615,16 +5615,16 @@
},
{
"name": "secondnetwork/blade-tabler-icons",
- "version": "v3.44.0",
+ "version": "v3.46.0",
"source": {
"type": "git",
"url": "https://github.com/secondnetwork/blade-tabler-icons.git",
- "reference": "856ad75e2c0704096bf4a708b6464620e41d1a34"
+ "reference": "aedaebc18f382a9d0a1870e9ad394c5c89abd72b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/secondnetwork/blade-tabler-icons/zipball/856ad75e2c0704096bf4a708b6464620e41d1a34",
- "reference": "856ad75e2c0704096bf4a708b6464620e41d1a34",
+ "url": "https://api.github.com/repos/secondnetwork/blade-tabler-icons/zipball/aedaebc18f382a9d0a1870e9ad394c5c89abd72b",
+ "reference": "aedaebc18f382a9d0a1870e9ad394c5c89abd72b",
"shasum": ""
},
"require": {
@@ -5667,9 +5667,9 @@
],
"support": {
"issues": "https://github.com/secondnetwork/blade-tabler-icons/issues",
- "source": "https://github.com/secondnetwork/blade-tabler-icons/tree/v3.44.0"
+ "source": "https://github.com/secondnetwork/blade-tabler-icons/tree/v3.46.0"
},
- "time": "2026-05-11T14:36:11+00:00"
+ "time": "2026-07-30T13:49:44+00:00"
},
{
"name": "seld/jsonlint",
@@ -5737,16 +5737,16 @@
},
{
"name": "seld/phar-utils",
- "version": "1.2.1",
+ "version": "1.2.2",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/phar-utils.git",
- "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c"
+ "reference": "990bbd0e92caa216d52eca0935f6e35e589bfaa5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c",
- "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c",
+ "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/990bbd0e92caa216d52eca0935f6e35e589bfaa5",
+ "reference": "990bbd0e92caa216d52eca0935f6e35e589bfaa5",
"shasum": ""
},
"require": {
@@ -5779,9 +5779,9 @@
],
"support": {
"issues": "https://github.com/Seldaek/phar-utils/issues",
- "source": "https://github.com/Seldaek/phar-utils/tree/1.2.1"
+ "source": "https://github.com/Seldaek/phar-utils/tree/1.2.2"
},
- "time": "2022-08-31T10:31:18+00:00"
+ "time": "2026-08-01T12:48:55+00:00"
},
{
"name": "seld/signal-handler",
@@ -6005,16 +6005,16 @@
},
{
"name": "symfony/console",
- "version": "v7.4.14",
+ "version": "v7.4.16",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87"
+ "reference": "f4c69c9aed03abf933b294257d618bdd9b30a06d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87",
- "reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87",
+ "url": "https://api.github.com/repos/symfony/console/zipball/f4c69c9aed03abf933b294257d618bdd9b30a06d",
+ "reference": "f4c69c9aed03abf933b294257d618bdd9b30a06d",
"shasum": ""
},
"require": {
@@ -6079,7 +6079,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.4.14"
+ "source": "https://github.com/symfony/console/tree/v7.4.16"
},
"funding": [
{
@@ -6099,7 +6099,7 @@
"type": "tidelift"
}
],
- "time": "2026-06-16T11:50:14+00:00"
+ "time": "2026-07-31T12:37:14+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -6174,16 +6174,16 @@
},
{
"name": "symfony/error-handler",
- "version": "v7.4.14",
+ "version": "v7.4.15",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
- "reference": "4e1a093b481f323e6e326451f9760c3868430673"
+ "reference": "d49f6a19f326db41ae7103bdc38e3eb35a791261"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/error-handler/zipball/4e1a093b481f323e6e326451f9760c3868430673",
- "reference": "4e1a093b481f323e6e326451f9760c3868430673",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/d49f6a19f326db41ae7103bdc38e3eb35a791261",
+ "reference": "d49f6a19f326db41ae7103bdc38e3eb35a791261",
"shasum": ""
},
"require": {
@@ -6232,7 +6232,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/error-handler/tree/v7.4.14"
+ "source": "https://github.com/symfony/error-handler/tree/v7.4.15"
},
"funding": [
{
@@ -6252,20 +6252,20 @@
"type": "tidelift"
}
],
- "time": "2026-06-05T06:22:21+00:00"
+ "time": "2026-07-21T15:13:06+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v7.4.14",
+ "version": "v7.4.15",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "51fe3d170227be8d1772214b82ae506e15ed78ff"
+ "reference": "336e7f3b9e95aba04f93ea9143920c2186abfbb9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/51fe3d170227be8d1772214b82ae506e15ed78ff",
- "reference": "51fe3d170227be8d1772214b82ae506e15ed78ff",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/336e7f3b9e95aba04f93ea9143920c2186abfbb9",
+ "reference": "336e7f3b9e95aba04f93ea9143920c2186abfbb9",
"shasum": ""
},
"require": {
@@ -6317,7 +6317,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.14"
+ "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.15"
},
"funding": [
{
@@ -6337,7 +6337,7 @@
"type": "tidelift"
}
],
- "time": "2026-06-06T11:10:32+00:00"
+ "time": "2026-07-21T15:13:06+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -6421,16 +6421,16 @@
},
{
"name": "symfony/filesystem",
- "version": "v7.4.11",
+ "version": "v7.4.15",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50"
+ "reference": "ff16a16bf87fdf264638b8f6995b3515975e3c79"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
- "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/ff16a16bf87fdf264638b8f6995b3515975e3c79",
+ "reference": "ff16a16bf87fdf264638b8f6995b3515975e3c79",
"shasum": ""
},
"require": {
@@ -6467,7 +6467,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v7.4.11"
+ "source": "https://github.com/symfony/filesystem/tree/v7.4.15"
},
"funding": [
{
@@ -6487,7 +6487,7 @@
"type": "tidelift"
}
],
- "time": "2026-05-11T16:38:44+00:00"
+ "time": "2026-07-22T07:36:05+00:00"
},
{
"name": "symfony/finder",
@@ -6559,16 +6559,16 @@
},
{
"name": "symfony/http-foundation",
- "version": "v7.4.14",
+ "version": "v7.4.16",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "06db5ae1552177bf8572f8908839f12e3c06aed3"
+ "reference": "b676451bb638e99a7d34d8a2be90406822e301eb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/06db5ae1552177bf8572f8908839f12e3c06aed3",
- "reference": "06db5ae1552177bf8572f8908839f12e3c06aed3",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b676451bb638e99a7d34d8a2be90406822e301eb",
+ "reference": "b676451bb638e99a7d34d8a2be90406822e301eb",
"shasum": ""
},
"require": {
@@ -6617,7 +6617,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v7.4.14"
+ "source": "https://github.com/symfony/http-foundation/tree/v7.4.16"
},
"funding": [
{
@@ -6637,20 +6637,20 @@
"type": "tidelift"
}
],
- "time": "2026-06-11T07:31:44+00:00"
+ "time": "2026-08-07T11:50:27+00:00"
},
{
"name": "symfony/http-kernel",
- "version": "v7.4.14",
+ "version": "v7.4.16",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "e99af79b1e776646eda0e1c23b7b45c184ff99be"
+ "reference": "f5e728670fa2218ae8be8ea91f2b44b7d6e5304c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/e99af79b1e776646eda0e1c23b7b45c184ff99be",
- "reference": "e99af79b1e776646eda0e1c23b7b45c184ff99be",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f5e728670fa2218ae8be8ea91f2b44b7d6e5304c",
+ "reference": "f5e728670fa2218ae8be8ea91f2b44b7d6e5304c",
"shasum": ""
},
"require": {
@@ -6708,7 +6708,7 @@
"symfony/validator": "^6.4|^7.0|^8.0",
"symfony/var-dumper": "^6.4|^7.0|^8.0",
"symfony/var-exporter": "^6.4|^7.0|^8.0",
- "twig/twig": "^3.12"
+ "twig/twig": "^3.12|^4.0"
},
"type": "library",
"autoload": {
@@ -6736,7 +6736,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-kernel/tree/v7.4.14"
+ "source": "https://github.com/symfony/http-kernel/tree/v7.4.16"
},
"funding": [
{
@@ -6756,20 +6756,20 @@
"type": "tidelift"
}
],
- "time": "2026-06-27T09:14:35+00:00"
+ "time": "2026-08-07T18:00:13+00:00"
},
{
"name": "symfony/mime",
- "version": "v7.4.13",
+ "version": "v7.4.16",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "a845722765c4f6b2ce88beaf4f4479975b186770"
+ "reference": "20094b76a7106dbe978d31cd3bd7aa1ed248d0e5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/a845722765c4f6b2ce88beaf4f4479975b186770",
- "reference": "a845722765c4f6b2ce88beaf4f4479975b186770",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/20094b76a7106dbe978d31cd3bd7aa1ed248d0e5",
+ "reference": "20094b76a7106dbe978d31cd3bd7aa1ed248d0e5",
"shasum": ""
},
"require": {
@@ -6825,7 +6825,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v7.4.13"
+ "source": "https://github.com/symfony/mime/tree/v7.4.16"
},
"funding": [
{
@@ -6845,7 +6845,7 @@
"type": "tidelift"
}
],
- "time": "2026-05-23T16:22:37+00:00"
+ "time": "2026-08-07T14:56:57+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -6932,16 +6932,16 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.38.1",
+ "version": "v1.41.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "e9247d281d694a5120554d9afaf54e070e88a603"
+ "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603",
- "reference": "e9247d281d694a5120554d9afaf54e070e88a603",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d",
+ "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d",
"shasum": ""
},
"require": {
@@ -6990,7 +6990,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0"
},
"funding": [
{
@@ -7010,7 +7010,7 @@
"type": "tidelift"
}
],
- "time": "2026-05-26T05:58:03+00:00"
+ "time": "2026-07-28T08:25:59+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
@@ -7351,16 +7351,16 @@
},
{
"name": "symfony/polyfill-php85",
- "version": "v1.38.1",
+ "version": "v1.41.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php85.git",
- "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1"
+ "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1",
- "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1",
+ "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a",
+ "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a",
"shasum": ""
},
"require": {
@@ -7407,7 +7407,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1"
+ "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0"
},
"funding": [
{
@@ -7427,7 +7427,7 @@
"type": "tidelift"
}
],
- "time": "2026-05-26T02:25:22+00:00"
+ "time": "2026-07-01T12:47:55+00:00"
},
{
"name": "symfony/process",
@@ -7496,16 +7496,16 @@
},
{
"name": "symfony/routing",
- "version": "v7.4.13",
+ "version": "v7.4.15",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d"
+ "reference": "80c0a93d3f8e7499f716204a1fb38ead942a7a2b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/3a162171bb008e5e0f15dce6581373a4c0e8390d",
- "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/80c0a93d3f8e7499f716204a1fb38ead942a7a2b",
+ "reference": "80c0a93d3f8e7499f716204a1fb38ead942a7a2b",
"shasum": ""
},
"require": {
@@ -7557,7 +7557,7 @@
"url"
],
"support": {
- "source": "https://github.com/symfony/routing/tree/v7.4.13"
+ "source": "https://github.com/symfony/routing/tree/v7.4.15"
},
"funding": [
{
@@ -7577,7 +7577,7 @@
"type": "tidelift"
}
],
- "time": "2026-05-24T11:20:33+00:00"
+ "time": "2026-07-21T15:13:06+00:00"
},
{
"name": "symfony/service-contracts",
@@ -7668,16 +7668,16 @@
},
{
"name": "symfony/string",
- "version": "v7.4.13",
+ "version": "v7.4.15",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde"
+ "reference": "e394af32256bf9e7bf80849d95e589167c10097b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/961683010db3b27ec6ebcd7308e6e1ee8fa7ffde",
- "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde",
+ "url": "https://api.github.com/repos/symfony/string/zipball/e394af32256bf9e7bf80849d95e589167c10097b",
+ "reference": "e394af32256bf9e7bf80849d95e589167c10097b",
"shasum": ""
},
"require": {
@@ -7735,7 +7735,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v7.4.13"
+ "source": "https://github.com/symfony/string/tree/v7.4.15"
},
"funding": [
{
@@ -7755,20 +7755,20 @@
"type": "tidelift"
}
],
- "time": "2026-05-23T15:23:29+00:00"
+ "time": "2026-07-28T07:33:02+00:00"
},
{
"name": "symfony/translation",
- "version": "v7.4.14",
+ "version": "v7.4.16",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "a1af4dacb24eb7ef4f1ca71b94da8ddbce572281"
+ "reference": "501e0ff4553c744209ca1a68790d8a4541563710"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/a1af4dacb24eb7ef4f1ca71b94da8ddbce572281",
- "reference": "a1af4dacb24eb7ef4f1ca71b94da8ddbce572281",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/501e0ff4553c744209ca1a68790d8a4541563710",
+ "reference": "501e0ff4553c744209ca1a68790d8a4541563710",
"shasum": ""
},
"require": {
@@ -7835,7 +7835,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/translation/tree/v7.4.14"
+ "source": "https://github.com/symfony/translation/tree/v7.4.16"
},
"funding": [
{
@@ -7855,7 +7855,7 @@
"type": "tidelift"
}
],
- "time": "2026-06-06T09:33:19+00:00"
+ "time": "2026-07-30T12:37:26+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -7941,16 +7941,16 @@
},
{
"name": "symfony/var-dumper",
- "version": "v7.4.14",
+ "version": "v7.4.15",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358"
+ "reference": "04ba4add636a95ff437af3a5a9499bb1d6c6d4bd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358",
- "reference": "9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/04ba4add636a95ff437af3a5a9499bb1d6c6d4bd",
+ "reference": "04ba4add636a95ff437af3a5a9499bb1d6c6d4bd",
"shasum": ""
},
"require": {
@@ -7966,7 +7966,7 @@
"symfony/http-kernel": "^6.4|^7.0|^8.0",
"symfony/process": "^6.4|^7.0|^8.0",
"symfony/uid": "^6.4|^7.0|^8.0",
- "twig/twig": "^3.12"
+ "twig/twig": "^3.12|^4.0"
},
"bin": [
"Resources/bin/var-dump-server"
@@ -8004,7 +8004,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v7.4.14"
+ "source": "https://github.com/symfony/var-dumper/tree/v7.4.15"
},
"funding": [
{
@@ -8024,20 +8024,20 @@
"type": "tidelift"
}
],
- "time": "2026-06-08T20:24:16+00:00"
+ "time": "2026-07-21T15:13:06+00:00"
},
{
"name": "tracy/tracy",
- "version": "v2.12.0",
+ "version": "v2.12.1",
"source": {
"type": "git",
"url": "https://github.com/nette/tracy.git",
- "reference": "535eda4871fd04f2756c8d95f6bdfa43706d79be"
+ "reference": "90d24872cd97202f79281aac7eb09c27ec48bf69"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/tracy/zipball/535eda4871fd04f2756c8d95f6bdfa43706d79be",
- "reference": "535eda4871fd04f2756c8d95f6bdfa43706d79be",
+ "url": "https://api.github.com/repos/nette/tracy/zipball/90d24872cd97202f79281aac7eb09c27ec48bf69",
+ "reference": "90d24872cd97202f79281aac7eb09c27ec48bf69",
"shasum": ""
},
"require": {
@@ -8102,9 +8102,9 @@
],
"support": {
"issues": "https://github.com/nette/tracy/issues",
- "source": "https://github.com/nette/tracy/tree/v2.12.0"
+ "source": "https://github.com/nette/tracy/tree/v2.12.1"
},
- "time": "2026-05-04T00:23:53+00:00"
+ "time": "2026-08-18T10:56:11+00:00"
},
{
"name": "vlucas/phpdotenv",
@@ -8549,19 +8549,21 @@
},
{
"name": "hamcrest/hamcrest-php",
- "version": "v2.1.1",
+ "version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/hamcrest/hamcrest-php.git",
- "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487"
+ "reference": "b61cd040da1a4925bc90a51c074f5297e7c0fa52"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487",
- "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487",
+ "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/b61cd040da1a4925bc90a51c074f5297e7c0fa52",
+ "reference": "b61cd040da1a4925bc90a51c074f5297e7c0fa52",
"shasum": ""
},
"require": {
+ "ext-ctype": "*",
+ "ext-dom": "*",
"php": "^7.4|^8.0"
},
"replace": {
@@ -8570,13 +8572,15 @@
"kodova/hamcrest-php": "*"
},
"require-dev": {
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-phpunit": "^2.0",
"phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0",
"phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.1-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
@@ -8594,9 +8598,9 @@
],
"support": {
"issues": "https://github.com/hamcrest/hamcrest-php/issues",
- "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1"
+ "source": "https://github.com/hamcrest/hamcrest-php/tree/v3.0.0"
},
- "time": "2025-04-30T06:54:44+00:00"
+ "time": "2026-03-17T11:56:53+00:00"
},
{
"name": "jean85/pretty-package-versions",
@@ -8660,29 +8664,28 @@
},
{
"name": "mockery/mockery",
- "version": "1.6.12",
+ "version": "1.6.15",
"source": {
"type": "git",
"url": "https://github.com/mockery/mockery.git",
- "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699"
+ "reference": "967a801bd188989a5669bd280f252d51c0fdc9ee"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699",
- "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699",
+ "url": "https://api.github.com/repos/mockery/mockery/zipball/967a801bd188989a5669bd280f252d51c0fdc9ee",
+ "reference": "967a801bd188989a5669bd280f252d51c0fdc9ee",
"shasum": ""
},
"require": {
- "hamcrest/hamcrest-php": "^2.0.1",
- "lib-pcre": ">=7.0",
+ "hamcrest/hamcrest-php": "^2.0 || ^3.0",
"php": ">=7.3"
},
"conflict": {
"phpunit/phpunit": "<8.0"
},
"require-dev": {
- "phpunit/phpunit": "^8.5 || ^9.6.17",
- "symplify/easy-coding-standard": "^12.1.14"
+ "phpunit/phpunit": "^9.6.36",
+ "symplify/easy-coding-standard": "^13.2.17"
},
"type": "library",
"autoload": {
@@ -8739,24 +8742,24 @@
"security": "https://github.com/mockery/mockery/security/advisories",
"source": "https://github.com/mockery/mockery"
},
- "time": "2024-05-16T03:13:13+00:00"
+ "time": "2026-08-19T19:37:52+00:00"
},
{
"name": "myclabs/deep-copy",
- "version": "1.13.4",
+ "version": "1.14.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
+ "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
- "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae",
+ "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae",
"shasum": ""
},
"require": {
- "php": "^7.1 || ^8.0"
+ "php": "^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
@@ -8791,15 +8794,15 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
- "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0"
},
"funding": [
{
- "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
- "type": "tidelift"
+ "url": "https://github.com/mnapoli",
+ "type": "github"
}
],
- "time": "2025-08-01T08:46:24+00:00"
+ "time": "2026-08-11T10:17:44+00:00"
},
{
"name": "nikic/php-parser",
@@ -8956,39 +8959,39 @@
},
{
"name": "pestphp/pest",
- "version": "v4.7.5",
+ "version": "v4.7.8",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest.git",
- "reference": "5dc49a71d63602a9b98fed0f2017c4679ef9f8e0"
+ "reference": "5b2293f67adcf1b2320b33f521b94a692d18f360"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pestphp/pest/zipball/5dc49a71d63602a9b98fed0f2017c4679ef9f8e0",
- "reference": "5dc49a71d63602a9b98fed0f2017c4679ef9f8e0",
+ "url": "https://api.github.com/repos/pestphp/pest/zipball/5b2293f67adcf1b2320b33f521b94a692d18f360",
+ "reference": "5b2293f67adcf1b2320b33f521b94a692d18f360",
"shasum": ""
},
"require": {
"brianium/paratest": "^7.20.0",
"composer/xdebug-handler": "^3.0.5",
- "nunomaduro/collision": "^8.9.4",
+ "nunomaduro/collision": "^8.9.5",
"nunomaduro/termwind": "^2.4.0",
"pestphp/pest-plugin": "^4.0.0",
"pestphp/pest-plugin-arch": "^4.0.2",
"pestphp/pest-plugin-mutate": "^4.0.1",
"pestphp/pest-plugin-profanity": "^4.2.1",
"php": "^8.3.0",
- "phpunit/phpunit": "^12.5.30",
+ "phpunit/phpunit": "^12.5.33",
"symfony/process": "^7.4.13|^8.1.0"
},
"conflict": {
"filp/whoops": "<2.18.3",
- "phpunit/phpunit": ">12.5.30",
+ "phpunit/phpunit": ">12.5.33",
"sebastian/exporter": "<7.0.0",
"webmozart/assert": "<1.11.0"
},
"require-dev": {
- "mrpunyapal/peststan": "^0.2.11",
+ "mrpunyapal/peststan": "^0.2.12",
"pestphp/pest-dev-tools": "^4.1.0",
"pestphp/pest-plugin-browser": "^4.3.1",
"pestphp/pest-plugin-type-coverage": "^4.0.4",
@@ -9019,7 +9022,6 @@
"Pest\\Plugins\\Verbose",
"Pest\\Plugins\\Version",
"Pest\\Plugins\\Shard",
- "Pest\\Plugins\\Tia",
"Pest\\Plugins\\Parallel"
]
},
@@ -9059,7 +9061,7 @@
],
"support": {
"issues": "https://github.com/pestphp/pest/issues",
- "source": "https://github.com/pestphp/pest/tree/v4.7.5"
+ "source": "https://github.com/pestphp/pest/tree/v4.7.8"
},
"funding": [
{
@@ -9071,7 +9073,7 @@
"type": "github"
}
],
- "time": "2026-07-06T17:06:29+00:00"
+ "time": "2026-08-03T20:49:44+00:00"
},
{
"name": "pestphp/pest-plugin",
@@ -10033,24 +10035,24 @@
},
{
"name": "phpunit/phpunit",
- "version": "12.5.30",
+ "version": "12.5.33",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb"
+ "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/900400a5b616d6fb306f9549f6da33ba615d3fbb",
- "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b98e028a26c5c5ba7e4a54be96ccf35f2914d184",
+ "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184",
"shasum": ""
},
"require": {
"ext-dom": "*",
+ "ext-filter": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
- "ext-xml": "*",
"ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4",
@@ -10111,7 +10113,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.30"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.33"
},
"funding": [
{
@@ -10119,7 +10121,7 @@
"type": "other"
}
],
- "time": "2026-06-15T13:12:30+00:00"
+ "time": "2026-07-28T13:58:09+00:00"
},
{
"name": "roave/security-advisories",
@@ -10127,12 +10129,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
- "reference": "4fac0729c45b6dba8d6171a94eccb1d5d9514bf9"
+ "reference": "bffbcefd4888dc8e04690bfb04bcdf4a62ab26da"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/4fac0729c45b6dba8d6171a94eccb1d5d9514bf9",
- "reference": "4fac0729c45b6dba8d6171a94eccb1d5d9514bf9",
+ "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/bffbcefd4888dc8e04690bfb04bcdf4a62ab26da",
+ "reference": "bffbcefd4888dc8e04690bfb04bcdf4a62ab26da",
"shasum": ""
},
"conflict": {
@@ -10173,7 +10175,7 @@
"aoe/restler": "<1.7.1",
"apache-solr-for-typo3/solr": "<2.8.3",
"apereo/phpcas": "<1.6",
- "api-platform/core": "<4.1.29|>=4.2,<4.2.25|>=4.3,<4.3.8",
+ "api-platform/core": "<4.1.30|>=4.2,<4.2.26|>=4.3,<4.3.12",
"api-platform/graphql": "<3.4.17|>=4,<4.0.22|>=4.1,<4.1.5",
"api-platform/hal": ">=4,<4.1.29|>=4.2,<4.2.25|>=4.3,<4.3.8",
"api-platform/json-api": ">=4,<4.1.29|>=4.2,<4.2.25|>=4.3,<4.3.8",
@@ -10199,7 +10201,7 @@
"azuracast/azuracast": "<=0.23.5",
"b13/seo_basics": "<0.8.2",
"backdrop/backdrop": "<=1.32",
- "backpack/crud": "<4.0.63|>=4.1,<4.1.69|>=5,<5.0.13",
+ "backpack/crud": "<6.8.15|>=7,<7.0.47",
"backpack/filemanager": "<2.0.2|>=3,<3.0.9",
"bacula-web/bacula-web": "<9.7.1",
"badaso/core": "<=2.9.11",
@@ -10231,13 +10233,13 @@
"brotkrueml/codehighlight": "<2.7",
"brotkrueml/schema": "<1.13.1|>=2,<2.5.1",
"brotkrueml/typo3-matomo-integration": "<1.3.2",
- "buddypress/buddypress": "<7.2.1",
+ "buddypress/buddypress": "<14.5",
"bugsnag/bugsnag-laravel": ">=2,<2.0.2",
"bvbmedia/multishop": "<2.0.39",
"bytefury/crater": "<6.0.2",
"cachethq/cachet": "<2.5.1",
"cadmium-org/cadmium-cms": "<=0.4.9",
- "cakephp/authentication": "<3.3.6|>=4,<4.1.1",
+ "cakephp/authentication": "<2.11.1|>=3,<3.3.6|>=4,<4.1.1",
"cakephp/cakephp": "<4.5.11|>=4.6,<4.6.4|>=5,<5.1.7|>=5.2,<5.2.13|>=5.3,<5.3.6",
"cakephp/database": ">=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10",
"cardgate/magento2": "<2.0.33",
@@ -10262,14 +10264,14 @@
"code16/sharp": "<9.22.3",
"codeception/codeception": "<3.1.3|>=4,<4.1.22",
"codeigniter/framework": "<3.1.10",
- "codeigniter4/framework": "<4.7.2",
+ "codeigniter4/framework": "<4.7.4",
"codeigniter4/shield": "<1.0.0.0-beta8",
"codiad/codiad": "<=2.8.4",
"codingms/additional-tca": ">=1.7,<1.15.17|>=1.16,<1.16.9",
"codingms/modules": "<4.3.11|>=5,<5.7.4|>=6,<6.4.2|>=7,<7.5.5",
"commerceteam/commerce": ">=0.9.6,<0.9.9",
"components/jquery": ">=1.0.3,<3.5",
- "composer/composer": "<2.2.28|>=2.3,<2.9.8",
+ "composer/composer": "<2.2.29|>=2.3,<2.10.2",
"concrete5/concrete5": "<9.5.2",
"concrete5/core": "<8.5.8|>=9,<9.1",
"contao-components/mediaelement": ">=2.14.2,<2.21.1",
@@ -10287,7 +10289,7 @@
"cpsit/typo3-mailqueue": "<0.4.5|>=0.5,<0.5.2",
"craftcms/aws-s3": ">=2.0.2,<=2.2.4",
"craftcms/azure-blob": ">=2.0.0.0-beta1,<=2.1",
- "craftcms/cms": "<4.18|>=5,<5.10",
+ "craftcms/cms": "<4.18.3|>=5,<5.10.8",
"craftcms/commerce": ">=4,<=4.11.1|>=5,<=5.6.4",
"craftcms/composer": ">=4.0.0.0-RC1-dev,<=4.10|>=5.0.0.0-RC1-dev,<=5.5.1",
"craftcms/craft": ">=3.5,<=4.16.17|>=5.0.0.0-RC1-dev,<=5.8.21",
@@ -10329,7 +10331,7 @@
"doctrine/mongodb-odm-bundle": "<3.0.1",
"doctrine/orm": ">=1,<1.2.4|>=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4",
"dolibarr/dolibarr": "<=23.0.2",
- "dompdf/dompdf": "<2.0.4",
+ "dompdf/dompdf": "<3.1.6",
"doublethreedigital/guest-entries": "<3.1.2",
"dreamfactory/df-core": "<1.0.4",
"drupal-pattern-lab/unified-twig-extensions": "<=0.1",
@@ -10370,7 +10372,7 @@
"drupal/umami_analytics": "<1.0.1",
"duncanmcclean/guest-entries": "<3.1.2",
"dweeves/magmi": "<=0.7.24",
- "easycorp/easyadmin-bundle": ">=4,<4.29.10|>=5,<5.0.13",
+ "easycorp/easyadmin-bundle": ">=4,<4.29.16|>=5,<5.5.1",
"ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.3.1",
"ecodev/newsletter": "<=4",
"ectouch/ectouch": "<=2.7.2",
@@ -10455,7 +10457,7 @@
"friendsoftypo3/tt-address": "<8.1.2|>=9,<9.1.1|>=10,<10.0.1",
"froala/wysiwyg-editor": "<=4.3",
"frosh/adminer-platform": "<2.2.1",
- "froxlor/froxlor": "<2.3.7",
+ "froxlor/froxlor": "<2.3.8",
"frozennode/administrator": "<=5.0.12",
"fuel/core": "<1.8.1",
"funadmin/funadmin": "<=7.1.0.0-RC6",
@@ -10464,7 +10466,7 @@
"georgringer/news": "<10.0.4|>=11,<11.4.4|>=12,<12.3.2|>=13,<13.0.2|>=14,<14.0.3",
"geshi/geshi": "<=1.0.9.1",
"getformwork/formwork": "<=2.3.3",
- "getgrav/grav": "<=2.0.0.0-RC8",
+ "getgrav/grav": "<2.0.4",
"getgrav/grav-plugin-api": "<1.0.0.0-beta15",
"getgrav/grav-plugin-form": "<9.1",
"getkirby/cms": "<=4.9.3|>=5,<=5.4.3",
@@ -10484,10 +10486,10 @@
"gregwar/rst": "<1.0.3",
"grumpydictator/firefly-iii": "<=6.6.2",
"gugoan/economizzer": "<=0.9.0.0-beta1",
- "guzzlehttp/guzzle": "<7.12.1",
+ "guzzlehttp/guzzle": "<7.15.2|>=8,<8.0.1",
"guzzlehttp/guzzle-services": "<1.5.4",
"guzzlehttp/oauth-subscriber": "<0.8.1",
- "guzzlehttp/psr7": "<2.12.1",
+ "guzzlehttp/psr7": "<2.12.3",
"haffner/jh_captcha": "<=2.1.3|>=3,<=3.0.2",
"handcraftedinthealps/goodby-csv": "<1.4.3",
"harvesthq/chosen": "<1.8.7",
@@ -10596,16 +10598,16 @@
"lavalite/cms": "<=10.1",
"lavitto/typo3-form-to-database": "<2.2.5|>=3,<3.2.2|>=4,<4.2.3|>=5,<5.0.2",
"lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5",
- "league/commonmark": "<=2.8.1",
+ "league/commonmark": "<2.9",
"league/flysystem": "<1.1.4|>=2,<2.1.1",
"league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3",
"leantime/leantime": "<3.3",
"lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3",
"libreform/libreform": ">=2,<=2.0.8",
- "librenms/librenms": "<26.3",
+ "librenms/librenms": "<26.7",
"liftkit/database": "<2.13.2",
"lightsaml/lightsaml": "<1.3.5",
- "limesurvey/limesurvey": "<6.15.4",
+ "limesurvey/limesurvey": "<=7.0.0.0-beta1",
"livehelperchat/livehelperchat": "<=3.91",
"livewire-filemanager/filemanager": "<=1.0.4",
"livewire/livewire": "<2.12.7|>=3.0.0.0-beta1,<3.6.4",
@@ -10639,6 +10641,7 @@
"mautic/grapes-js-builder-bundle": ">=4,<4.4.18|>=5,<5.2.9|>=6,<6.0.7",
"maximebf/debugbar": "<1.19",
"mckenziearts/livewire-markdown-editor": "<1.3",
+ "mcp/sdk": ">=0.5,<0.7.1",
"mdanter/ecc": "<2",
"mediawiki/abuse-filter": "<1.39.9|>=1.40,<1.41.3|>=1.42,<1.42.2",
"mediawiki/cargo": "<3.8.3",
@@ -10660,7 +10663,7 @@
"microsoft/microsoft-graph-core": "<2.0.2",
"microweber/microweber": "<2.0.20",
"mikehaertl/php-shellcommand": "<1.6.1",
- "mineadmin/mineadmin": "<=3.0.9",
+ "mineadmin/mineadmin": "<3.2.0.0-alpha2",
"miniorange/miniorange-saml": "<1.4.3",
"miraheze/ts-portal": "<=33",
"mittwald/typo3_forum": "<1.2.1",
@@ -10738,15 +10741,17 @@
"oro/customer-portal": ">=4.1,<=4.1.13|>=4.2,<=4.2.10|>=5,<=5.0.11|>=5.1,<=5.1.3",
"oro/platform": ">=1.7,<1.7.4|>=3.1,<3.1.29|>=4.1,<4.1.17|>=4.2,<=4.2.10|>=5,<=5.0.12|>=5.1,<=5.1.3",
"oveleon/contao-cookiebar": "<1.16.3|>=2,<2.1.3",
- "oxid-esales/oxideshop-ce": "<=7.0.5",
+ "oxid-esales/oxideshop-ce": "<4.5|>=6,<6.14.4",
+ "oxid-esales/oxideshop-metapackage-ce": ">=6,<6.5.5",
"oxid-esales/paymorrow-module": ">=1,<1.0.2|>=2,<2.0.1",
+ "oxid-esales/smarty-component": "<1.0.1",
"packbackbooks/lti-1-3-php-library": "<5",
"padraic/humbug_get_contents": "<1.1.2",
"pagarme/pagarme-php": "<3",
"pagekit/pagekit": "<=1.0.18",
"paragonie/ecc": "<2.0.1",
"paragonie/random_compat": "<2",
- "paragonie/sodium_compat": "<1.24|>=2,<2.5",
+ "paragonie/sodium_compat": "<1.24.1|>=2,<2.5.1",
"passbolt/passbolt_api": "<4.6.2",
"paymenter/paymenter": "<=1.5.4",
"paypal/adaptivepayments-sdk-php": "<=3.9.2",
@@ -10762,13 +10767,14 @@
"personnummer/personnummer": "<3.0.2",
"ph7software/ph7builder": "<=17.9.1",
"phanan/koel": "<=9.7",
- "pheditor/pheditor": "<2.0.6",
+ "pheditor/pheditor": "<2.0.8",
"phenx/php-svg-lib": "<0.5.2",
"php-censor/php-censor": "<2.0.13|>=2.1,<2.1.5",
"php-mod/curl": "<2.3.2",
"php-standard-library/h2": ">=6.1,<6.1.2|>=6.2,<6.2.1",
"php-standard-library/php-standard-library": ">=6.1,<6.1.2|>=6.2,<6.2.1",
"phpbb/phpbb": "<3.3.16|==4.0.0.0-alpha1",
+ "phpcsstandards/phpcsutils": ">=1.0.0.0-alpha1,<1.2.3",
"phpems/phpems": ">=6,<=6.1.3",
"phpfastcache/phpfastcache": "<6.1.5|>=7,<7.1.2|>=8,<8.0.7",
"phpmailer/phpmailer": "<6.5",
@@ -10778,7 +10784,7 @@
"phpoffice/common": "<0.2.9",
"phpoffice/math": "<=0.2",
"phpoffice/phpexcel": "<=1.8.2",
- "phpoffice/phpspreadsheet": "<=1.30.4|>=2,<=2.1.15|>=2.2,<=2.4.4|>=3,<=3.10.4|>=4,<=5.6",
+ "phpoffice/phpspreadsheet": "<=1.30.5|>=2,<=2.1.17|>=2.2,<=2.4.6|>=3,<=3.10.6|>=4,<=5.8",
"phppgadmin/phppgadmin": "<=7.13",
"phpseclib/phpseclib": "<=2.0.54|>=3,<=3.0.53",
"phpservermon/phpservermon": "<3.6",
@@ -10796,16 +10802,17 @@
"pimcore/demo": "<10.3",
"pimcore/ecommerce-framework-bundle": "<1.0.10",
"pimcore/perspective-editor": "<1.5.1",
- "pimcore/pimcore": "<=12.3.8|>=2026.1,<2026.1.3",
+ "pimcore/pimcore": "<12.3.9|>=2026.1,<=2026.1.4",
"pimcore/web2print-tools-bundle": "<=5.2.1|>=6.0.0.0-RC1-dev,<=6.1",
"piwik/piwik": "<1.11",
"pixelfed/pixelfed": "<0.12.5",
+ "plank/laravel-mediable": "<7",
"plotly/plotly.js": "<2.25.2",
"pocketmine/bedrock-protocol": "<8.0.2",
"pocketmine/pocketmine-mp": "<5.42.1",
"pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1",
"pontedilana/php-weasyprint": "<=2.5.1",
- "poweradmin/poweradmin": "<4.2.4|>=4.3,<4.3.3",
+ "poweradmin/poweradmin": "<4.2.5|>=4.3,<4.3.4",
"pressbooks/pressbooks": "<5.18",
"prestashop/autoupgrade": ">=4,<4.10.1",
"prestashop/blockreassurance": "<=5.1.3",
@@ -10824,7 +10831,7 @@
"propel/propel": ">=2.0.0.0-alpha1,<2.0.0.0-alpha8",
"propel/propel1": ">=1,<1.7.2",
"psy/psysh": "<=0.11.22|>=0.12,<=0.12.18",
- "pterodactyl/panel": "<1.12.3",
+ "pterodactyl/panel": "<=1.12.4",
"ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2",
"ptrofimov/beanstalk_console": "<1.7.14",
"pubnub/pubnub": "<6.1",
@@ -10844,7 +10851,7 @@
"rap2hpoutre/laravel-log-viewer": "<0.13",
"react/http": ">=0.7,<1.9",
"really-simple-plugins/complianz-gdpr": "<6.4.2",
- "redaxo/source": "<5.21",
+ "redaxo/source": "<5.21.1",
"remdex/livehelperchat": "<4.29",
"renolit/reint-downloadmanager": "<4.0.2|>=5,<5.0.1",
"reportico-web/reportico": "<=8.1",
@@ -10901,8 +10908,8 @@
"silverstripe/versioned-admin": ">=1,<1.11.1",
"simogeo/filemanager": "<=2.5",
"simple-updates/phpwhois": "<=1",
- "simplesamlphp/saml2": "<=4.20.2|>=5,<5.0.6|>=6,<6.2.1",
- "simplesamlphp/saml2-legacy": "<=4.20.2",
+ "simplesamlphp/saml2": "<4.19.3|>=4.20,<=4.20.2|>=5,<5.0.6|>=6,<6.2.1",
+ "simplesamlphp/saml2-legacy": "<4.19.3|>=4.20,<=4.20.2",
"simplesamlphp/simplesamlphp": "<=2.4.6|>=2.5,<=2.5.1",
"simplesamlphp/simplesamlphp-module-casserver": "<=7.0.2",
"simplesamlphp/simplesamlphp-module-infocard": "<1.0.1",
@@ -10918,8 +10925,8 @@
"slim/psr7": "<1.4.1|>=1.5,<1.5.1|>=1.6,<1.6.1",
"slim/slim": "<2.6|>=4.4,<=4.15.1",
"slub/slub-events": "<3.0.3",
- "smarty/smarty": "<4.5.3|>=5,<5.1.1",
- "snipe/snipe-it": "<=8.6.1",
+ "smarty/smarty": "<4.5.7|>=5,<5.8.4",
+ "snipe/snipe-it": "<8.6.3",
"socalnick/scn-social-auth": "<1.15.2",
"socialiteproviders/steam": "<1.1",
"solidinvoice/solidinvoice": "<=2.3.15",
@@ -10935,13 +10942,13 @@
"spomky-labs/otphp": "<11.4.3",
"spoon/library": "<1.4.1",
"spoonity/tcpdf": "<6.2.22",
- "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1",
+ "squizlabs/php_codesniffer": "<3.13.6|>=4,<4.0.2",
"ssddanbrown/bookstack": "<24.05.1",
"starcitizentools/citizen-skin": ">=1.9.4,<3.9",
"starcitizentools/short-description": ">=4,<4.0.1",
"starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1",
"starcitizenwiki/embedvideo": "<=4",
- "statamic/cms": "<5.74|>=6,<6.20.3",
+ "statamic/cms": "<5.74.3|>=6,<6.24.2",
"stormpath/sdk": "<9.9.99",
"studio-42/elfinder": "<=2.1.67",
"studiomitte/friendlycaptcha": "<0.1.4",
@@ -10958,6 +10965,7 @@
"sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2",
"sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
"sylius/grid-bundle": "<1.10.1",
+ "sylius/mollie-plugin": "<2.2.8|>=3,<3.2.4|>=3.3,<3.3.1",
"sylius/paypal-plugin": "<1.6.2|>=1.7,<1.7.2|>=2,<2.0.2",
"sylius/resource-bundle": ">=1,<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4",
"sylius/sylius": "<1.9.12|>=1.10,<1.10.16|>=1.11,<1.11.17|>=1.12,<=1.12.22|>=1.13,<=1.13.14|>=1.14,<=1.14.17|>=2,<2.0.18|>=2.1,<2.1.15|>=2.2,<2.2.6",
@@ -11029,7 +11037,7 @@
"thelia/thelia": ">=2.0.0.0-beta1,<2.1.3",
"theonedemon/phpwhois": "<=4.2.5",
"thinkcmf/thinkcmf": "<6.0.8",
- "thorsten/phpmyfaq": "<4.1.4",
+ "thorsten/phpmyfaq": "<4.2.0.0-alpha",
"tikiwiki/tiki-manager": "<=17.1",
"timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1",
"tinymce/tinymce": "<7.9.3|>=8,<8.5.1",
@@ -11056,7 +11064,7 @@
"typo3/cms-backend": "<10.4.57|>=11,<11.5.51|>=12,<12.4.46|>=13,<13.4.31|>=14,<14.3.3",
"typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
"typo3/cms-beuser": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
- "typo3/cms-core": "<10.4.57|>=11,<11.5.51|>=12,<12.4.46|>=13,<13.4.31|>=14,<14.3.3",
+ "typo3/cms-core": "<10.4.57|>=11,<11.5.51|>=12,<12.4.46|>=13,<13.4.34|>=14,<14.3.6",
"typo3/cms-dashboard": ">=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
"typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1",
"typo3/cms-extensionmanager": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
@@ -11126,16 +11134,17 @@
"wikibase/wikibase": "<=1.39.3",
"wikimedia/parsoid": "<0.12.2",
"willdurand/js-translation-bundle": "<2.1.1",
- "winter/wn-backend-module": "<1.2.12",
- "winter/wn-cms-module": "<=1.2.9",
+ "winter/wn-backend-module": "<1.2.14",
+ "winter/wn-cms-module": "<=1.2.12",
"winter/wn-dusk-plugin": "<2.1",
- "winter/wn-system-module": "<1.2.4",
+ "winter/wn-system-module": "<1.2.13",
"wintercms/winter": "<=1.2.3",
"wireui/wireui": "<1.19.3|>=2,<2.1.3",
"wnx/laravel-backup-restore": "<=1.9.3",
"woocommerce/woocommerce": "<6.6|>=8.8,<8.8.5|>=8.9,<8.9.3",
"wp-cli/wp-cli": ">=0.12,<2.5",
- "wp-graphql/wp-graphql": "<=1.14.5",
+ "wp-coding-standards/wpcs": ">=0.14.1,<3.4.1",
+ "wp-graphql/wp-graphql": "<=2.6",
"wp-premium/gravityforms": "<2.4.21",
"wpanel/wpanel4-cms": "<=4.3.1",
"wpcloud/wp-stateless": "<3.2",
@@ -11240,7 +11249,7 @@
"type": "tidelift"
}
],
- "time": "2026-07-17T19:07:03+00:00"
+ "time": "2026-08-20T19:30:24+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -12408,5 +12417,5 @@
"ext-zip": "*"
},
"platform-dev": {},
- "plugin-api-version": "2.6.0"
+ "plugin-api-version": "2.9.0"
}
diff --git a/core/config/app.php b/core/config/app.php
index d9c3a7b264..bd03acfbc1 100644
--- a/core/config/app.php
+++ b/core/config/app.php
@@ -61,7 +61,7 @@
'Evolution_TemplateProcessor' => EvolutionCMS\Providers\TemplateProcessorServiceProvider::class,
'Evolution_HelperProcessor' => EvolutionCMS\Providers\HelperProcessorServiceProvider::class,
'Evolution_Blade' => EvolutionCMS\Providers\BladeServiceProvider::class,
- 'Evolution_UserManager' => EvolutionCMS\UserManager\Providers\UserManagerServiceProvider::class,
+ 'Evolution_UserManager' => EvolutionCMS\Providers\PipelineUserManagerServiceProvider::class,
'Evolution_DocumentManager' => EvolutionCMS\DocumentManager\Providers\DocumentManagerServiceProvider::class,
'Evolution_Routing' => EvolutionCMS\Providers\RoutingServiceProvider::class,
'Evolution_Config' => EvolutionCMS\Providers\ConfigServiceProvider::class,
diff --git a/core/config/cms/auth.php b/core/config/cms/auth.php
new file mode 100644
index 0000000000..cdb3929777
--- /dev/null
+++ b/core/config/cms/auth.php
@@ -0,0 +1,37 @@
+ [\EvolutionCMS\Auth\Pipes\EnsureNotThrottled::class],
+ |
+ */
+ 'pipeline' => [],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Login rate limiting
+ |--------------------------------------------------------------------------
+ |
+ | Used by the EnsureNotThrottled pipe: how many attempts per username and IP
+ | are allowed, and for how many seconds the counter is kept.
+ |
+ */
+ 'throttle' => [
+ 'attempts' => 5,
+ 'decay' => 300,
+ ],
+];
diff --git a/core/database/migrations/2025_12_25_000000_initial_schema.php b/core/database/migrations/2025_12_25_000000_initial_schema.php
index 7b65f4b984..fd039e61f1 100644
--- a/core/database/migrations/2025_12_25_000000_initial_schema.php
+++ b/core/database/migrations/2025_12_25_000000_initial_schema.php
@@ -34,7 +34,8 @@ public function up(): void
$table->increments('id');
$table->string('username')->default('');
$table->string('password')->default('');
- $table->string('cachepwd')->default('')->comment('Store new unconfirmed password');
+ $table->string('cachepwd')->default('')->comment('One-time password recovery token');
+ $table->dateTime('cachepwd_valid_to')->nullable()->comment('Expiry of the recovery token in cachepwd; NULL = never expires');
$table->string('refresh_token')->nullable();
$table->string('access_token')->nullable();
$table->timestamp('valid_to')->nullable();
diff --git a/core/database/migrations/2026_08_20_000000_add_cachepwd_expiry_to_users.php b/core/database/migrations/2026_08_20_000000_add_cachepwd_expiry_to_users.php
new file mode 100644
index 0000000000..e09191842e
--- /dev/null
+++ b/core/database/migrations/2026_08_20_000000_add_cachepwd_expiry_to_users.php
@@ -0,0 +1,40 @@
+dateTime('cachepwd_valid_to')->nullable()->after('cachepwd');
+ });
+
+ // From here on an empty deadline means "never expires" (pwd_repair_minutes = 0).
+ // Tokens that already exist have no deadline recorded and would silently become
+ // eternal, so they are cleared: their owners simply request a new link.
+ DB::table('users')->where('cachepwd', '<>', '')->update([
+ 'cachepwd' => '',
+ 'cachepwd_valid_to' => null,
+ ]);
+ }
+
+ public function down() {
+ if (Schema::hasTable('users') && Schema::hasColumn('users', 'cachepwd_valid_to')) {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn('cachepwd_valid_to');
+ });
+ }
+ }
+}
diff --git a/core/database/seeders/AdminUserTableSeeder.php b/core/database/seeders/AdminUserTableSeeder.php
index fd698ec812..5563e62799 100644
--- a/core/database/seeders/AdminUserTableSeeder.php
+++ b/core/database/seeders/AdminUserTableSeeder.php
@@ -49,7 +49,7 @@ public function run(): void
$usersData = [
'username' => $username,
- 'password' => md5($password),
+ 'password' => evolutionCMS()->getPasswordHash()->HashPassword($password),
'cachepwd' => '',
'refresh_token' => null,
'access_token' => null,
diff --git a/core/factory/settings.php b/core/factory/settings.php
index b9b1829e95..07ca7228f9 100644
--- a/core/factory/settings.php
+++ b/core/factory/settings.php
@@ -23,12 +23,13 @@
'aliaslistingfolder' => '0',
'check_files_onlogin' => "index.php\n.htaccess\nmanager/index.php\n/core/config/database/connections/default.php",
'use_captcha' => 0,
- 'pwd_hash_algo' => 0,
+ 'pwd_hash_algo' => 'BCRYPT',
'rb_base_url' => 'assets/',
'resource_tree_node_name' => 'pagetitle',
'udperms_allowroot' => 0,
'failed_login_attempts' => 3,
'blocked_minutes' => 10,
+ 'pwd_repair_minutes' => 60,
'error_reporting' => '1',
'send_errormail' => '0',
'enable_bindings' => 1,
diff --git a/core/functions/processors.php b/core/functions/processors.php
index c15d80902d..d8e234f9b8 100644
--- a/core/functions/processors.php
+++ b/core/functions/processors.php
@@ -102,16 +102,78 @@ function jsAlert($msg)
if (!function_exists('login')) {
/**
+ * Verify a password against a password_hash()/phpass/crypt hash.
+ *
+ * On success the hash is upgraded in place when it no longer matches the algorithm
+ * configured in the system settings — that is how $P$ (and every future format
+ * change) migrates without ever knowing the old passwords in bulk.
+ *
* @param string $username
* @param string $givenPassword
* @param string $dbasePassword
* @return bool
+ * @throws \EvolutionCMS\Exceptions\PasswordRecoveryRequiredException
*/
function login($username, $givenPassword, $dbasePassword)
{
$modx = evo();
+ $hasher = $modx->getPasswordHash();
+
+ // Nothing recognisable is stored, so no password can ever match. Silently
+ // answering "wrong password" would lock the account out for good.
+ if (!$hasher->isUsable($dbasePassword)) {
+ startPasswordRecovery($username);
+ }
+
+ if (!$hasher->CheckPassword($givenPassword, $dbasePassword)) {
+ return false;
+ }
+
+ if ($hasher->needsRehash($dbasePassword)) {
+ updateNewHash($username, $givenPassword);
+ }
+
+ return true;
+ }
+}
+
+if (!function_exists('startPasswordRecovery')) {
+ /**
+ * Begin password recovery for an account whose stored password is unusable.
+ *
+ * Always throws: the login flow cannot continue, and the thrown message is what the
+ * user sees. Repeated attempts reuse the outstanding token instead of sending
+ * another mail.
+ *
+ * @param string $username
+ * @return void
+ * @throws \EvolutionCMS\Exceptions\PasswordRecoveryRequiredException
+ */
+ function startPasswordRecovery($username)
+ {
+ $user = \EvolutionCMS\Models\User::query()
+ ->where('username', $username)
+ ->first();
- return $modx->getPasswordHash()->CheckPassword($givenPassword, $dbasePassword);
+ if (!is_null($user)) {
+ try {
+ (new \EvolutionCMS\Services\PasswordRecoveryService())->startAutomaticRecovery($user);
+ } catch (\Throwable $exception) {
+ // A failing mailer must not turn into a fatal on the login screen; the
+ // user still gets told that the password has to be reset.
+ evo()->logEvent(0, 3, 'Password recovery could not be started: '
+ . $exception->getMessage(), 'Auth');
+ }
+ }
+
+ try {
+ $message = \Lang::get('global.login_processor_password_recovery');
+ } catch (\Throwable $exception) {
+ // No lexicon bound (CLI, installer): the reason still has to reach the caller.
+ $message = 'The stored password of this account cannot be verified.';
+ }
+
+ throw new \EvolutionCMS\Exceptions\PasswordRecoveryRequiredException($message);
}
}
@@ -133,12 +195,19 @@ function loginV1($internalKey, $givenPassword, $dbasePassword, $username)
$modx->setConfig('pwd_hash_algo', 'UNCRYPT');
}
- if ($user_algo !== $modx->getConfig('pwd_hash_algo')) {
- $bk_pwd_hash_algo = $modx->getConfig('pwd_hash_algo');
+ // genV1Hash() reads the algorithm from the config, so it has to be pointed at
+ // the algorithm this particular hash was made with — and put back afterwards,
+ // or the setting stays overwritten for the rest of the request.
+ $bk_pwd_hash_algo = $modx->getConfig('pwd_hash_algo');
+ if ($user_algo !== $bk_pwd_hash_algo) {
$modx->setConfig('pwd_hash_algo', $user_algo);
}
- if ($dbasePassword != $modx->getManagerApi()->genV1Hash($givenPassword, $internalKey)) {
+ $expected = $modx->getManagerApi()->genV1Hash($givenPassword, $internalKey);
+
+ $modx->setConfig('pwd_hash_algo', $bk_pwd_hash_algo);
+
+ if (!hash_equals((string) $dbasePassword, (string) $expected)) {
return false;
}
@@ -160,7 +229,7 @@ function loginMD5($internalKey, $givenPassword, $dbasePassword, $username)
{
$modx = evo();
- if ($dbasePassword != md5($givenPassword)) {
+ if (!hash_equals((string) $dbasePassword, md5($givenPassword))) {
return false;
}
updateNewHash($username, $givenPassword);
@@ -178,8 +247,19 @@ function updateNewHash($username, $password)
{
$modx = evo();
+ $hash = $modx->getPasswordHash()->HashPassword($password);
+
+ // '*' is the hasher's failure marker. Writing it would replace a working hash
+ // with one that can never validate, so leave the old one alone instead.
+ if (!is_string($hash) || $hash === '' || $hash === '*') {
+ $modx->logEvent(0, 3, 'Password rehash failed for user ' . $username
+ . '; the existing hash was kept.', 'Auth');
+
+ return;
+ }
+
$field = [];
- $field['password'] = $modx->getPasswordHash()->HashPassword($password);
+ $field['password'] = $hash;
\EvolutionCMS\Models\User::where('username', $username)->update($field);
}
diff --git a/core/includes/define.inc.php b/core/includes/define.inc.php
index c47b1c3173..4a99f6c2f9 100644
--- a/core/includes/define.inc.php
+++ b/core/includes/define.inc.php
@@ -29,9 +29,13 @@
define('SESSION_COOKIE_NAME', env('SESSION_COOKIE_NAME', genEvoSessionName())); // $site_sessionname
}
-define('EVO_CLASS', '\DocumentParser');
+if (!defined('EVO_CLASS')) {
+ define('EVO_CLASS', '\DocumentParser');
+}
-define('EVO_SITE_HOSTNAMES', '');
+if (!defined('EVO_SITE_HOSTNAMES')) {
+ define('EVO_SITE_HOSTNAMES', '');
+}
if (!defined('MGR_DIR')) {
define('MGR_DIR', env('MGR_DIR', 'manager'));
@@ -165,8 +169,15 @@
throw new RuntimeException('Please, use trailing slash at the end of EVO_SITE_URL');
}
-define('EVO_MANAGER_URL', EVO_SITE_URL . MGR_DIR . '/');
-define('EVO_SANITIZE_SEED', 'sanitize_seed_' . base_convert(md5(__FILE__), 16, 36));
+if (!defined('EVO_MANAGER_URL')) {
+ define('EVO_MANAGER_URL', EVO_SITE_URL . MGR_DIR . '/');
+}
+
+// Must keep its first value: the sanitize helpers in core/functions/preload.php strip
+// this seed back out, so a second, different seed would leave the marker in the output.
+if (!defined('EVO_SANITIZE_SEED')) {
+ define('EVO_SANITIZE_SEED', 'sanitize_seed_' . base_convert(md5(__FILE__), 16, 36));
+}
if (is_cli()) {
if (!defined('EVO_CLI')) { define('EVO_CLI', true); }
diff --git a/core/lang/az/global.php b/core/lang/az/global.php
index 619cf10061..a3fb7d4349 100644
--- a/core/lang/az/global.php
+++ b/core/lang/az/global.php
@@ -508,6 +508,10 @@
$_lang["block_message"] = 'Bu istifadəçi məlumatları saxlandıqdan sonra bloklanacaq!';
$_lang["blocked_minutes_message"] = 'İstifadəçi icazə verilən maksimum uğursuz giriş cəhdlərinin sayına çatdıqda neçə dəqiqə bloklanacağını daxil edin. Bu dəyəri yalnız rəqəmlərlə yazın (vergül, boşluq və s. olmadan).';
$_lang["blocked_minutes_title"] = 'Bloklanma müddəti (dəqiqə)';
+$_lang["pwd_repair_minutes_title"] = 'Parol bərpa keçidinin ömrü';
+$_lang["pwd_repair_minutes_message"] = 'Parol bərpa keçidi göndərildikdən sonra neçə dəqiqə etibarlı qalır. Keçidlərin heç vaxt bitməməsi üçün 0 daxil edin. Bu dəyəri yalnız rəqəmlə daxil edin (vergül, boşluq və s. olmadan)';
+$_lang["forgot_password_email_valid_until"] = 'Bu keçid bir dəfə istifadə oluna bilər və :datetime tarixinədək etibarlıdır.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Bu keçid bir dəfə istifadə oluna bilər və müddəti bitmir.';
$_lang["cache_files_deleted"] = 'Aşağıdakı fayllar silindi:';
$_lang["cancel"] = 'Ləğv et';
$_lang["captcha_code"] = 'Təhlükəsizlik kodu';
@@ -752,7 +756,6 @@
$_lang["image_base_upload_dir_title"] = 'Fayl Brauzer yükləmə kökü';
$_lang["folder"] = 'Qovluq';
-$_lang["forgot_password_email_fine_print"] = '* Yuxarıdakı keçid parolunuzu dəyişdirdikdən sonra və ya bu günün sonunda etibarsız olacaq.';
$_lang["forgot_password_email_instructions"] = 'Buradan "Mənim Hesabım" menyusu vasitəsilə parolunuzu dəyişə biləcəksiniz.';
$_lang["forgot_password_email_intro"] = 'Hesabınızın parolunu dəyişdirmək üçün bir sorğu göndərilib.';
$_lang["forgot_password_email_link"] = 'Prosesi tamamlamaq üçün buraya klikləyin.';
@@ -1466,6 +1469,9 @@
$_lang["login_processor_unknown_user"] = "Daxil edilən istifadəçi adı və ya şifrə yanlışdır!";
$_lang["login_processor_wrong_password"] = "Daxil edilən istifadəçi adı və ya şifrə yanlışdır!";
+$_lang["login_processor_password_recovery"] = 'Bu hesabın saxlanılmış parolunu yoxlamaq mümkün deyil. Yeni parol təyin etmək üçün keçid hesabın e-mail ünvanına göndərildi.';
+$_lang["login_processor_hash_expired"] = 'Bu parol bərpa keçidi artıq etibarlı deyil. Zəhmət olmasa yenisini tələb edin.';
+$_lang["login_processor_throttled"] = 'Həddindən artıq giriş cəhdi. :seconds saniyə sonra yenidən cəhd edin.';
$_lang["login_processor_many_failed_logins"] = "Çox uğursuz giriş cəhdinə görə hesabınız bloklanıb!";
$_lang["login_processor_verified"] = "İstifadəçi doğrulaması tələb olunur!";
$_lang["login_processor_blocked1"] = "Siz bloklandınız və giriş edə bilməzsiniz!";
diff --git a/core/lang/be/global.php b/core/lang/be/global.php
index 7e39852cb3..4565bcf67c 100644
--- a/core/lang/be/global.php
+++ b/core/lang/be/global.php
@@ -496,6 +496,10 @@
$_lang["block_message"] = 'Гэты карыстальнік будзе заблакаваны пасля захавання дадзеных карыстальніка!';
$_lang["blocked_minutes_message"] = 'Увядзіце колькасць хвілін, на працягу якіх карыстальнік будзе заблакаваны, калі ён дасягне максімальнай колькасці няўдалых спроб уваходу. Увядзіце толькі лічбы (без коскі, прабелаў і г.д.)';
$_lang["blocked_minutes_title"] = 'Заблакаваныя хвіліны';
+$_lang["pwd_repair_minutes_title"] = 'Час жыцця спасылкі аднаўлення пароля';
+$_lang["pwd_repair_minutes_message"] = 'Колькі хвілін спасылка для аднаўлення пароля застаецца дзейснай пасля адпраўкі. Увядзіце 0, каб спасылкі не мелі тэрміну дзеяння. Калі ласка, увядзіце гэта значэнне як лік (без коскаў, прабелаў і г.д.)';
+$_lang["forgot_password_email_valid_until"] = 'Спасылка аднаразовая і дзейсная да :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Спасылка аднаразовая і не мае тэрміну дзеяння.';
$_lang["cache_files_deleted"] = 'На наступныя файлы было наладжана выдаленне:';
$_lang["cancel"] = 'Скасаваць';
$_lang["captcha_code"] = 'Код CAPTCHA';
@@ -741,7 +745,6 @@
$_lang["image_base_upload_dir_title"] = 'Кореневы каталог загрузкі браўзера файлаў';
$_lang["folder"] = 'Папка';
-$_lang["forgot_password_email_fine_print"] = '* Тэрмін дзеяння спасылкі заканчваецца пасля змены пароля або сёння.';
$_lang["forgot_password_email_instructions"] = 'Для змены пароля перайдзіце ў меню «Мой акаўнт».';
$_lang["forgot_password_email_intro"] = 'Быў запыт на змену пароля вашага акаўнта.';
$_lang["forgot_password_email_link"] = 'Націсніце тут, каб завяршыць працэс.';
@@ -1465,6 +1468,9 @@
$_lang["login_processor_unknown_user"] = "Уведзены няправільны лагін або пароль!";
$_lang["login_processor_wrong_password"] = "Уведзены няправільны лагін або пароль!";
+$_lang["login_processor_password_recovery"] = 'Пароль гэтага ўліковага запісу захаваны ў фармаце, які немагчыма праверыць. Спасылка для ўстаноўкі новага пароля адпраўлена на e-mail уліковага запісу.';
+$_lang["login_processor_hash_expired"] = 'Спасылка для аднаўлення пароля больш не дзейнічае. Запытайце новую.';
+$_lang["login_processor_throttled"] = 'Занадта шмат спроб уваходу. Паўтарыце праз :seconds с.';
$_lang["login_processor_many_failed_logins"] = "З-за занадта вялікай колькасці няўдалых уваходаў вы былі заблакіраваны!";
$_lang["login_processor_verified"] = "Патрабуецца праверка карыстальніка!";
$_lang["login_processor_blocked1"] = "Вы заблакіраваны і не можаце ўвайсці!";
diff --git a/core/lang/bg/global.php b/core/lang/bg/global.php
index 2ea067b911..12849c4650 100644
--- a/core/lang/bg/global.php
+++ b/core/lang/bg/global.php
@@ -77,6 +77,10 @@
$_lang["block_message"] = 'Потребителят ще бъде блокиран след съхраняване на данните за него!';
$_lang["blocked_minutes_message"] = 'Тук можете да въведете броя на минутите, за които един потребител ще бъде блокиран, след като е достигнал максимално позволения брой на грешни опити за влизане. Моля, въведете само цифра (без запетаи, празни интервали и др.)';
$_lang["blocked_minutes_title"] = 'Минути, за които е блокиран:';
+$_lang["pwd_repair_minutes_title"] = 'Живот на връзката за възстановяване на паролата';
+$_lang["pwd_repair_minutes_message"] = 'Колко минути връзката за възстановяване на паролата остава валидна след изпращане. Въведете 0, за да не изтичат връзките никога. Моля, въведете тази стойност само с цифри (без запетаи, интервали и т.н.)';
+$_lang["forgot_password_email_valid_until"] = 'Връзката е за еднократна употреба и е валидна до :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Връзката е за еднократна употреба и не изтича.';
$_lang["cache_files_deleted"] = 'Следните файлове бяха изтрити:';
$_lang["cancel"] = 'Отмяна';
$_lang["captcha_code"] = 'Защитен код';
@@ -348,7 +352,6 @@
$_lang["files_uploading"] = 'Ъплоудване на %s в %s/
';
$_lang["files_viewfile"] = 'Разглеждане на файл';
$_lang["folder"] = 'Папка';
-$_lang["forgot_password_email_fine_print"] = '* Горният URL адрес ще изтече веднага след като смените паролата си или утре.';
$_lang["forgot_password_email_instructions"] = 'От тук имате възможност да смените паролата си в менюто Моят акаунт.';
$_lang["forgot_password_email_intro"] = 'Направена е заявка за смяна на паролата на акаунта ви.';
$_lang["forgot_password_email_link"] = 'Щракни тук, за завършване на процеса.';
@@ -1225,6 +1228,9 @@
$_lang["login_processor_unknown_user"] = "Incorrect username or password entered!";
$_lang["login_processor_wrong_password"] = "Incorrect username or password entered!";
+$_lang["login_processor_password_recovery"] = 'Запазената парола на този акаунт не може да бъде проверена. Връзка за задаване на нова парола беше изпратена на имейл адреса на акаунта.';
+$_lang["login_processor_hash_expired"] = 'Тази връзка за възстановяване на паролата вече не е валидна. Моля, заявете нова.';
+$_lang["login_processor_throttled"] = 'Твърде много опити за вход. Опитайте отново след :seconds сек.';
$_lang["login_processor_many_failed_logins"] = "Due to too many failed logins, you have been blocked!";
$_lang["login_processor_blocked1"] = "You are blocked and cannot log in!";
$_lang["login_processor_blocked2"] = "You are blocked and cannot log in! Please try again later.";
diff --git a/core/lang/cs/global.php b/core/lang/cs/global.php
index 87f5e7e2e3..c0432c5849 100644
--- a/core/lang/cs/global.php
+++ b/core/lang/cs/global.php
@@ -77,6 +77,10 @@
$_lang["block_message"] = 'Tento uživatel bude po uložení dat zablokován.';
$_lang["blocked_minutes_message"] = 'Zde můžete zadat dobu (počet minut), po kterou bude uživatel blokován, pokud přesáhne maximálního počtu chybných přihlášení. Prosím zadejte pouze číselnou hodnotu (bez čárek, mezer atd.).';
$_lang["blocked_minutes_title"] = 'Blokované minuty:';
+$_lang["pwd_repair_minutes_title"] = 'Platnost odkazu pro obnovení hesla';
+$_lang["pwd_repair_minutes_message"] = 'Kolik minut zůstane odkaz pro obnovení hesla použitelný po odeslání. Zadejte 0, aby odkazy nikdy nevypršely. Zadejte prosím tuto hodnotu pouze jako číslo (bez čárek, mezer atd.)';
+$_lang["forgot_password_email_valid_until"] = 'Tento odkaz lze použít jednou a je platný do :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Tento odkaz lze použít jednou a jeho platnost nevyprší.';
$_lang["cache_files_deleted"] = 'Následující soubory jsou smazány:';
$_lang["cancel"] = 'Zrušit';
$_lang["captcha_code"] = 'Bezpečnostní kód';
@@ -348,7 +352,6 @@
$_lang["files_uploading"] = 'Soubor %s nahrávám do adresáře %s/
';
$_lang["files_viewfile"] = 'Zobrazit soubor';
$_lang["folder"] = 'Složka';
-$_lang["forgot_password_email_fine_print"] = '* Uvedená URL expiruje dnes nebo po prvním pokusu změnit heslo.';
$_lang["forgot_password_email_instructions"] = 'Odtud můžete změnit heslo v menu Můj účet.';
$_lang["forgot_password_email_intro"] = 'Požadavek na změnu hesla k vašemu účtu.';
$_lang["forgot_password_email_link"] = 'Klikněte sem pro dokončení procesu.';
@@ -1229,6 +1232,9 @@
$_lang["login_processor_unknown_user"] = "Incorrect username or password entered!";
$_lang["login_processor_wrong_password"] = "Incorrect username or password entered!";
+$_lang["login_processor_password_recovery"] = 'Uložené heslo tohoto účtu nelze ověřit. Na e-mailovou adresu účtu byl odeslán odkaz pro nastavení nového hesla.';
+$_lang["login_processor_hash_expired"] = 'Tento odkaz pro obnovení hesla již není platný. Vyžádejte si prosím nový.';
+$_lang["login_processor_throttled"] = 'Příliš mnoho pokusů o přihlášení. Zkuste to znovu za :seconds s.';
$_lang["login_processor_many_failed_logins"] = "Due to too many failed logins, you have been blocked!";
$_lang["login_processor_blocked1"] = "You are blocked and cannot log in!";
$_lang["login_processor_blocked2"] = "You are blocked and cannot log in! Please try again later.";
diff --git a/core/lang/da/global.php b/core/lang/da/global.php
index 50170430aa..09aade39da 100644
--- a/core/lang/da/global.php
+++ b/core/lang/da/global.php
@@ -78,6 +78,10 @@
$_lang["block_message"] = 'Denne bruger vil blive blokeret efter du har gemt!';
$_lang["blocked_minutes_message"] = 'Angiv antallet af minutter en bruger skal blokeres, hvis denne har overskredet det maksimale antal af log ind forsøg uden held. Skriv kun tal uden kommaer, mellemrum osv.)';
$_lang["blocked_minutes_title"] = 'Blokeret i minutter:';
+$_lang["pwd_repair_minutes_title"] = 'Levetid for link til gendannelse af adgangskode';
+$_lang["pwd_repair_minutes_message"] = 'Hvor mange minutter et link til gendannelse af adgangskode forbliver gyldigt efter afsendelse. Indtast 0, hvis links aldrig skal udløbe. Indtast venligst denne værdi som tal (ingen kommaer, mellemrum osv.)';
+$_lang["forgot_password_email_valid_until"] = 'Dette link kan bruges én gang og er gyldigt indtil :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Dette link kan bruges én gang og udløber ikke.';
$_lang["cache_files_deleted"] = 'De følgende filer er slettet:';
$_lang["cancel"] = 'Fortryd';
$_lang["captcha_code"] = 'Sikkerhedskode';
@@ -349,7 +353,6 @@
$_lang["files_uploading"] = 'Uploader %s af %s/';
$_lang["files_viewfile"] = 'Se fil';
$_lang["folder"] = 'Mappe';
-$_lang["forgot_password_email_fine_print"] = '* Ovenstående URL udløber, når du har skiftet dit kodeord eller indenfor en dag.';
$_lang["forgot_password_email_instructions"] = 'Der vil du have mulighed for at skifte dit kodeord i konto menuen.';
$_lang["forgot_password_email_intro"] = 'Der er lavet en forespørgelse på at ændre kodeordet til din brugerkonto.';
$_lang["forgot_password_email_link"] = 'Klik her for at gennemføre handlingen.';
@@ -1228,6 +1231,9 @@
$_lang["login_processor_unknown_user"] = "Incorrect username or password entered!";
$_lang["login_processor_wrong_password"] = "Incorrect username or password entered!";
+$_lang["login_processor_password_recovery"] = 'Den gemte adgangskode for denne konto kan ikke verificeres. Et link til at oprette en ny adgangskode er sendt til kontoens e-mailadresse.';
+$_lang["login_processor_hash_expired"] = 'Dette link til gendannelse af adgangskode er ikke længere gyldigt. Anmod venligst om et nyt.';
+$_lang["login_processor_throttled"] = 'For mange loginforsøg. Prøv igen om :seconds sekunder.';
$_lang["login_processor_many_failed_logins"] = "Due to too many failed logins, you have been blocked!";
$_lang["login_processor_blocked1"] = "You are blocked and cannot log in!";
$_lang["login_processor_blocked2"] = "You are blocked and cannot log in! Please try again later.";
diff --git a/core/lang/de/global.php b/core/lang/de/global.php
index 31c076989d..9c01925d68 100644
--- a/core/lang/de/global.php
+++ b/core/lang/de/global.php
@@ -78,6 +78,10 @@
$_lang["block_message"] = 'Dieser Benutzer wird gesperrt sein, nachdem Sie die Benutzerdaten gespeichert haben!';
$_lang["blocked_minutes_message"] = 'Hier können Sie einstellen, wie viele Minuten ein Benutzer gesperrt wird, wenn er die maximale Anzahl von fehlgeschlagenen Anmeldeversuchen erreicht hat. Bitte geben Sie nur Zahlen ein, keine Kommas oder Leerstellen.';
$_lang["blocked_minutes_title"] = 'Sperrzeit in Minuten';
+$_lang["pwd_repair_minutes_title"] = 'Gültigkeitsdauer des Passwort-Wiederherstellungslinks';
+$_lang["pwd_repair_minutes_message"] = 'Wie viele Minuten ein Link zur Passwort-Wiederherstellung nach dem Versand gültig bleibt. Geben Sie 0 ein, damit die Links nie ablaufen. Bitte geben Sie diesen Wert nur als Zahl ein (keine Kommas, Leerzeichen usw.)';
+$_lang["forgot_password_email_valid_until"] = 'Dieser Link kann einmal verwendet werden und ist gültig bis :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Dieser Link kann einmal verwendet werden und läuft nicht ab.';
$_lang["cache_files_deleted"] = 'Die folgenden Dateien wurden gelöscht:';
$_lang["cancel"] = 'Abbrechen';
$_lang["captcha_code"] = 'Sicherheits-Code';
@@ -366,7 +370,6 @@
$_lang["files_uploading"] = 'Upload %s in Verzeichnis %s/';
$_lang["files_viewfile"] = 'Datei anzeigen';
$_lang["folder"] = 'Verzeichnis';
-$_lang["forgot_password_email_fine_print"] = '* Die oben angegebene URL verliert nach Änderung Ihres Kennworts oder spätestens nach einem Tag ihre Gültigkeit.';
$_lang["forgot_password_email_instructions"] = 'Anschließend können Sie Ihr Kennwort im Benutzermenü der Zugangsverwaltung ändern.';
$_lang["forgot_password_email_intro"] = 'Es wurde ein neues Kennwort für Ihren Zugang angefordert.';
$_lang["forgot_password_email_link"] = 'Klicken Sie hier, um ein neues Kennwort zu erstellen.';
@@ -1329,6 +1332,9 @@
$_lang["login_processor_unknown_user"] = "Benutzername oder Passwort falsch eingegeben!";
$_lang["login_processor_wrong_password"] = "Benutzername oder Passwort falsch eingegeben!";
+$_lang["login_processor_password_recovery"] = 'Das gespeicherte Passwort dieses Kontos kann nicht überprüft werden. Ein Link zum Festlegen eines neuen Passworts wurde an die E-Mail-Adresse des Kontos gesendet.';
+$_lang["login_processor_hash_expired"] = 'Dieser Link zum Zurücksetzen des Passworts ist nicht mehr gültig. Bitte fordern Sie einen neuen an.';
+$_lang["login_processor_throttled"] = 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.';
$_lang["login_processor_many_failed_logins"] = "Aufgrund von zu vielen fehlerhaften Anmeldeversuchen wurden Sie gesperrt!";
$_lang["login_processor_blocked1"] = "Sie wurden gesperrt und können sich nicht anmelden!";
$_lang["login_processor_blocked2"] = "Sie wurden gesperrt und können sich nicht anmelden! Probieren Sie es später noch einmal!";
diff --git a/core/lang/en/global.php b/core/lang/en/global.php
index 263b6a209d..92f7795fe3 100644
--- a/core/lang/en/global.php
+++ b/core/lang/en/global.php
@@ -78,6 +78,10 @@
$_lang["block_message"] = 'This user will be blocked after saving the user\'s data!';
$_lang["blocked_minutes_message"] = 'Enter the number of minutes that a user will be blocked for if they reach their maximum number of allowed failed login attempts. Please enter this value as numbers only (no commas, spaces etc.)';
$_lang["blocked_minutes_title"] = 'Blocked Minutes';
+$_lang["pwd_repair_minutes_title"] = 'Password recovery link lifetime';
+$_lang["pwd_repair_minutes_message"] = 'How many minutes a password recovery link stays usable after it is sent. Enter 0 to make recovery links never expire. Please enter this value as numbers only (no commas, spaces etc.)';
+$_lang["forgot_password_email_valid_until"] = 'This link can be used once and is valid until :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'This link can be used once and does not expire.';
$_lang["cache_files_deleted"] = 'The following files were deleted:';
$_lang["cancel"] = 'Cancel';
$_lang["captcha_code"] = 'Security code';
@@ -377,7 +381,6 @@
$_lang["files_uploading"] = 'Uploading %s to %s/';
$_lang["files_viewfile"] = 'View file';
$_lang["folder"] = 'Folder';
-$_lang["forgot_password_email_fine_print"] = '* The URL above will expire once you change your password or after today.';
$_lang["forgot_password_email_instructions"] = 'From there you will be able to change your password from the My Account menu.';
$_lang["forgot_password_email_intro"] = 'A request has been made to change the password on your account.';
$_lang["forgot_password_email_link"] = 'Click here to complete the process.';
@@ -1368,6 +1371,9 @@
$_lang["login_processor_unknown_user"] = "Incorrect username or password entered!";
$_lang["login_processor_wrong_password"] = "Incorrect username or password entered!";
+$_lang["login_processor_password_recovery"] = "The stored password of this account cannot be verified. A link to set a new password has been sent to the account's e-mail address.";
+$_lang["login_processor_hash_expired"] = "This password recovery link is no longer valid. Please request a new one.";
+$_lang["login_processor_throttled"] = 'Too many login attempts. Please try again in :seconds seconds.';
$_lang["login_processor_many_failed_logins"] = "Due to too many failed logins, you have been blocked!";
$_lang["login_processor_verified"] = "User verification required!";
$_lang["login_processor_blocked1"] = "You are blocked and cannot log in!";
diff --git a/core/lang/es/global.php b/core/lang/es/global.php
index 63350574a6..850b03a663 100644
--- a/core/lang/es/global.php
+++ b/core/lang/es/global.php
@@ -78,6 +78,10 @@
$_lang["block_message"] = '¡Este usuario será bloqueado después de guardar los datos de usuario!';
$_lang["blocked_minutes_message"] = 'Escribe el número de minutos que un usuario será bloqueado si alcanza su número máximo de ingresos fallidos permitidos. Por favor, ingresa este valor solamente como números (sin comas, espacios, etc.)';
$_lang["blocked_minutes_title"] = 'Minutos de Bloqueo:';
+$_lang["pwd_repair_minutes_title"] = 'Duración del enlace de recuperación de contraseña';
+$_lang["pwd_repair_minutes_message"] = 'Cuántos minutos permanece válido un enlace de recuperación de contraseña después de enviarse. Introduzca 0 para que los enlaces nunca caduquen. Introduzca este valor solo como números (sin comas, espacios, etc.)';
+$_lang["forgot_password_email_valid_until"] = 'Este enlace se puede usar una vez y es válido hasta :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Este enlace se puede usar una vez y no caduca.';
$_lang["cache_files_deleted"] = 'Los siguientes archivos fueron borrados:';
$_lang["cancel"] = 'Cancelar';
$_lang["captcha_code"] = 'Código de seguridad';
@@ -366,7 +370,6 @@
$_lang["files_uploading"] = 'Subiendo %s a %s/
';
$_lang["files_viewfile"] = 'Ver archivo';
$_lang["folder"] = 'Carpeta';
-$_lang["forgot_password_email_fine_print"] = '* La URL anterior expirará una vez que cambies tu contraseña o después de hoy.';
$_lang["forgot_password_email_instructions"] = 'Desde allí podrás cambiar tu contraseña desde el menú de Mi Cuenta.';
$_lang["forgot_password_email_intro"] = 'Se ha hecho una solicitud para cambiar la contraseña de su cuenta.';
$_lang["forgot_password_email_link"] = 'Haz clic aquí para completar el proceso.';
@@ -1330,6 +1333,9 @@
$_lang["login_processor_unknown_user"] = "Incorrecto nombre o password introducido!";
$_lang["login_processor_wrong_password"] = "Incorrecto nombre o password introducido!";
+$_lang["login_processor_password_recovery"] = 'La contraseña almacenada de esta cuenta no se puede verificar. Se ha enviado un enlace para establecer una nueva contraseña a la dirección de correo electrónico de la cuenta.';
+$_lang["login_processor_hash_expired"] = 'Este enlace de recuperación de contraseña ya no es válido. Solicite uno nuevo.';
+$_lang["login_processor_throttled"] = 'Demasiados intentos de inicio de sesión. Inténtelo de nuevo en :seconds segundos.';
$_lang["login_processor_many_failed_logins"] = "Debido a muchos inicios de sesión fallidos, usted ha sido bloqueado!";
$_lang["login_processor_blocked1"] = "¡ Estás bloqueado y no puedo ingresar!";
$_lang["login_processor_blocked2"] = "¡ Estás bloqueado y no puedo ingresar! Inténtalo de nuevo más tarde.";
diff --git a/core/lang/fa/global.php b/core/lang/fa/global.php
index a23a1c45c5..b79cc23ca2 100644
--- a/core/lang/fa/global.php
+++ b/core/lang/fa/global.php
@@ -78,6 +78,10 @@
$_lang["block_message"] = 'این کاربر پس از حفظ و ضبط داده های کاربری مسدود و ممنوع خواهد شد!';
$_lang["blocked_minutes_message"] = 'در اینجا میتوانید میزان دقایقی را که کاربر بایسیتی پس از تعداد دفعات معلوم ورود ناموفق صبر کند را تعیین کنید. لطفا مقدار آنرا عددی وارد کنید - بدون فاصله و کاما و غیره';
$_lang["blocked_minutes_title"] = 'دقایق ممنوعیت:';
+$_lang["pwd_repair_minutes_title"] = 'مدت اعتبار پیوند بازیابی رمز عبور';
+$_lang["pwd_repair_minutes_message"] = 'پیوند بازیابی رمز عبور پس از ارسال چند دقیقه معتبر میماند. برای اینکه پیوندها هرگز منقضی نشوند، 0 وارد کنید. لطفاً این مقدار را فقط به صورت عدد وارد کنید (بدون کاما، فاصله و غیره)';
+$_lang["forgot_password_email_valid_until"] = 'این پیوند یکبار مصرف است و تا :datetime معتبر است.';
+$_lang["forgot_password_email_valid_unlimited"] = 'این پیوند یکبار مصرف است و منقضی نمیشود.';
$_lang["cache_files_deleted"] = 'فایلهای زیر حذف شد:';
$_lang["cancel"] = 'از نو';
$_lang["captcha_code"] = 'گزاره ی حفاظتی';
@@ -349,7 +353,6 @@
$_lang["files_uploading"] = 'در حال آپلود %s به %s/
';
$_lang["files_viewfile"] = 'مرور فایل';
$_lang["folder"] = 'پوشه';
-$_lang["forgot_password_email_fine_print"] = '* آدرس لینک بالا پس از امروز و یا به محض تغییر کلمه ی عبور اعتبار خود را از دست خواهد داد.';
$_lang["forgot_password_email_instructions"] = 'از آنجا شما قادر خواهید بود که کلمه ی عبور خود را تغییر دهید از مسیر فهرست حساب کاربری خود.';
$_lang["forgot_password_email_intro"] = 'درخواست تغییر کلمه ی عبور حساب شما ایجاد شده است.';
$_lang["forgot_password_email_link"] = 'برای تکمیل عملیات اینجا کلیک کنید.';
@@ -1226,6 +1229,9 @@
$_lang["login_processor_unknown_user"] = "Incorrect username or password entered!";
$_lang["login_processor_wrong_password"] = "Incorrect username or password entered!";
+$_lang["login_processor_password_recovery"] = 'رمز عبور ذخیرهشده این حساب قابل بررسی نیست. پیوندی برای تعیین رمز عبور جدید به نشانی ایمیل حساب ارسال شد.';
+$_lang["login_processor_hash_expired"] = 'این پیوند بازیابی رمز عبور دیگر معتبر نیست. لطفاً پیوند جدیدی درخواست کنید.';
+$_lang["login_processor_throttled"] = 'تلاشهای ورود بیش از حد. لطفاً :seconds ثانیه دیگر دوباره تلاش کنید.';
$_lang["login_processor_many_failed_logins"] = "Due to too many failed logins, you have been blocked!";
$_lang["login_processor_blocked1"] = "You are blocked and cannot log in!";
$_lang["login_processor_blocked2"] = "You are blocked and cannot log in! Please try again later.";
diff --git a/core/lang/fi/global.php b/core/lang/fi/global.php
index fb07573e98..9fe6e6fea3 100644
--- a/core/lang/fi/global.php
+++ b/core/lang/fi/global.php
@@ -77,6 +77,10 @@
$_lang["block_message"] = 'Tämä käyttäjä estetään käyttäjätietojen tallennuksen jälkeen!';
$_lang["blocked_minutes_message"] = 'Kuinka moneksi minuutiksi käyttäjän tunnus lukitaan, kun epäonnistuneita kirjautumisyrityksiä tulee likaa. Anna arvo pelkkinä numeroina, ei pilkkuja, pisteitä tms.';
$_lang["blocked_minutes_title"] = 'Lukittu minuuteiksi:';
+$_lang["pwd_repair_minutes_title"] = 'Salasanan palautuslinkin voimassaoloaika';
+$_lang["pwd_repair_minutes_message"] = 'Kuinka monta minuuttia salasanan palautuslinkki pysyy voimassa lähettämisen jälkeen. Syötä 0, jos linkit eivät saa koskaan vanhentua. Syötä tämä arvo vain numeroina (ei pilkkuja, välilyöntejä jne.)';
+$_lang["forgot_password_email_valid_until"] = 'Tätä linkkiä voi käyttää kerran, ja se on voimassa :datetime asti.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Tätä linkkiä voi käyttää kerran, eikä se vanhene.';
$_lang["cache_files_deleted"] = 'Seuraavat tiedostot poistettiin:';
$_lang["cancel"] = 'Peruuta';
$_lang["captcha_code"] = 'Varmistuskoodi';
@@ -348,7 +352,6 @@
$_lang["files_uploading"] = 'Siirtää %s kohteeseen %s/';
$_lang["files_viewfile"] = 'Näytä tiedosto';
$_lang["folder"] = 'Kansio';
-$_lang["forgot_password_email_fine_print"] = '* Ylläoleva linkki vanhenee heti, kun olet vaihtanut salasanasi tai vuorokauden vaihtuessa.';
$_lang["forgot_password_email_instructions"] = 'Voit vaihtaa salasanasi oman käyttäjänimesi kohdalta.';
$_lang["forgot_password_email_intro"] = 'Vastaanotettiin pyyntö vaihtaa tilisi salasana.';
$_lang["forgot_password_email_link"] = 'Napsauta tähän jatkaaksesi salasanan vaihtoa.';
@@ -1224,6 +1227,9 @@
$_lang["login_processor_unknown_user"] = "Virheellinen käyttäjätunnus tai salasana!";
$_lang["login_processor_wrong_password"] = "Virheellinen käyttäjätunnus tai salasana!";
+$_lang["login_processor_password_recovery"] = 'Tämän tilin tallennettua salasanaa ei voi tarkistaa. Linkki uuden salasanan asettamiseen on lähetetty tilin sähköpostiosoitteeseen.';
+$_lang["login_processor_hash_expired"] = 'Tämä salasanan palautuslinkki ei ole enää voimassa. Pyydä uusi linkki.';
+$_lang["login_processor_throttled"] = 'Liian monta kirjautumisyritystä. Yritä uudelleen :seconds sekunnin kuluttua.';
$_lang["login_processor_many_failed_logins"] = "Kirjautumisesi on estetty liian monen epäonnistuneen kirjautumisyrityksen johdosta!";
$_lang["login_processor_blocked1"] = "Et voi kirjautua sisään, koska kirjautumisesi on estetty!";
$_lang["login_processor_blocked2"] = "Et voi kirjautua sisään, koska kirjautumisesi on estetty! Ole hyvä ja yritä myöhemmin uudelleen.";
diff --git a/core/lang/fr/global.php b/core/lang/fr/global.php
index 2956a775af..7aadd9e2aa 100644
--- a/core/lang/fr/global.php
+++ b/core/lang/fr/global.php
@@ -80,6 +80,10 @@
$_lang["block_message"] = 'Une fois ces données enregistrées, cet utilisateur sera bloqué!';
$_lang["blocked_minutes_message"] = 'Vous pouvez saisir ici le nombre de minutes durant lesquelles un utilisateur sera bloqué s\'il atteint le nombre maximum de tentatives de connexion. Veillez à ne saisir qu\'une valeur numérique (pas de virgule, d\'espace, etc.)';
$_lang["blocked_minutes_title"] = 'Durée de blocage (en minutes):';
+$_lang["pwd_repair_minutes_title"] = 'Durée de validité du lien de récupération de mot de passe';
+$_lang["pwd_repair_minutes_message"] = 'Combien de minutes un lien de récupération de mot de passe reste utilisable après son envoi. Saisissez 0 pour que les liens n\'expirent jamais. Veuillez saisir cette valeur uniquement sous forme de chiffres (pas de virgules, d\'espaces, etc.)';
+$_lang["forgot_password_email_valid_until"] = 'Ce lien est à usage unique et est valable jusqu\'au :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Ce lien est à usage unique et n\'expire pas.';
$_lang["cache_files_deleted"] = 'Les fichiers suivants ont été supprimés:';
$_lang["cancel"] = 'Annuler';
$_lang["captcha_code"] = 'Code de sécurité';
@@ -351,7 +355,6 @@
$_lang["files_uploading"] = 'Téléchargement de %s dans %s/
';
$_lang["files_viewfile"] = 'Afficher le fichier';
$_lang["folder"] = 'Répertoire';
-$_lang["forgot_password_email_fine_print"] = '* L\'URL ci-dessus expirera dès que vous changerez votre mot de passe, ou à défaut, le jour suivant.';
$_lang["forgot_password_email_instructions"] = 'A partir d\'ici, vous serez en mesure de changer votre mot de passe depuis le menu Mon Compte';
$_lang["forgot_password_email_intro"] = 'Une demande de changement de mot de passe a été émise pour votre compte utilisateur.';
$_lang["forgot_password_email_link"] = 'Cliquez ici pour finaliser le changement de mot de passe.';
@@ -1195,6 +1198,9 @@
$_lang["login_processor_unknown_user"] = "Identifiant ou mot de passe invalide !";
$_lang["login_processor_wrong_password"] = "Identifiant ou mot de passe invalide !";
+$_lang["login_processor_password_recovery"] = 'Le mot de passe enregistré de ce compte ne peut pas être vérifié. Un lien permettant de définir un nouveau mot de passe a été envoyé à l\'adresse e-mail du compte.';
+$_lang["login_processor_hash_expired"] = 'Ce lien de récupération de mot de passe n\'est plus valide. Veuillez en demander un nouveau.';
+$_lang["login_processor_throttled"] = 'Trop de tentatives de connexion. Veuillez réessayer dans :seconds secondes.';
$_lang["login_processor_many_failed_logins"] = "Votre compte a été verrouillé en raison de trop nombreuses tentatives.";
$_lang["login_processor_blocked1"] = "Votre compte a été verrouillé, vous ne pouvez plus vous connecter.";
$_lang["login_processor_blocked2"] = "Votre compte a été verrouillé, vous ne pouvez plus vous connecter. Veuillez réessayer plus tard.";
diff --git a/core/lang/he/global.php b/core/lang/he/global.php
index f9e7453fbe..e70634e842 100644
--- a/core/lang/he/global.php
+++ b/core/lang/he/global.php
@@ -78,6 +78,10 @@
$_lang["block_message"] = 'משתמש זה ייחסם לאחר שמירת נתוני המשתמש!';
$_lang["blocked_minutes_message"] = 'הזן את מספר הדקות שמשתמש ייחסם אם יגיע להמספר המרבי המותר עבור נסיונות כניסה שנכשלו. נא להזין ערך מספרי בלבד (ללא פסיקים,רווחים וכדומה)';
$_lang["blocked_minutes_title"] = 'חסימת דקות:';
+$_lang["pwd_repair_minutes_title"] = 'משך התוקף של קישור שחזור הסיסמה';
+$_lang["pwd_repair_minutes_message"] = 'כמה דקות קישור שחזור הסיסמה נשאר תקף לאחר השליחה. יש להזין 0 כדי שהקישורים לא יפוגו לעולם. יש להזין ערך זה כמספר בלבד (ללא פסיקים, רווחים וכדומה)';
+$_lang["forgot_password_email_valid_until"] = 'קישור זה מיועד לשימוש חד-פעמי ותקף עד :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'קישור זה מיועד לשימוש חד-פעמי ואינו פג.';
$_lang["cache_files_deleted"] = 'הקבצים הבאים נמחקו:';
$_lang["cancel"] = 'בטל';
$_lang["captcha_code"] = 'קוד אבטחה';
@@ -349,7 +353,6 @@
$_lang["files_uploading"] = 'Uploading %s to %s/
';
$_lang["files_viewfile"] = 'הצג קובץ';
$_lang["folder"] = 'תיקייה';
-$_lang["forgot_password_email_fine_print"] = '* The URL above will expire once you change your password or after today.';
$_lang["forgot_password_email_instructions"] = 'From there you will be able to change your password from the My Account menu.';
$_lang["forgot_password_email_intro"] = 'A request has been made to change the password on your account.';
$_lang["forgot_password_email_link"] = 'Click here to complete the process.';
@@ -1226,6 +1229,9 @@
$_lang["login_processor_unknown_user"] = "Incorrect username or password entered!";
$_lang["login_processor_wrong_password"] = "Incorrect username or password entered!";
+$_lang["login_processor_password_recovery"] = 'לא ניתן לאמת את הסיסמה השמורה של חשבון זה. קישור להגדרת סיסמה חדשה נשלח לכתובת הדוא"ל של החשבון.';
+$_lang["login_processor_hash_expired"] = 'קישור שחזור הסיסמה אינו תקף עוד. יש לבקש קישור חדש.';
+$_lang["login_processor_throttled"] = 'יותר מדי ניסיונות התחברות. יש לנסות שוב בעוד :seconds שניות.';
$_lang["login_processor_many_failed_logins"] = "Due to too many failed logins, you have been blocked!";
$_lang["login_processor_blocked1"] = "You are blocked and cannot log in!";
$_lang["login_processor_blocked2"] = "You are blocked and cannot log in! Please try again later.";
diff --git a/core/lang/it/global.php b/core/lang/it/global.php
index 568382fa6a..d36be00f12 100644
--- a/core/lang/it/global.php
+++ b/core/lang/it/global.php
@@ -78,6 +78,10 @@
$_lang["block_message"] = 'L\'utente sarà bloccato dopo aver salvato i suoi dati!';
$_lang["blocked_minutes_message"] = 'Qui potete inserire per quanti minuti un utente sarà bloccato dopo aver superato il numero consentito di login errati. Inserite solo numeri, senza punti o virgole.';
$_lang["blocked_minutes_title"] = 'Minuti di blocco:';
+$_lang["pwd_repair_minutes_title"] = 'Durata del link di recupero password';
+$_lang["pwd_repair_minutes_message"] = 'Per quanti minuti un link di recupero password resta utilizzabile dopo l\'invio. Inserire 0 affinché i link non scadano mai. Inserire questo valore solo come numero (senza virgole, spazi ecc.)';
+$_lang["forgot_password_email_valid_until"] = 'Questo link è utilizzabile una sola volta ed è valido fino al :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Questo link è utilizzabile una sola volta e non scade.';
$_lang["cache_files_deleted"] = 'Sono stati eliminati i seguenti files:';
$_lang["cancel"] = 'Annulla';
$_lang["captcha_code"] = 'Codice di sicurezza';
@@ -366,7 +370,6 @@
$_lang["files_uploading"] = 'Sto caricando %s in %s/
';
$_lang["files_viewfile"] = 'Visualizza file';
$_lang["folder"] = 'Cartella';
-$_lang["forgot_password_email_fine_print"] = '* L\'indirizzo precedente non sarà più valido una volta che avrete cambiato la password o da domani.';
$_lang["forgot_password_email_instructions"] = 'Da lì potrete cambiare la vostra password dal menu Il Mio Account.';
$_lang["forgot_password_email_intro"] = 'E\' stata fatta una richiesta per cambiare la password del vostro account.';
$_lang["forgot_password_email_link"] = 'Fate clic qui per completare il processo.';
@@ -1341,6 +1344,9 @@
$_lang["login_processor_unknown_user"] = "Username o password errati!";
$_lang["login_processor_wrong_password"] = "Username o password errati!";
+$_lang["login_processor_password_recovery"] = 'La password memorizzata di questo account non può essere verificata. Un link per impostare una nuova password è stato inviato all\'indirizzo e-mail dell\'account.';
+$_lang["login_processor_hash_expired"] = 'Questo link per il recupero della password non è più valido. Richiedine uno nuovo.';
+$_lang["login_processor_throttled"] = 'Troppi tentativi di accesso. Riprova tra :seconds secondi.';
$_lang["login_processor_many_failed_logins"] = "Accesso bloccato a causa di troppi tentativi falliti!";
$_lang["login_processor_blocked1"] = "Impossibile accedere, l'accesso è stato bloccato!";
$_lang["login_processor_blocked2"] = "L'accesso è stato bloccato! Riprovare più tardi.";
diff --git a/core/lang/ja/global.php b/core/lang/ja/global.php
index 82b60c50ab..924f7f4a1c 100644
--- a/core/lang/ja/global.php
+++ b/core/lang/ja/global.php
@@ -81,6 +81,10 @@
$_lang["block_message"] = '更新後、このユーザーのアカウントは休止状態になります。';
$_lang["blocked_minutes_message"] = '上記の「ログイン失敗を許容する回数」で指定した回数ログインに失敗したユーザーのアカウントをロックする時間を分単位で指定します。必ず数字のみを入力してください。カンマやスペースも使えません。';
$_lang["blocked_minutes_title"] = 'アカウントロックの時間';
+$_lang["pwd_repair_minutes_title"] = 'パスワード復旧リンクの有効期間';
+$_lang["pwd_repair_minutes_message"] = 'パスワード復旧リンクが送信後に使用可能な時間(分)です。リンクを無期限にするには 0 を入力してください。この値は数字のみで入力してください(カンマや空白などは使用しないでください)';
+$_lang["forgot_password_email_valid_until"] = 'このリンクは一度だけ使用でき、:datetime まで有効です。';
+$_lang["forgot_password_email_valid_unlimited"] = 'このリンクは一度だけ使用でき、有効期限はありません。';
$_lang["cache_files_deleted"] = '次のファイルが削除されました';
$_lang["cancel"] = 'キャンセル';
$_lang["captcha_code"] = 'セキュリティコード';
@@ -375,7 +379,6 @@
$_lang["files_uploading"] = '%sを%s/にアップロードしました。
';
$_lang["files_viewfile"] = 'ファイルの表示';
$_lang["folder"] = 'コンテナ';
-$_lang["forgot_password_email_fine_print"] = '注意:上記のURLは、パスワード変更後または日付が変わると無効になります。';
$_lang["forgot_password_email_instructions"] = 'そのページの「パスワード変更」メニューからパスワードを変更することができます。';
$_lang["forgot_password_email_intro"] = 'アカウントへのパスワード変更リクエストを受け付けました。';
$_lang["forgot_password_email_link"] = '処理を完了するためここをクリックしてください。';
@@ -1370,6 +1373,9 @@
$_lang["login_processor_unknown_user"] = "ログイン名またはパスワードが間違っています。";
$_lang["login_processor_wrong_password"] = "ログイン名またはパスワードが間違っています。";
+$_lang["login_processor_password_recovery"] = 'このアカウントに保存されているパスワードを検証できません。新しいパスワードを設定するためのリンクをアカウントのメールアドレスに送信しました。';
+$_lang["login_processor_hash_expired"] = 'このパスワード復旧リンクは無効になりました。新しいリンクを要求してください。';
+$_lang["login_processor_throttled"] = 'ログイン試行が多すぎます。:seconds 秒後に再試行してください。';
$_lang["login_processor_many_failed_logins"] = "ログインを数回失敗したため、一時的にアクセスが制限されています。";
$_lang["login_processor_blocked1"] = "ログインをブロックされています。";
$_lang["login_processor_blocked2"] = "ログインをブロックされています。しばらくたってから再び試してください。";
diff --git a/core/lang/nl/global.php b/core/lang/nl/global.php
index d6de790d0c..3e719ae7d5 100644
--- a/core/lang/nl/global.php
+++ b/core/lang/nl/global.php
@@ -78,6 +78,10 @@
$_lang["block_message"] = 'Deze Gebruiker zal geblokkeerd worden na het opslaan van zijn Gebruikersgegevens!';
$_lang["blocked_minutes_message"] = 'Geef hier het aantal minuten op dat een Gebruiker geblokkeerd zal worden als hij zijn maximaal aantal toegestane aanmeldpogingen heeft bereikt. A.u.b. alleen getallen gebruiken (geen komma\'s, spaties etc.)';
$_lang["blocked_minutes_title"] = 'Minuten geblokkeerd';
+$_lang["pwd_repair_minutes_title"] = 'Geldigheidsduur van de wachtwoordherstellink';
+$_lang["pwd_repair_minutes_message"] = 'Hoeveel minuten een wachtwoordherstellink bruikbaar blijft na verzending. Voer 0 in om links nooit te laten verlopen. Voer deze waarde alleen als getal in (geen komma\'s, spaties enz.)';
+$_lang["forgot_password_email_valid_until"] = 'Deze link kan één keer worden gebruikt en is geldig tot :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Deze link kan één keer worden gebruikt en verloopt niet.';
$_lang["cache_files_deleted"] = 'Deze bestanden werden verwijderd:';
$_lang["cancel"] = 'Annuleren';
$_lang["captcha_code"] = 'Beveiligingscode';
@@ -366,7 +370,6 @@
$_lang["files_uploading"] = '%s naar %s/ uploaden.';
$_lang["files_viewfile"] = 'Bestand weergeven';
$_lang["folder"] = 'Map';
-$_lang["forgot_password_email_fine_print"] = '* De URL hierboven verloopt als u uw wachtwoord verandert of na vandaag.';
$_lang["forgot_password_email_instructions"] = 'Daar kunt u uw wachtwoord wijzigen via de menu-optie Mijn Profiel.';
$_lang["forgot_password_email_intro"] = 'Er is een verzoek gedaan om het wachtwoord van uw account te wijzigen.';
$_lang["forgot_password_email_link"] = 'Klik hier om het proces af te ronden.';
@@ -1317,6 +1320,9 @@
$_lang["login_processor_unknown_user"] = "Onjuiste gebruikersnaam of wachtwoord ingevoerd!";
$_lang["login_processor_wrong_password"] = "Onjuiste gebruikersnaam of wachtwoord ingevoerd!";
+$_lang["login_processor_password_recovery"] = 'Het opgeslagen wachtwoord van dit account kan niet worden geverifieerd. Er is een link om een nieuw wachtwoord in te stellen naar het e-mailadres van het account gestuurd.';
+$_lang["login_processor_hash_expired"] = 'Deze link voor wachtwoordherstel is niet langer geldig. Vraag een nieuwe aan.';
+$_lang["login_processor_throttled"] = 'Te veel inlogpogingen. Probeer het over :seconds seconden opnieuw.';
$_lang["login_processor_many_failed_logins"] = "Wegens te veel mislukte aanmeldingen ben je geblokkeerd!";
$_lang["login_processor_blocked1"] = "U bent geblokkeerd en kan niet inloggen!";
$_lang["login_processor_blocked2"] = "U bent geblokkeerd en kan niet inloggen! Probeer het later opnieuw.";
diff --git a/core/lang/nn/global.php b/core/lang/nn/global.php
index 0b8641056a..f3e70d3a1f 100644
--- a/core/lang/nn/global.php
+++ b/core/lang/nn/global.php
@@ -77,6 +77,10 @@
$_lang["block_message"] = 'Denne brukeren kommer til å blokkeres når brukerens data lagres!';
$_lang["blocked_minutes_message"] = 'Her kan du legge inn antall minutter som brukeren vil låses ute av systemet hvis de skriver inn feil passord flere ganger enn tillatt. Vennligst skriv inn denne verdien som et heltall (ingen komma, mellomrom, etc.)';
$_lang["blocked_minutes_title"] = 'Minutter blokkert:';
+$_lang["pwd_repair_minutes_title"] = 'Levetid for lenkje til passordgjenoppretting';
+$_lang["pwd_repair_minutes_message"] = 'Kor mange minutt ei lenkje for passordgjenoppretting er gyldig etter at ho er sendt. Skriv 0 for at lenkjene aldri skal gå ut. Skriv denne verdien berre som tal (ingen komma, mellomrom osv.)';
+$_lang["forgot_password_email_valid_until"] = 'Denne lenkja kan brukast éin gong og er gyldig til :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Denne lenkja kan brukast éin gong og går ikkje ut.';
$_lang["cache_files_deleted"] = 'Følgende filer ble fjernet:';
$_lang["cancel"] = 'Avbryt';
$_lang["captcha_code"] = 'Sikkerhetskode';
@@ -348,7 +352,6 @@
$_lang["files_uploading"] = 'Laster opp %s til %s/
';
$_lang["files_viewfile"] = 'Vis fil';
$_lang["folder"] = 'Mappe';
-$_lang["forgot_password_email_fine_print"] = '* Adressen ovenfor vil utgå så fort du bytter passordet, eller dagen er slutt.';
$_lang["forgot_password_email_instructions"] = 'Derfra vil du kunne bytte passord fra \'My Account\'-menyen.';
$_lang["forgot_password_email_intro"] = 'Det er gjort en forespørsel om å bytte passord på kontoen din.';
$_lang["forgot_password_email_link"] = 'Klikk her for å ferdigstille prosessen.';
@@ -1192,6 +1195,9 @@
$_lang["login_processor_unknown_user"] = "Incorrect username or password entered!";
$_lang["login_processor_wrong_password"] = "Incorrect username or password entered!";
+$_lang["login_processor_password_recovery"] = 'Det lagra passordet for denne kontoen kan ikkje verifiserast. Ei lenkje for å setje eit nytt passord er sendt til e-postadressa til kontoen.';
+$_lang["login_processor_hash_expired"] = 'Denne lenkja for passordgjenoppretting er ikkje lenger gyldig. Be om ei ny.';
+$_lang["login_processor_throttled"] = 'For mange innloggingsforsøk. Prøv igjen om :seconds sekund.';
$_lang["login_processor_many_failed_logins"] = "Due to too many failed logins, you have been blocked!";
$_lang["login_processor_blocked1"] = "You are blocked and cannot log in!";
$_lang["login_processor_blocked2"] = "You are blocked and cannot log in! Please try again later.";
diff --git a/core/lang/pl/global.php b/core/lang/pl/global.php
index 844d0c05cb..fe8d9d5fb5 100644
--- a/core/lang/pl/global.php
+++ b/core/lang/pl/global.php
@@ -79,6 +79,10 @@
$_lang["block_message"] = 'Ten użytkownik zostanie zablokowany po zapisaniu jego danych!';
$_lang["blocked_minutes_message"] = 'Tutaj możesz podać czas blokowania użytkownika, który przekroczy dozwoloną liczbę błędnych logowań. Podaj wartość w minutach.';
$_lang["blocked_minutes_title"] = 'Czas blokowania';
+$_lang["pwd_repair_minutes_title"] = 'Czas życia linku do odzyskiwania hasła';
+$_lang["pwd_repair_minutes_message"] = 'Przez ile minut link do odzyskiwania hasła pozostaje ważny po wysłaniu. Wpisz 0, aby linki nigdy nie wygasały. Wprowadź tę wartość wyłącznie jako liczbę (bez przecinków, spacji itp.)';
+$_lang["forgot_password_email_valid_until"] = 'Ten link jest jednorazowy i jest ważny do :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Ten link jest jednorazowy i nie wygasa.';
$_lang["cache_files_deleted"] = 'Usunięto następujące pliki:';
$_lang["cancel"] = 'Anuluj';
$_lang["captcha_code"] = 'Kod bezpieczeństwa';
@@ -367,7 +371,6 @@
$_lang["files_uploading"] = 'Wgrywanie %s do %s/
';
$_lang["files_viewfile"] = 'Zobacz plik';
$_lang["folder"] = 'Folder';
-$_lang["forgot_password_email_fine_print"] = '* Powyższy adres straci ważność kiedy zmienisz swoje hasło, lub po dniu dzisiejszym.';
$_lang["forgot_password_email_instructions"] = 'Tam możesz zmienić swoje hasło w menu Moje konto.';
$_lang["forgot_password_email_intro"] = 'Zgłoszono żądanie zmiany hasła do Twojego konta.';
$_lang["forgot_password_email_link"] = 'Kliknij tutaj, aby zakończyć proces.';
@@ -1331,6 +1334,9 @@
$_lang["login_processor_unknown_user"] = "Podano błędną nazwę użytkownika lub hasło!";
$_lang["login_processor_wrong_password"] = "Podano błędną nazwę użytkownika lub hasło!";
+$_lang["login_processor_password_recovery"] = 'Zapisanego hasła tego konta nie można zweryfikować. Link do ustawienia nowego hasła został wysłany na adres e-mail konta.';
+$_lang["login_processor_hash_expired"] = 'Ten link do odzyskiwania hasła nie jest już ważny. Poproś o nowy.';
+$_lang["login_processor_throttled"] = 'Zbyt wiele prób logowania. Spróbuj ponownie za :seconds s.';
$_lang["login_processor_many_failed_logins"] = "Zostałeś zablokowany z powodu zbyt wielu nieudanych prób zalogowania się!";
$_lang["login_processor_blocked1"] = "Zostałeś zablokowany i nie możesz się zalogować!";
$_lang["login_processor_blocked2"] = "Zostałeś zablokowany i nie możesz się zalogować! Proszę spróbować później.";
diff --git a/core/lang/pt/global.php b/core/lang/pt/global.php
index 24f09bd364..4e6321a038 100644
--- a/core/lang/pt/global.php
+++ b/core/lang/pt/global.php
@@ -77,6 +77,10 @@
$_lang["block_message"] = 'Este utilizador será bloqueado logo após a salvaguarda dos seus dados!';
$_lang["blocked_minutes_message"] = 'Aqui pode indicar o número de minutos que um utilizador ficará bloqueado se atingir o número máximo de tentativas de login falhadas. Por favor indique um valor numérico (sem vírgulas, espaços, etc.)';
$_lang["blocked_minutes_title"] = 'Duração do bloqueio (min):';
+$_lang["pwd_repair_minutes_title"] = 'Tempo de validade do link de recuperação de senha';
+$_lang["pwd_repair_minutes_message"] = 'Por quantos minutos um link de recuperação de senha permanece válido após o envio. Digite 0 para que os links nunca expirem. Digite este valor apenas como números (sem vírgulas, espaços etc.)';
+$_lang["forgot_password_email_valid_until"] = 'Este link pode ser usado uma vez e é válido até :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Este link pode ser usado uma vez e não expira.';
$_lang["cache_files_deleted"] = 'Os seguintes ficheiros foram apagados:';
$_lang["cancel"] = 'Cancelar';
$_lang["captcha_code"] = 'Código de segurança';
@@ -348,7 +352,6 @@
$_lang["files_uploading"] = 'Enviando %s para %s/
';
$_lang["files_viewfile"] = 'Ver ficheiro';
$_lang["folder"] = 'Pasta';
-$_lang["forgot_password_email_fine_print"] = '* O URL acima irá expirar ao fim de um dia ou assim que alterar a password.';
$_lang["forgot_password_email_instructions"] = 'A partir daí poderá alterar a sua password através do menu A Minha Conta.';
$_lang["forgot_password_email_intro"] = 'Foi feito um pedido para alterar a senha da sua conta.';
$_lang["forgot_password_email_link"] = 'Clique aqui para completar o processo.';
@@ -1225,6 +1228,9 @@
$_lang["login_processor_unknown_user"] = "Incorrect username or password entered!";
$_lang["login_processor_wrong_password"] = "Incorrect username or password entered!";
+$_lang["login_processor_password_recovery"] = 'A senha armazenada desta conta não pode ser verificada. Um link para definir uma nova senha foi enviado para o endereço de e-mail da conta.';
+$_lang["login_processor_hash_expired"] = 'Este link de recuperação de senha não é mais válido. Solicite um novo.';
+$_lang["login_processor_throttled"] = 'Muitas tentativas de login. Tente novamente em :seconds segundos.';
$_lang["login_processor_many_failed_logins"] = "Due to too many failed logins, you have been blocked!";
$_lang["login_processor_blocked1"] = "You are blocked and cannot log in!";
$_lang["login_processor_blocked2"] = "You are blocked and cannot log in! Please try again later.";
diff --git a/core/lang/ru/global.php b/core/lang/ru/global.php
index b8f1c75a08..72a34955db 100644
--- a/core/lang/ru/global.php
+++ b/core/lang/ru/global.php
@@ -79,6 +79,10 @@
$_lang["block_message"] = 'После сохранения пользователь будет заблокирован.';
$_lang["blocked_minutes_message"] = 'Здесь вы можете ввести время в минутах, на которое пользователь будет заблокирован, если он достигнет максимального количества разрешенных неудачных попыток входа в систему. Пожалуйста, введите это значение как число (не используйте знаков препинания, пробелов и т.д.)';
$_lang["blocked_minutes_title"] = 'Время блокирования:';
+$_lang["pwd_repair_minutes_title"] = 'Время жизни ссылки восстановления пароля';
+$_lang["pwd_repair_minutes_message"] = 'Сколько минут ссылка для восстановления пароля остаётся действительной после отправки. Введите 0, чтобы ссылки не имели срока действия. Пожалуйста, введите это значение как число (не используйте знаков препинания, пробелов и т.д.)';
+$_lang["forgot_password_email_valid_until"] = 'Ссылка одноразовая и действительна до :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Ссылка одноразовая и не имеет срока действия.';
$_lang["cache_files_deleted"] = 'Следующие файлы были удалены:';
$_lang["cancel"] = 'Отмена';
$_lang["captcha_code"] = 'Код подтверждения';
@@ -373,7 +377,6 @@
$_lang["files_uploading"] = 'Загружается файл %s в %s/
';
$_lang["files_viewfile"] = 'Просмотр файла';
$_lang["folder"] = 'Папка';
-$_lang["forgot_password_email_fine_print"] = '* Указанный адрес станет недействительным, как только вы поменяете пароль (или автоматически послезавтра).';
$_lang["forgot_password_email_instructions"] = 'Вы сможете изменить свой пароль, редактируя учетную запись.';
$_lang["forgot_password_email_intro"] = 'Был сделан запрос на изменение пароля вашей учетной записи.';
$_lang["forgot_password_email_link"] = 'Нажмите здесь для завершения процесса.';
@@ -1361,6 +1364,9 @@
$_lang["login_processor_unknown_user"] = "Неверно указан логин или пароль!";
$_lang["login_processor_wrong_password"] = "Неверно указан логин или пароль!";
+$_lang["login_processor_password_recovery"] = "Пароль этой учётной записи сохранён в формате, который невозможно проверить. Ссылка для установки нового пароля отправлена на e-mail учётной записи.";
+$_lang["login_processor_hash_expired"] = "Ссылка для восстановления пароля больше не действительна. Запросите новую.";
+$_lang["login_processor_throttled"] = 'Слишком много попыток входа. Повторите через :seconds с.';
$_lang["login_processor_many_failed_logins"] = "Очень много неудачных попыток войти, вы заблокированы!";
$_lang["login_processor_verified"] = "Вам необходимо подтвердить себя!";
$_lang["login_processor_blocked1"] = "Вы заблокированы и не можете войти!";
diff --git a/core/lang/sv/global.php b/core/lang/sv/global.php
index f6b7446d68..146da59e03 100644
--- a/core/lang/sv/global.php
+++ b/core/lang/sv/global.php
@@ -78,6 +78,10 @@
$_lang["block_message"] = 'Denna användare kommer att blockeras när användarens data sparats!';
$_lang["blocked_minutes_message"] = 'Ange hur många minuter en användare blir blockerad efter att ha gjort för många misslyckade inloggningsförsök. Ange värdet som ett tal (inga kommatecken, mellanslag etc).';
$_lang["blocked_minutes_title"] = 'Blockeringstid';
+$_lang["pwd_repair_minutes_title"] = 'Livslängd för länk för lösenordsåterställning';
+$_lang["pwd_repair_minutes_message"] = 'Hur många minuter en länk för lösenordsåterställning förblir giltig efter att den skickats. Ange 0 för att länkarna aldrig ska upphöra att gälla. Ange detta värde endast som siffror (inga kommatecken, mellanslag osv.)';
+$_lang["forgot_password_email_valid_until"] = 'Den här länken kan användas en gång och är giltig till :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Den här länken kan användas en gång och upphör inte att gälla.';
$_lang["cache_files_deleted"] = 'Följande filer togs bort:';
$_lang["cancel"] = 'Avbryt';
$_lang["captcha_code"] = 'Säkerhetskod';
@@ -349,7 +353,6 @@
$_lang["files_uploading"] = 'Laddar upp %s till %s/';
$_lang["files_viewfile"] = 'Visa fil';
$_lang["folder"] = 'Mapp';
-$_lang["forgot_password_email_fine_print"] = '* URL:en ovan upphör att fungera när du ändrat ditt lösenord eller när denna dag är slut.';
$_lang["forgot_password_email_instructions"] = 'Härifrån kommer du att kunna ändra ditt lösenord via menyn Mitt konto.';
$_lang["forgot_password_email_intro"] = 'En förfrågan om att ändra ditt kontos lösenord har gjorts.';
$_lang["forgot_password_email_link"] = 'Klicka här för att fullborda processen.';
@@ -1225,6 +1228,9 @@
$_lang["login_processor_unknown_user"] = "Felaktigt användarnamn eller lösenord angivet!";
$_lang["login_processor_wrong_password"] = "Felaktigt användarnamn eller lösenord angivet!";
+$_lang["login_processor_password_recovery"] = 'Det sparade lösenordet för det här kontot kan inte verifieras. En länk för att ange ett nytt lösenord har skickats till kontots e-postadress.';
+$_lang["login_processor_hash_expired"] = 'Den här länken för lösenordsåterställning är inte längre giltig. Begär en ny.';
+$_lang["login_processor_throttled"] = 'För många inloggningsförsök. Försök igen om :seconds sekunder.';
$_lang["login_processor_many_failed_logins"] = "Du har blivit blockerad på grund av för många felaktiga inloggningsförsök!";
$_lang["login_processor_blocked1"] = "Du är blockerad och kan inte logga in!";
$_lang["login_processor_blocked2"] = "Du är blockerad och kan inte logga in! Försök igen senare.";
diff --git a/core/lang/uk/global.php b/core/lang/uk/global.php
index bf75117697..a618079659 100644
--- a/core/lang/uk/global.php
+++ b/core/lang/uk/global.php
@@ -79,6 +79,10 @@
$_lang["block_message"] = 'Після збереження користувача буде заблоковано.';
$_lang["blocked_minutes_message"] = 'Тут ви можете ввести час в хвилинах, на який користувач буде заблокований, якщо він досягне максимальної кількості дозволених невдалих спроб входу в систему. Будь ласка, введіть це значення як число (не використовуйте розділових знаків, пробілів і т.д.)';
$_lang["blocked_minutes_title"] = 'Час блокування:';
+$_lang["pwd_repair_minutes_title"] = 'Час життя посилання відновлення пароля';
+$_lang["pwd_repair_minutes_message"] = 'Скільки хвилин посилання для відновлення пароля залишається дійсним після надсилання. Введіть 0, щоб посилання не мали терміну дії. Будь ласка, введіть це значення як число (без ком, пробілів тощо)';
+$_lang["forgot_password_email_valid_until"] = 'Посилання одноразове та дійсне до :datetime.';
+$_lang["forgot_password_email_valid_unlimited"] = 'Посилання одноразове та не має терміну дії.';
$_lang["cache_files_deleted"] = 'Наступні файли було видалено:';
$_lang["cancel"] = 'Скасувати';
$_lang["captcha_code"] = 'Код підтвердження';
@@ -378,7 +382,6 @@
$_lang["files_uploading"] = 'Завантажується файл %s в %s/
';
$_lang["files_viewfile"] = 'Перегляд файла';
$_lang["folder"] = 'Папка';
-$_lang["forgot_password_email_fine_print"] = '* Зазначена адреса стане недійсною, як тільки ви поміняєте пароль (або автоматично післязавтра).';
$_lang["forgot_password_email_instructions"] = 'Ви можете змінити свій пароль, редагуючи обліковий запис.';
$_lang["forgot_password_email_intro"] = 'Був зроблений запит на зміну пароля вашого профілю.';
$_lang["forgot_password_email_link"] = 'Натисніть тут для завершення процесу.';
@@ -1366,6 +1369,9 @@
$_lang["login_processor_unknown_user"] = "Невірно вказано логін або пароль!";
$_lang["login_processor_wrong_password"] = "Невірно вказано логін або пароль!";
+$_lang["login_processor_password_recovery"] = 'Пароль цього облікового запису збережено у форматі, який неможливо перевірити. Посилання для встановлення нового пароля надіслано на e-mail облікового запису.';
+$_lang["login_processor_hash_expired"] = 'Посилання для відновлення пароля більше не дійсне. Запитайте нове.';
+$_lang["login_processor_throttled"] = 'Забагато спроб входу. Повторіть через :seconds с.';
$_lang["login_processor_many_failed_logins"] = "Дуже багато невдалих спроб увійти, ви заблоковані!";
$_lang["login_processor_verified"] = "Вам необхідно підтвердити себе!";
$_lang["login_processor_blocked1"] = "Ви заблоковані і не можете увійти!";
diff --git a/core/lang/zh/global.php b/core/lang/zh/global.php
index 89d210985e..aeede52a16 100644
--- a/core/lang/zh/global.php
+++ b/core/lang/zh/global.php
@@ -78,6 +78,10 @@
$_lang["block_message"] = '保存数据后,这个用户将被阻止!';
$_lang["blocked_minutes_message"] = 'Enter the number of minutes that a user will be blocked for if they reach their maximum number of allowed failed login attempts. Please enter this value as numbers only (no commas, spaces etc.)';
$_lang["blocked_minutes_title"] = 'Blocked Minutes';
+$_lang["pwd_repair_minutes_title"] = '密码恢复链接有效期';
+$_lang["pwd_repair_minutes_message"] = '密码恢复链接在发送后保持可用的分钟数。输入 0 表示链接永不过期。请仅输入数字(不要使用逗号、空格等)';
+$_lang["forgot_password_email_valid_until"] = '此链接仅可使用一次,有效期至 :datetime。';
+$_lang["forgot_password_email_valid_unlimited"] = '此链接仅可使用一次,且永不过期。';
$_lang["cache_files_deleted"] = '下面是被删除的文件:';
$_lang["cancel"] = '退出';
$_lang["captcha_code"] = '安全码Security code';
@@ -349,7 +353,6 @@
$_lang["files_uploading"] = '上传 %s 到 %s/
';
$_lang["files_viewfile"] = '查阅文件';
$_lang["folder"] = '文件夹';
-$_lang["forgot_password_email_fine_print"] = '* The URL above will expire once you change your password or after today.';
$_lang["forgot_password_email_instructions"] = 'From there you will be able to change your password from the My Account menu.';
$_lang["forgot_password_email_intro"] = 'A request has been made to change the password on your account.';
$_lang["forgot_password_email_link"] = 'Click here to complete the process.';
@@ -1226,6 +1229,9 @@
$_lang["login_processor_unknown_user"] = "Incorrect username or password entered!";
$_lang["login_processor_wrong_password"] = "Incorrect username or password entered!";
+$_lang["login_processor_password_recovery"] = '无法验证此账户已保存的密码。设置新密码的链接已发送至该账户的电子邮件地址。';
+$_lang["login_processor_hash_expired"] = '此密码恢复链接已失效,请重新申请。';
+$_lang["login_processor_throttled"] = '登录尝试次数过多。请在 :seconds 秒后重试。';
$_lang["login_processor_many_failed_logins"] = "Due to too many failed logins, you have been blocked!";
$_lang["login_processor_blocked1"] = "You are blocked and cannot log in!";
$_lang["login_processor_blocked2"] = "You are blocked and cannot log in! Please try again later.";
diff --git a/core/src/Auth/Contracts/LoginPipe.php b/core/src/Auth/Contracts/LoginPipe.php
new file mode 100644
index 0000000000..101fe45ca6
--- /dev/null
+++ b/core/src/Auth/Contracts/LoginPipe.php
@@ -0,0 +1,33 @@
+manager->loginByIdWithoutPipeline(['id' => $id]);` for a social
+ * or SSO login. Skipping `$next()` skips the password check that follows.
+ *
+ * A pipe may also post-process: `$user = $next($attempt); ...; return $user;`.
+ *
+ * Implementing this interface is not required — Illuminate\Pipeline only needs a
+ * `handle()` method — but it documents the contract and lets static analysis see it.
+ */
+interface LoginPipe
+{
+ /**
+ * @param LoginAttempt $attempt
+ * @param Closure $next
+ * @return mixed the authenticated user model
+ */
+ public function handle(LoginAttempt $attempt, Closure $next);
+}
diff --git a/core/src/Auth/LoginAttempt.php b/core/src/Auth/LoginAttempt.php
new file mode 100644
index 0000000000..f2b0400007
--- /dev/null
+++ b/core/src/Auth/LoginAttempt.php
@@ -0,0 +1,37 @@
+data[$key] ?? $default;
+ }
+}
diff --git a/core/src/Auth/LoginPipeline.php b/core/src/Auth/LoginPipeline.php
new file mode 100644
index 0000000000..9552bfa321
--- /dev/null
+++ b/core/src/Auth/LoginPipeline.php
@@ -0,0 +1,118 @@
+ [\EvolutionCMS\Auth\Pipes\EnsureNotThrottled::class],
+ * 'login' => [\Acme\Sso\Pipes\SocialAuthentication::class],
+ * ];
+ *
+ * `*` runs for every entry point and is where a second factor belongs: a pipe listed
+ * only under `login` is trivially bypassed through a password recovery link
+ * (`hashLogin`) or through remember-me (`loginById`).
+ */
+class LoginPipeline
+{
+ /** Entry points a pipeline can be attached to. */
+ public const STAGE_LOGIN = 'login';
+ public const STAGE_LOGIN_BY_ID = 'loginById';
+ public const STAGE_HASH_LOGIN = 'hashLogin';
+
+ /** Pseudo-stage: runs before every stage above. */
+ public const STAGE_ANY = '*';
+
+ /**
+ * @var Container|null
+ */
+ protected $container;
+
+ /**
+ * @param Container|null $container
+ */
+ public function __construct(?Container $container = null)
+ {
+ $this->container = $container;
+ }
+
+ /**
+ * @param LoginAttempt $attempt
+ * @param Closure $destination what the entry point would have done on its own
+ * @return mixed
+ */
+ public function run(LoginAttempt $attempt, Closure $destination)
+ {
+ $pipes = $this->pipesFor($attempt->stage);
+
+ if ($pipes === []) {
+ return $destination($attempt);
+ }
+
+ return (new Pipeline($this->resolveContainer()))
+ ->send($attempt)
+ ->through($pipes)
+ ->then($destination);
+ }
+
+ /**
+ * Ordered pipes for one stage: the `*` pipes first, then the stage's own.
+ *
+ * @param string $stage
+ * @return array
+ */
+ public function pipesFor(string $stage): array
+ {
+ $configured = $this->configuredPipeline();
+
+ // A flat list is read as "every stage", which is the safe reading: a pipe the
+ // author did not think about placing must not silently cover only one door.
+ if ($configured !== [] && array_is_list($configured)) {
+ $configured = [self::STAGE_ANY => $configured];
+ }
+
+ $pipes = array_merge(
+ (array) ($configured[self::STAGE_ANY] ?? []),
+ (array) ($configured[$stage] ?? [])
+ );
+
+ return array_values(array_filter($pipes, static function ($pipe) {
+ return is_string($pipe) ? $pipe !== '' : is_object($pipe);
+ }));
+ }
+
+ /**
+ * @return array
+ */
+ protected function configuredPipeline(): array
+ {
+ try {
+ $configured = config('cms.auth.pipeline', []);
+ } catch (\Throwable $exception) {
+ return [];
+ }
+
+ return is_array($configured) ? $configured : [];
+ }
+
+ /**
+ * @return Container
+ */
+ protected function resolveContainer(): Container
+ {
+ return $this->container ?? evolutionCMS();
+ }
+}
diff --git a/core/src/Auth/PipelineUserManager.php b/core/src/Auth/PipelineUserManager.php
new file mode 100644
index 0000000000..7d51e75fd7
--- /dev/null
+++ b/core/src/Auth/PipelineUserManager.php
@@ -0,0 +1,95 @@
+pipeline = $pipeline instanceof LoginPipeline ? $pipeline : new LoginPipeline();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function login(array $userData, bool $events = true, bool $cache = true)
+ {
+ return $this->pipeline->run(
+ new LoginAttempt(LoginPipeline::STAGE_LOGIN, $userData, $this, $events, $cache),
+ function (LoginAttempt $attempt) {
+ return parent::login($attempt->data, $attempt->events, $attempt->cache);
+ }
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function loginById(array $userData, bool $events = true, bool $cache = true)
+ {
+ return $this->pipeline->run(
+ new LoginAttempt(LoginPipeline::STAGE_LOGIN_BY_ID, $userData, $this, $events, $cache),
+ function (LoginAttempt $attempt) {
+ return parent::loginById($attempt->data, $attempt->events, $attempt->cache);
+ }
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function hashLogin(array $userData, bool $events = true, bool $cache = true)
+ {
+ return $this->pipeline->run(
+ new LoginAttempt(LoginPipeline::STAGE_HASH_LOGIN, $userData, $this, $events, $cache),
+ function (LoginAttempt $attempt) {
+ return parent::hashLogin($attempt->data, $attempt->events, $attempt->cache);
+ }
+ );
+ }
+
+ /**
+ * Log a user in by id without re-entering the pipeline.
+ *
+ * For pipes that authenticate the user themselves — social login, SSO — and only
+ * need the session written. Calling loginById() from inside a pipe would run the
+ * loginById pipeline nested inside the login pipeline, so every `*` pipe would run
+ * twice for one attempt.
+ *
+ * @param array $userData
+ * @param bool $events
+ * @param bool $cache
+ * @return mixed
+ */
+ public function loginByIdWithoutPipeline(array $userData, bool $events = true, bool $cache = true)
+ {
+ return parent::loginById($userData, $events, $cache);
+ }
+}
diff --git a/core/src/Auth/Pipes/EnsureNotThrottled.php b/core/src/Auth/Pipes/EnsureNotThrottled.php
new file mode 100644
index 0000000000..fa55dbfb2c
--- /dev/null
+++ b/core/src/Auth/Pipes/EnsureNotThrottled.php
@@ -0,0 +1,132 @@
+ 5, 'decay' => 300]
+ */
+class EnsureNotThrottled implements LoginPipe
+{
+ public const DEFAULT_ATTEMPTS = 5;
+ public const DEFAULT_DECAY_SECONDS = 300;
+
+ /**
+ * @var RateLimiter|null
+ */
+ protected $limiter;
+
+ /**
+ * @param RateLimiter|null $limiter
+ */
+ public function __construct($limiter = null)
+ {
+ $this->limiter = $limiter instanceof RateLimiter ? $limiter : null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function handle(LoginAttempt $attempt, Closure $next)
+ {
+ $limiter = $this->limiter();
+
+ if (is_null($limiter)) {
+ // No cache available (CLI, installer): rate limiting is a safety net, not a
+ // precondition, so a missing limiter must not block a legitimate login.
+ return $next($attempt);
+ }
+
+ $key = $this->throttleKey($attempt);
+ [$attempts, $decay] = $this->limits();
+
+ if ($limiter->tooManyAttempts($key, $attempts)) {
+ throw new ServiceActionException($this->message($limiter->availableIn($key)));
+ }
+
+ $limiter->hit($key, $decay);
+
+ $user = $next($attempt);
+
+ // Only a completed login clears the counter — a pipe further down the chain may
+ // still refuse the attempt, and that refusal has to keep counting.
+ $limiter->clear($key);
+
+ return $user;
+ }
+
+ /**
+ * @param LoginAttempt $attempt
+ * @return string
+ */
+ protected function throttleKey(LoginAttempt $attempt): string
+ {
+ $identity = (string) ($attempt->get('username', $attempt->get('id', '')));
+ $address = (string) ($_SERVER['REMOTE_ADDR'] ?? 'cli');
+
+ return 'evo-login|' . $attempt->stage . '|' . mb_strtolower($identity) . '|' . $address;
+ }
+
+ /**
+ * @return array{0: int, 1: int}
+ */
+ protected function limits(): array
+ {
+ try {
+ $configured = config('cms.auth.throttle', []);
+ } catch (\Throwable $exception) {
+ $configured = [];
+ }
+
+ $attempts = (int) ($configured['attempts'] ?? self::DEFAULT_ATTEMPTS);
+ $decay = (int) ($configured['decay'] ?? self::DEFAULT_DECAY_SECONDS);
+
+ return [
+ $attempts > 0 ? $attempts : self::DEFAULT_ATTEMPTS,
+ $decay > 0 ? $decay : self::DEFAULT_DECAY_SECONDS,
+ ];
+ }
+
+ /**
+ * @param int $seconds
+ * @return string
+ */
+ protected function message(int $seconds): string
+ {
+ try {
+ return \Lang::get('global.login_processor_throttled', ['seconds' => $seconds]);
+ } catch (\Throwable $exception) {
+ return 'Too many login attempts. Please try again in ' . $seconds . ' seconds.';
+ }
+ }
+
+ /**
+ * @return RateLimiter|null
+ */
+ protected function limiter(): ?RateLimiter
+ {
+ if (!is_null($this->limiter)) {
+ return $this->limiter;
+ }
+
+ try {
+ return evolutionCMS()->make(RateLimiter::class);
+ } catch (\Throwable $exception) {
+ return null;
+ }
+ }
+}
diff --git a/core/src/Controllers/SystemSettings.php b/core/src/Controllers/SystemSettings.php
index 5a3417fa65..3423bd76b7 100644
--- a/core/src/Controllers/SystemSettings.php
+++ b/core/src/Controllers/SystemSettings.php
@@ -1,6 +1,7 @@
parameterPasswordHash())) {
+ $out['pwd_hash_algo'] = Legacy\PasswordHash::ALGO_BCRYPT;
+ }
+
$out['filemanager_path'] = str_replace(
EVO_BASE_PATH,
'[(base_path)]',
@@ -289,36 +297,26 @@ protected function parameterPasswordHash(): array
$managerApi = $this->managerTheme->getCore()
->getManagerApi();
+ // Only algorithms that password_hash() can actually produce are offered. The
+ // pre-3.5 names (BLOWFISH_*, SHA*, MD5, UNCRYPT) described the legacy "v1"
+ // scheme, which is verify-only now: a stored v1 hash carries its own algorithm
+ // name, so nothing needs to select it here. Sites still holding one of those
+ // values keep working — PasswordHash::resolveAlgorithm() maps them to bcrypt.
return [
- 'BLOWFISH_Y' => [
- 'value' => 'BLOWFISH_Y',
- 'text' => 'CRYPT_BLOWFISH_Y (salt & stretch)',
- 'disabled' => $managerApi->checkHashAlgorithm('BLOWFISH_Y') ? 0 : 1
- ],
- 'BLOWFISH_A' => [
- 'value' => 'BLOWFISH_A',
- 'text' => 'CRYPT_BLOWFISH_A (salt & stretch)',
- 'disabled' => $managerApi->checkHashAlgorithm('BLOWFISH_A') ? 0 : 1
- ],
- 'SHA512' => [
- 'value' => 'SHA512',
- 'text' => 'CRYPT_SHA512 (salt & stretch)',
- 'disabled' => $managerApi->checkHashAlgorithm('SHA512') ? 0 : 1
- ],
- 'SHA256' => [
- 'value' => 'SHA256',
- 'text' => 'CRYPT_SHA256 (salt & stretch)',
- 'disabled' => $managerApi->checkHashAlgorithm('SHA256') ? 0 : 1
+ 'BCRYPT' => [
+ 'value' => 'BCRYPT',
+ 'text' => 'bcrypt (cost ' . \EvolutionCMS\Legacy\PasswordHash::BCRYPT_COST . ')',
+ 'disabled' => $managerApi->checkHashAlgorithm('BCRYPT') ? 0 : 1
],
- 'MD5' => [
- 'value' => 'MD5',
- 'text' => 'CRYPT_MD5 (salt & stretch)',
- 'disabled' => $managerApi->checkHashAlgorithm('MD5') ? 0 : 1
+ 'ARGON2ID' => [
+ 'value' => 'ARGON2ID',
+ 'text' => 'Argon2id (memory-hard, recommended)',
+ 'disabled' => $managerApi->checkHashAlgorithm('ARGON2ID') ? 0 : 1
],
- 'UNCRYPT' => [
- 'value' => 'UNCRYPT',
- 'text' => 'UNCRYPT(32 chars salt + SHA-1 hash)',
- 'disabled' => $managerApi->checkHashAlgorithm('UNCRYPT') ? 0 : 1
+ 'ARGON2I' => [
+ 'value' => 'ARGON2I',
+ 'text' => 'Argon2i',
+ 'disabled' => $managerApi->checkHashAlgorithm('ARGON2I') ? 0 : 1
],
];
}
diff --git a/core/src/Controllers/Users/ChangePassword.php b/core/src/Controllers/Users/ChangePassword.php
index bc01e707e8..eebc1d6803 100644
--- a/core/src/Controllers/Users/ChangePassword.php
+++ b/core/src/Controllers/Users/ChangePassword.php
@@ -30,6 +30,8 @@ public function process() : bool
{
try {
\UserManager::changeManagerPassword($_POST);
+ }catch (ServiceActionException $exception){
+ EvolutionCMS()->webAlertAndQuit($exception->getMessage(), 'index.php?a=28');
}catch (ServiceValidationException $exception){
foreach ($exception->getValidationErrors() as $errors){
if(is_array($errors)){
diff --git a/core/src/Controllers/Users/EditOrNewUser.php b/core/src/Controllers/Users/EditOrNewUser.php
index 6574de545b..38f6950ef1 100644
--- a/core/src/Controllers/Users/EditOrNewUser.php
+++ b/core/src/Controllers/Users/EditOrNewUser.php
@@ -57,7 +57,9 @@ public function process(): bool
if (isset($userData['password'])) {
$userData['clearPassword'] = $userData['password'];
$user->password = EvolutionCMS()->getPasswordHash()->HashPassword($userData['password']);
+ // A new password invalidates any outstanding recovery link.
$user->cachepwd = '';
+ $user->cachepwd_valid_to = null;
$user->save();
}
}
diff --git a/core/src/Controllers/Users/LogInOut.php b/core/src/Controllers/Users/LogInOut.php
index 8cbe550b3c..c00e58bc01 100644
--- a/core/src/Controllers/Users/LogInOut.php
+++ b/core/src/Controllers/Users/LogInOut.php
@@ -130,6 +130,16 @@ public function loginFromHash()
$_GET['hash'] = $hash;
+ // Recovery links are one-time and time limited. hashLogin() only matches the
+ // token itself, so the expiry is enforced here, before the login happens.
+ $recovery = new \EvolutionCMS\Services\PasswordRecoveryService();
+ $user = $recovery->findByToken($hash);
+
+ if (is_null($user)) {
+ jsAlert(\Lang::get('global.login_processor_hash_expired'));
+ exit();
+ }
+
try {
\UserManager::hashLogin($_GET);
} catch (ServiceActionException $exception) {
@@ -137,6 +147,10 @@ public function loginFromHash()
exit();
}
+ // hashLogin() clears the token; drop the expiry with it so no stale deadline
+ // is left behind on the row.
+ $recovery->clearToken($user->refresh());
+
header('Location: ' . EVO_MANAGER_URL . '#?a=28');
exit();
}
diff --git a/core/src/Core.php b/core/src/Core.php
index 23447d13f8..70441c9963 100644
--- a/core/src/Core.php
+++ b/core/src/Core.php
@@ -5521,12 +5521,29 @@ public function changeWebUserPassword($oldPwd, $newPwd)
->where('id', (int) $this->getLoginUserID())
->first();
- $row = $ds->toArray();
- if (!$row) {
+ if (is_null($ds)) {
return false;
}
- if ($row['password'] !== md5($oldPwd)) {
+ // Read the attributes off the model: `password` is in $hidden, so toArray()
+ // would drop it and every comparison below would silently fail.
+ $row = [
+ 'id' => $ds->getKey(),
+ 'username' => $ds->username,
+ 'password' => (string) $ds->password,
+ ];
+
+ $hashType = $this->getManagerApi()->getHashType($row['password']);
+
+ if ($hashType === 'md5') {
+ $matched = hash_equals((string) $row['password'], md5($oldPwd));
+ } elseif ($hashType === 'v1') {
+ $matched = loginV1($row['id'], $oldPwd, $row['password'], $row['username']);
+ } else {
+ $matched = $this->getPasswordHash()->CheckPassword($oldPwd, $row['password']);
+ }
+
+ if (!$matched) {
return 'Incorrect password.';
}
if (strlen($newPwd) < 6) {
@@ -5537,9 +5554,15 @@ public function changeWebUserPassword($oldPwd, $newPwd)
return "You didn't specify a password for this user!";
}
+ $hash = $this->getPasswordHash()->HashPassword($newPwd);
+
+ if (!is_string($hash) || $hash === '' || $hash === '*') {
+ return 'Password could not be hashed.';
+ }
+
\EvolutionCMS\Models\User::where('id', (int) $this->getLoginUserID())
->update([
- 'password' => $newPwd
+ 'password' => $hash
]);
// invoke OnWebChangePassword event
diff --git a/core/src/Exceptions/PasswordRecoveryRequiredException.php b/core/src/Exceptions/PasswordRecoveryRequiredException.php
new file mode 100644
index 0000000000..6bdda6ad91
--- /dev/null
+++ b/core/src/Exceptions/PasswordRecoveryRequiredException.php
@@ -0,0 +1,11 @@
+ loginMD5(), needs the user id
+ * v1 -> loginV1(), needs the user id (Evolution 1.x "algorithm>hash")
+ * phpass -> login(), everything password_hash()/phpass/crypt can verify
+ *
+ * Anything unrecognisable — plaintext, a truncated hash, an empty column — is also
+ * routed to login(), which detects it via PasswordHash::isUsable() and starts
+ * password recovery instead of reporting a wrong password.
+ *
* @param string $db_value
- * @return string
+ * @return string md5|v1|phpass
*/
public function getHashType($db_value = '')
- { // md5 | v1 | phpass
- $c = substr($db_value, 0, 1);
- if ($c === '$') {
- return 'phpass';
- } elseif (strlen($db_value) === 32) {
+ {
+ $db_value = (string) $db_value;
+
+ if (strlen($db_value) === 32 && ctype_xdigit($db_value)) {
return 'md5';
- } elseif ($c !== '$' && strpos($db_value, '>') !== false) {
+ }
+
+ if (substr($db_value, 0, 1) !== '$' && strpos($db_value, '>') !== false) {
return 'v1';
- } else {
- return 'unknown';
}
+
+ return 'phpass';
}
/**
@@ -208,6 +223,15 @@ public function checkHashAlgorithm($algorithm = '')
$result = false;
if (!empty($algorithm)) {
switch ($algorithm) {
+ case 'BCRYPT':
+ $result = true;
+ break;
+ case 'ARGON2ID':
+ $result = defined('PASSWORD_ARGON2ID');
+ break;
+ case 'ARGON2I':
+ $result = defined('PASSWORD_ARGON2I');
+ break;
case 'BLOWFISH_Y':
if (defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH == 1) {
if (version_compare('5.3.7', PHP_VERSION) <= 0) {
diff --git a/core/src/Legacy/PasswordHash.php b/core/src/Legacy/PasswordHash.php
index a36adbb451..7844fa7bbc 100644
--- a/core/src/Legacy/PasswordHash.php
+++ b/core/src/Legacy/PasswordHash.php
@@ -28,6 +28,22 @@
//
class PasswordHash implements PasswordHashInterface
{
+ /** Algorithm names as stored in the `pwd_hash_algo` system setting. */
+ public const ALGO_BCRYPT = 'BCRYPT';
+ public const ALGO_ARGON2ID = 'ARGON2ID';
+ public const ALGO_ARGON2I = 'ARGON2I';
+
+ /** Deliberately above the PHP 8.3 default of 10; matches the PHP 8.4 default. */
+ public const BCRYPT_COST = 12;
+
+ /** Storage formats recognised by identify(). */
+ public const FORMAT_NATIVE = 'native';
+ public const FORMAT_PORTABLE = 'portable';
+ public const FORMAT_CRYPT = 'crypt';
+ public const FORMAT_MD5 = 'md5';
+ public const FORMAT_V1 = 'v1';
+ public const FORMAT_UNKNOWN = 'unknown';
+
public $itoa64;
public $iteration_count_log2;
public $portable_hashes;
@@ -246,8 +262,15 @@ public function gensalt_blowfish($input)
}
/**
+ * Hash a password with the algorithm selected in the system settings.
+ *
+ * Always produces a self-describing password_hash() string ($2y$ / $argon2id$).
+ * The portable phpass format ($P$) is no longer generated — it is only still
+ * verified, so hashes written by earlier Evolution versions keep working until
+ * their owner logs in and the hash is upgraded in place.
+ *
* @param string $password
- * @return string
+ * @return string '*' when hashing failed — never a usable hash
*/
public function HashPassword($password)
{
@@ -255,6 +278,161 @@ public function HashPassword($password)
return '*';
}
+ [$algorithm, $options] = $this->resolveAlgorithm();
+
+ $hash = password_hash($password, $algorithm, $options);
+
+ // password_hash() only returns a non-string on catastrophic failure, but a
+ // truncated or empty result must never reach the database: '*' can never be
+ // produced by any hashing algorithm, so it can never validate.
+ if (!is_string($hash) || strlen($hash) < 20) {
+ return '*';
+ }
+
+ return $hash;
+ }
+
+ /**
+ * Resolve the configured algorithm into a password_hash() algorithm + options pair.
+ *
+ * Unknown and pre-3.5 values (BLOWFISH_Y, SHA512, UNCRYPT, ...) map to bcrypt:
+ * those names described the legacy "v1" hashing scheme, which is verify-only now.
+ *
+ * @return array{0: string|int, 1: array}
+ */
+ public function resolveAlgorithm(): array
+ {
+ $configured = strtoupper((string) $this->configuredAlgorithm());
+
+ if (($configured === self::ALGO_ARGON2ID || $configured === self::ALGO_ARGON2I)
+ && defined('PASSWORD_' . $configured)) {
+ return [
+ constant('PASSWORD_' . $configured),
+ [
+ 'memory_cost' => PASSWORD_ARGON2_DEFAULT_MEMORY_COST,
+ 'time_cost' => PASSWORD_ARGON2_DEFAULT_TIME_COST,
+ 'threads' => PASSWORD_ARGON2_DEFAULT_THREADS,
+ ],
+ ];
+ }
+
+ return [PASSWORD_BCRYPT, ['cost' => self::BCRYPT_COST]];
+ }
+
+ /**
+ * @return string
+ */
+ protected function configuredAlgorithm(): string
+ {
+ // Reachable from the installer and from CLI seeding, where no CMS instance
+ // exists yet. Any failure to read the setting simply means "use the default".
+ try {
+ if (!function_exists('evolutionCMS')) {
+ return self::ALGO_BCRYPT;
+ }
+
+ $value = evolutionCMS()->getConfig('pwd_hash_algo');
+ } catch (\Throwable $exception) {
+ return self::ALGO_BCRYPT;
+ }
+
+ return is_scalar($value) ? (string) $value : self::ALGO_BCRYPT;
+ }
+
+ /**
+ * Name the storage format of a hash already in the database.
+ *
+ * @param string $stored_hash
+ * @return string native|portable|crypt|md5|v1|unknown
+ */
+ public function identify($stored_hash): string
+ {
+ $stored_hash = (string) $stored_hash;
+
+ if ($stored_hash === '' || $stored_hash === '*') {
+ return self::FORMAT_UNKNOWN;
+ }
+
+ if (str_starts_with($stored_hash, '$2') || str_starts_with($stored_hash, '$argon2')) {
+ return self::FORMAT_NATIVE;
+ }
+
+ if (str_starts_with($stored_hash, '$P$') || str_starts_with($stored_hash, '$H$')) {
+ return self::FORMAT_PORTABLE;
+ }
+
+ if (strlen($stored_hash) === 32 && ctype_xdigit($stored_hash)) {
+ return self::FORMAT_MD5;
+ }
+
+ // "algorithm>hash" — the Evolution 1.x scheme, verified by ManagerApi::genV1Hash()
+ if (str_contains($stored_hash, '>')) {
+ return self::FORMAT_V1;
+ }
+
+ if (preg_match('/^\$[156]\$/', $stored_hash) === 1 || str_starts_with($stored_hash, '_')) {
+ return self::FORMAT_CRYPT;
+ }
+
+ return self::FORMAT_UNKNOWN;
+ }
+
+ /**
+ * Can this stored value be used to verify a password at all?
+ *
+ * False means the row is unusable — plaintext left behind by a buggy extra, a
+ * truncated hash, an empty column. Such an account cannot log in by any password,
+ * so the caller must start password recovery instead of reporting "wrong password".
+ *
+ * @param string $stored_hash
+ * @return bool
+ */
+ public function isUsable($stored_hash): bool
+ {
+ return $this->identify($stored_hash) !== self::FORMAT_UNKNOWN;
+ }
+
+ /**
+ * Must this hash be replaced after a successful login?
+ *
+ * True for every legacy format, and for current-format hashes whose algorithm or
+ * cost no longer matches the system settings.
+ *
+ * @param string $stored_hash
+ * @return bool
+ */
+ public function needsRehash($stored_hash): bool
+ {
+ $format = $this->identify($stored_hash);
+
+ if ($format === self::FORMAT_UNKNOWN) {
+ return false;
+ }
+
+ if ($format !== self::FORMAT_NATIVE) {
+ return true;
+ }
+
+ [$algorithm, $options] = $this->resolveAlgorithm();
+
+ return password_needs_rehash((string) $stored_hash, $algorithm, $options);
+ }
+
+ /**
+ * Hash in the legacy portable phpass format ($P$).
+ *
+ * Kept for backwards compatibility and tests only — nothing in the CMS writes
+ * this format any more.
+ *
+ * @param string $password
+ * @return string
+ */
+ public function HashPasswordPortable($password)
+ {
+ if (strlen($password) > 4096) {
+ return '*';
+ }
+
$random = '';
if (CRYPT_BLOWFISH == 1 && !$this->portable_hashes) {
@@ -294,21 +472,39 @@ public function HashPassword($password)
}
/**
+ * Verify a password against any hash format this CMS has ever written,
+ * except the "v1" scheme, which needs the user id as its salt seed and is
+ * handled by loginV1() / ManagerApi::genV1Hash().
+ *
* @param string $password
* @param string $stored_hash
* @return bool
*/
public function CheckPassword($password, $stored_hash)
{
+ $password = (string) $password;
+ $stored_hash = (string) $stored_hash;
+
if (strlen($password) > 4096) {
return false;
}
- $hash = $this->crypt_private($password, $stored_hash);
- if (substr($hash, 0, 1) === '*') {
- $hash = crypt($password, $stored_hash);
+ switch ($this->identify($stored_hash)) {
+ case self::FORMAT_NATIVE:
+ return password_verify($password, $stored_hash);
+
+ case self::FORMAT_PORTABLE:
+ return hash_equals($stored_hash, $this->crypt_private($password, $stored_hash));
+
+ case self::FORMAT_CRYPT:
+ return hash_equals($stored_hash, crypt($password, $stored_hash));
+
+ case self::FORMAT_MD5:
+ return hash_equals($stored_hash, md5($password));
}
- return ($hash === $stored_hash) ? true : false;
+ // Unknown format, including plaintext: refuse rather than compare. A plaintext
+ // column must never authenticate anybody — recovery is the only way out.
+ return false;
}
}
diff --git a/core/src/ManagerTheme.php b/core/src/ManagerTheme.php
index 9425b85c6d..25e6799b2b 100644
--- a/core/src/ManagerTheme.php
+++ b/core/src/ManagerTheme.php
@@ -954,43 +954,49 @@ public function repairPassword($plh)
return $this->makeTemplate('repair_form', 'manager_login_tpl', $plh, false);
}
if (isset($_GET['email'])) {
- $user = UserAttribute::where('email', $_GET['email'])->first();
- if (is_null($user)) {
+ $attributes = UserAttribute::where('email', $_GET['email'])->first();
+ $user = is_null($attributes)
+ ? null
+ : \EvolutionCMS\Models\User::query()->find($attributes->internalKey);
+
+ if (is_null($attributes) || is_null($user)) {
$output .= '' . \Lang::get('global.could_not_find_user') . '';
- }
- if ($user->blocked == 1) {
+ } elseif ($attributes->blocked == 1) {
$output .= '' . \Lang::get('global.user_is_blocked') . '';
} else {
- $hash = '';
try {
- $hash = \UserManager::repairPassword(['id' => $user->internalKey, 'mode' => 'hash']);
- } catch (ServiceValidationException $exception) {
- foreach ($exception->getValidationErrors() as $errors) {
- foreach ($errors as $error) {
- $output .= '' . $error . '';
- }
- }
+ $recovery = new \EvolutionCMS\Services\PasswordRecoveryService();
+ $hash = $recovery->issueToken($user);
+ $output .= $recovery->sendRecoveryMail($user, $hash, 'hash')
+ ? '
' . \Lang::get('global.email_sent') . '
' + : '' . \Lang::get('global.error_sending_email') . ''; + } catch (\Throwable $exception) { + $output .= '' . \Lang::get('global.error_sending_email') . ''; } - if ($output == '') - $output .= $this->sendRepairMail($_GET['email'], $hash, 'hash'); } } return $output . $this->makeTemplate('repair_button', 'manager_login_tpl', $plh, false); } + /** + * Kept for themes and extras that call it directly; the message itself is built by + * PasswordRecoveryService so there is only one copy of the recovery mail. + * + * @param string $email + * @param string $hash + * @param string $mode + * @return string + */ public function sendRepairMail($email, $hash, $mode) { - $body = ' -' . \Lang::get('global.forgot_password_email_intro') . ' ' . \Lang::get('global.forgot_password_email_link') . '
-' . \Lang::get('global.forgot_password_email_instructions') . '
-' . \Lang::get('global.forgot_password_email_fine_print') . '
'; - - $param = []; - $param['from'] = $this->getCore()->getConfig('site_name') . '<' . $this->getCore()->getConfig('emailsender') . '>'; - $param['to'] = $email; - $param['subject'] = \Lang::get('global.password_change_request'); - $param['body'] = $body; - $rs = $this->getCore()->sendmail($param); //ignore mail errors in this case + $attributes = UserAttribute::where('email', $email)->first(); + $user = is_null($attributes) + ? null + : \EvolutionCMS\Models\User::query()->find($attributes->internalKey); + + $rs = is_null($user) + ? false + : (new \EvolutionCMS\Services\PasswordRecoveryService())->sendRecoveryMail($user, $hash, $mode); if (!$rs) return '' . \Lang::get('global.error_sending_email') . ''; diff --git a/core/src/Models/User.php b/core/src/Models/User.php index 78c6f17be3..c9dc1357bb 100644 --- a/core/src/Models/User.php +++ b/core/src/Models/User.php @@ -25,9 +25,13 @@ class User extends Eloquent\Model protected $hidden = [ 'password', 'cachepwd', + 'cachepwd_valid_to', 'verified_key', ]; + // `cachepwd_valid_to` is deliberately absent: the recovery token's deadline is only + // ever set by PasswordRecoveryService through direct assignment, so there is no + // reason to expose it to mass assignment from request data. protected $fillable = [ 'username', 'password', diff --git a/core/src/Providers/PipelineUserManagerServiceProvider.php b/core/src/Providers/PipelineUserManagerServiceProvider.php new file mode 100644 index 0000000000..f5d01de1d5 --- /dev/null +++ b/core/src/Providers/PipelineUserManagerServiceProvider.php @@ -0,0 +1,43 @@ +app->singleton(LoginPipeline::class, function ($app) { + return new LoginPipeline($app); + }); + + $this->app->singleton('UserManager', function ($app) { + return new PipelineUserManager($app->make(LoginPipeline::class)); + }); + + // Resolving the contract yields the same singleton, so code can type-hint + // UserManagerInterface instead of reaching for the facade — without a second + // instance and without breaking anything that still uses the string key. + $this->app->alias('UserManager', UserManagerInterface::class); + } +} diff --git a/core/src/Services/AuthServices.php b/core/src/Services/AuthServices.php index 7875573c2d..0b3e2c5dfd 100644 --- a/core/src/Services/AuthServices.php +++ b/core/src/Services/AuthServices.php @@ -93,37 +93,42 @@ public function viaRemember() public function attempt($checked = []) { foreach ($checked as $key => $value) { - if (isset($this->user->{$key})) { - if ($this->user->{$key} == $value) { - unset($checked[$key]); + // The password is never compared as a plain attribute: a row still holding + // a plaintext password would otherwise authenticate on an equality match. + if ($key !== 'password') { + if (isset($this->user->{$key})) { + if ($this->user->{$key} == $value) { + unset($checked[$key]); + } } - } - if (isset($this->user->attributes->{$key})) { - if ($this->user->attributes->{$key} == $value) { - unset($checked[$key]); + if (isset($this->user->attributes->{$key})) { + if ($this->user->attributes->{$key} == $value) { + unset($checked[$key]); + } } - } - if ($key == 'password') { - $matchPassword = false; + continue; + } - // check user password - local authentication - $hashType = evo()->getManagerApi()->getHashType($this->user->password); + // check user password - local authentication + $hashType = evo()->getManagerApi()->getHashType($this->user->password); - if ($hashType == 'phpass') { - $matchPassword = login($this->user->username, $value, $this->user->password); - } elseif ($hashType == 'md5') { + try { + if ($hashType == 'md5') { $matchPassword = loginMD5($this->user->getKey(), $value, $this->user->password, $this->user->username); } elseif ($hashType == 'v1') { $matchPassword = loginV1($this->user->getKey(), $value, $this->user->password, $this->user->username); } else { - $matchPassword = false; + $matchPassword = login($this->user->username, $value, $this->user->password); } + } catch (\EvolutionCMS\Exceptions\PasswordRecoveryRequiredException $exception) { + // Recovery has been started for an unusable hash. This entry point has + // no way to show the message, so it just refuses the login. + $matchPassword = false; + } - - if ($matchPassword) { - unset($checked[$key]); - } + if ($matchPassword) { + unset($checked[$key]); } } if (count($checked) > 0) return false; diff --git a/core/src/Services/PasswordRecoveryService.php b/core/src/Services/PasswordRecoveryService.php new file mode 100644 index 0000000000..de17aa6422 --- /dev/null +++ b/core/src/Services/PasswordRecoveryService.php @@ -0,0 +1,273 @@ +hasValidToken($user)) { + return $user->cachepwd; + } + + $token = bin2hex(random_bytes(16)); + $ttl = $this->ttlSeconds(); + + $user->cachepwd = $token; + if ($this->supportsExpiry()) { + // No deadline recorded means the link never expires — that is what + // `pwd_repair_minutes` = 0 asks for. + $user->cachepwd_valid_to = $ttl > 0 ? date('Y-m-d H:i:s', time() + $ttl) : null; + } + $user->save(); + + return $token; + } + + /** + * Configured lifetime of a recovery link, in seconds. + * + * @return int 0 when links never expire + */ + public function ttlSeconds(): int + { + $minutes = $this->configuredMinutes(); + + // A missing or malformed setting must not silently mean "never expires"; + // only an explicit 0 does. + if (!is_numeric($minutes) || (int) $minutes < 0) { + $minutes = self::DEFAULT_TTL_MINUTES; + } + + return (int) $minutes * 60; + } + + /** + * @return mixed the raw `pwd_repair_minutes` setting, or null when unreadable + */ + protected function configuredMinutes() + { + try { + return evolutionCMS()->getConfig('pwd_repair_minutes'); + } catch (\Throwable $exception) { + return null; + } + } + + /** + * @param User $user + * @return bool + */ + public function hasValidToken(User $user): bool + { + if ((string) $user->cachepwd === '') { + return false; + } + + return !$this->isExpired($user); + } + + /** + * Resolve a token coming from a recovery link. + * + * @param string $token + * @return User|null null when unknown or expired + */ + public function findByToken($token): ?User + { + $token = (string) $token; + + if ($token === '') { + return null; + } + + $user = User::query()->where('cachepwd', $token)->first(); + + if (is_null($user)) { + return null; + } + + if ($this->isExpired($user)) { + // An expired token is spent: drop it so the link cannot be replayed. + $this->clearToken($user); + + return null; + } + + return $user; + } + + /** + * @param User $user + * @return void + */ + public function clearToken(User $user): void + { + $user->cachepwd = ''; + if ($this->supportsExpiry()) { + $user->cachepwd_valid_to = null; + } + $user->save(); + } + + /** + * Start recovery for an account whose stored password cannot be verified. + * + * @param User $user + * @return bool true when a mail was sent for this attempt + */ + public function startAutomaticRecovery(User $user): bool + { + // A token that is still valid means the mail already went out; sending another + // one on every login attempt would turn a broken row into a mail amplifier. + if ($this->hasValidToken($user)) { + return false; + } + + $token = $this->issueToken($user); + + // Only the manager area has a password-reset destination in the core. On the + // front end the token is still issued, so an extra can build its own flow. + if (evolutionCMS()->getContext() !== 'mgr') { + return false; + } + + return $this->sendRecoveryMail($user, $token); + } + + /** + * @param User $user + * @param string $token + * @param string $mode + * @return bool + */ + public function sendRecoveryMail(User $user, $token, $mode = 'hash'): bool + { + $email = is_null($user->attributes) ? '' : (string) $user->attributes->email; + + if ($email === '' || !defined('EVO_MANAGER_URL')) { + return false; + } + + $link = EVO_MANAGER_URL . '?a=0&hash=' . rawurlencode((string) $token) . '&mode=' . rawurlencode((string) $mode); + + $body = ' +' . \Lang::get('global.forgot_password_email_intro') . ' ' . \Lang::get('global.forgot_password_email_link') . '
+' . \Lang::get('global.forgot_password_email_instructions') . '
+' . $this->validityNotice($user) . '
'; + + $param = []; + $param['from'] = evolutionCMS()->getConfig('site_name') . '<' . evolutionCMS()->getConfig('emailsender') . '>'; + $param['to'] = $email; + $param['subject'] = \Lang::get('global.password_change_request'); + $param['body'] = $body; + + return evolutionCMS()->sendmail($param); + } + + /** + * The sentence that tells the recipient how long the link stays usable. + * + * Built from the deadline actually stored on the row, not from the current setting: + * changing `pwd_repair_minutes` afterwards must not make an already sent mail lie. + * + * @param User $user + * @return string + */ + protected function validityNotice(User $user): string + { + $validTo = $this->supportsExpiry() ? $user->cachepwd_valid_to : null; + + if (empty($validTo)) { + return \Lang::get('global.forgot_password_email_valid_unlimited'); + } + + return \Lang::get('global.forgot_password_email_valid_until', [ + 'datetime' => evolutionCMS()->toDateFormat(strtotime((string) $validTo)), + ]); + } + + /** + * @param User $user + * @return bool + */ + protected function isExpired(User $user): bool + { + if (!$this->supportsExpiry()) { + return false; + } + + $validTo = $user->cachepwd_valid_to; + + // No deadline recorded = never expires (`pwd_repair_minutes` = 0). Tokens that + // predate the expiry column cannot be mistaken for these: the migration that + // added the column cleared every token that existed at the time. + if (empty($validTo)) { + return false; + } + + return strtotime((string) $validTo) < time(); + } + + /** + * @return bool + */ + protected function supportsExpiry(): bool + { + if (self::$hasExpiryColumn === null) { + try { + self::$hasExpiryColumn = Schema::hasColumn('users', 'cachepwd_valid_to'); + } catch (\Throwable $exception) { + self::$hasExpiryColumn = false; + } + } + + return self::$hasExpiryColumn; + } + + /** + * Test seam: forget the cached column probe. + * + * @return void + */ + public static function flushSchemaCache(): void + { + self::$hasExpiryColumn = null; + } +} diff --git a/core/tests/Unit/Security/LoginPipelineTest.php b/core/tests/Unit/Security/LoginPipelineTest.php new file mode 100644 index 0000000000..0cde3ff4a3 --- /dev/null +++ b/core/tests/Unit/Security/LoginPipelineTest.php @@ -0,0 +1,257 @@ +stage; + + return $next($attempt); + } +} + +class FirstPipe extends RecordingPipe {} +class SecondPipe extends RecordingPipe {} + +/** A second factor: refuses the login outright when the code is missing. */ +class RequireCodePipe implements LoginPipe +{ + public function handle(LoginAttempt $attempt, Closure $next) + { + if ($attempt->get('code') !== '123456') { + throw new ServiceActionException('second factor required'); + } + + return $next($attempt); + } +} + +/** A social login: authenticates by itself and never reaches the password check. */ +class SocialPipe implements LoginPipe +{ + public function handle(LoginAttempt $attempt, Closure $next) + { + if ($attempt->get('oauth_token') === 'valid-token') { + return 'social-user'; + } + + return $next($attempt); + } +} + +/** Post-processing: sees the result of everything below it. */ +class AuditPipe implements LoginPipe +{ + public static array $seen = []; + + public function handle(LoginAttempt $attempt, Closure $next) + { + $user = $next($attempt); + static::$seen[] = $user; + + return $user; + } +} + +/** Lets a test supply the configuration without a booted CMS. */ +class ArrayConfiguredPipeline extends LoginPipeline +{ + public array $configured = []; + + protected function configuredPipeline(): array + { + return $this->configured; + } +} + +beforeEach(function () { + RecordingPipe::$log = []; + AuditPipe::$seen = []; +}); + +function makeAttempt(array $data = [], string $stage = LoginPipeline::STAGE_LOGIN): LoginAttempt +{ + return new LoginAttempt($stage, $data, new stdClass()); +} + +function passwordLogin(): Closure +{ + return static fn (LoginAttempt $attempt) => 'password-user'; +} + +test('an empty pipeline changes nothing', function () { + $pipeline = new ArrayConfiguredPipeline(new Container()); + + expect($pipeline->run(makeAttempt(), passwordLogin()))->toBe('password-user') + ->and(RecordingPipe::$log)->toBe([]); +}); + +test('two extras compose into one chain, in configured order', function () { + $pipeline = new ArrayConfiguredPipeline(new Container()); + $pipeline->configured = ['login' => [FirstPipe::class, SecondPipe::class]]; + + expect($pipeline->run(makeAttempt(), passwordLogin()))->toBe('password-user') + ->and(RecordingPipe::$log)->toBe(['FirstPipe:login', 'SecondPipe:login']); + + // Order is configuration, not chance: swapping the array swaps the chain. + RecordingPipe::$log = []; + $pipeline->configured = ['login' => [SecondPipe::class, FirstPipe::class]]; + $pipeline->run(makeAttempt(), passwordLogin()); + + expect(RecordingPipe::$log)->toBe(['SecondPipe:login', 'FirstPipe:login']); +}); + +test('a pipe can veto the login, which an OnManagerAuthentication plugin cannot', function () { + $pipeline = new ArrayConfiguredPipeline(new Container()); + $pipeline->configured = ['login' => [RequireCodePipe::class]]; + + expect(fn () => $pipeline->run(makeAttempt(['username' => 'admin']), passwordLogin())) + ->toThrow(ServiceActionException::class, 'second factor required'); + + // With the factor supplied the chain completes normally. + expect($pipeline->run(makeAttempt(['username' => 'admin', 'code' => '123456']), passwordLogin())) + ->toBe('password-user'); +}); + +test('the veto is reported by the existing login error handling', function () { + // LogInOut::simpleLogin() catches ServiceActionException and shows its message, + // so a refusing pipe needs no new plumbing in the controllers. + expect(is_subclass_of(ServiceActionException::class, Throwable::class))->toBeTrue(); +}); + +test('a social pipe can authenticate on its own and skip the password check', function () { + $pipeline = new ArrayConfiguredPipeline(new Container()); + $pipeline->configured = ['login' => [SocialPipe::class, FirstPipe::class]]; + + // Short-circuits: the pipes below it and the password check never run. + expect($pipeline->run(makeAttempt(['oauth_token' => 'valid-token']), passwordLogin())) + ->toBe('social-user') + ->and(RecordingPipe::$log)->toBe([]); + + // Without a token it falls through to the ordinary password login. + expect($pipeline->run(makeAttempt(['username' => 'admin']), passwordLogin())) + ->toBe('password-user') + ->and(RecordingPipe::$log)->toBe(['FirstPipe:login']); +}); + +test('a pipe can post-process the authenticated user', function () { + $pipeline = new ArrayConfiguredPipeline(new Container()); + $pipeline->configured = ['login' => [AuditPipe::class]]; + + $pipeline->run(makeAttempt(), passwordLogin()); + + expect(AuditPipe::$seen)->toBe(['password-user']); +}); + +test('the star stage covers every door into a session', function () { + $pipeline = new ArrayConfiguredPipeline(new Container()); + $pipeline->configured = [LoginPipeline::STAGE_ANY => [RequireCodePipe::class]]; + + // A second factor listed only under 'login' would be bypassed by a password + // recovery link (hashLogin) or by remember-me (loginById). + foreach ([ + LoginPipeline::STAGE_LOGIN, + LoginPipeline::STAGE_LOGIN_BY_ID, + LoginPipeline::STAGE_HASH_LOGIN, + ] as $stage) { + expect(fn () => $pipeline->run(makeAttempt([], $stage), passwordLogin())) + ->toThrow(ServiceActionException::class); + } +}); + +test('star pipes run before the stage specific ones', function () { + $pipeline = new ArrayConfiguredPipeline(new Container()); + $pipeline->configured = [ + 'login' => [SecondPipe::class], + LoginPipeline::STAGE_ANY => [FirstPipe::class], + ]; + + $pipeline->run(makeAttempt(), passwordLogin()); + + // Declaration order in the config array must not decide it — throttling and + // second factors have to run before anything stage specific. + expect(RecordingPipe::$log)->toBe(['FirstPipe:login', 'SecondPipe:login']); +}); + +test('a flat list is read as covering every stage', function () { + $pipeline = new ArrayConfiguredPipeline(new Container()); + $pipeline->configured = [FirstPipe::class]; + + $pipeline->run(makeAttempt([], LoginPipeline::STAGE_HASH_LOGIN), passwordLogin()); + + expect(RecordingPipe::$log)->toBe(['FirstPipe:hashLogin']); +}); + +test('empty and malformed entries are ignored instead of fataling', function () { + $pipeline = new ArrayConfiguredPipeline(new Container()); + $pipeline->configured = ['login' => ['', null, FirstPipe::class, false]]; + + expect($pipeline->pipesFor('login'))->toBe([FirstPipe::class]) + ->and($pipeline->run(makeAttempt(), passwordLogin()))->toBe('password-user'); +}); + +test('every entry point into a session runs the pipeline', function () { + $source = (string) file_get_contents(dirname(__DIR__, 3) . '/src/Auth/PipelineUserManager.php'); + + foreach (['login', 'loginById', 'hashLogin'] as $method) { + expect($source)->toContain('public function ' . $method . '(array $userData') + ->and($source)->toContain('parent::' . $method . '($attempt->data'); + } + + expect($source)->toContain('STAGE_LOGIN,') + ->and($source)->toContain('STAGE_LOGIN_BY_ID,') + ->and($source)->toContain('STAGE_HASH_LOGIN,'); +}); + +test('the pipeline ships enabled but empty, so behaviour is unchanged by default', function () { + $config = require dirname(__DIR__, 3) . '/config/cms/auth.php'; + $appConfig = (string) file_get_contents(dirname(__DIR__, 3) . '/config/app.php'); + + // The wrapper is the default provider, but an empty pipeline makes it a + // pass-through: shipping pipes enabled would change every existing install. + expect($appConfig)->toContain("'Evolution_UserManager' => EvolutionCMS\Providers\PipelineUserManagerServiceProvider::class") + ->and($config['pipeline'])->toBe([]); +}); + +test('the default provider keeps the container key the facade resolves', function () { + $provider = (string) file_get_contents(dirname(__DIR__, 3) . '/src/Providers/PipelineUserManagerServiceProvider.php'); + $appConfig = (string) file_get_contents(dirname(__DIR__, 3) . '/config/app.php'); + + // The facade alias stays as shipped, so all 16 core call sites keep working. + expect($provider)->toContain("singleton('UserManager'") + ->and($appConfig)->toContain("'UserManager' => EvolutionCMS\UserManager\Facades\UserManager::class"); +}); + +test('a site can put the plain package implementation back with one file', function () { + $appConfig = (string) file_get_contents(dirname(__DIR__, 3) . '/config/app.php'); + $provider = (string) file_get_contents(dirname(__DIR__, 3) . '/src/Providers/PipelineUserManagerServiceProvider.php'); + + // The provider list stays keyed, which is what makes a single custom config file + // able to override exactly this one entry. + expect($appConfig)->toContain("'Evolution_UserManager' =>") + ->and($provider)->toContain('core/custom/config/app/providers/Evolution_UserManager.php') + ->and(class_exists(\EvolutionCMS\UserManager\Providers\UserManagerServiceProvider::class))->toBeTrue(); +}); + +test('the user-manager package itself is not modified', function () { + $package = dirname(__DIR__, 3) . '/vendor/evolutioncms-services/user-manager/src/Services/UserManager.php'; + + // The whole point of the subclass: the package stays upgradable. + expect(is_file($package))->toBeTrue() + ->and((string) file_get_contents($package))->not->toContain('LoginPipeline'); +}); diff --git a/core/tests/Unit/Security/PasswordHashFormatsTest.php b/core/tests/Unit/Security/PasswordHashFormatsTest.php new file mode 100644 index 0000000000..4fbb65edb2 --- /dev/null +++ b/core/tests/Unit/Security/PasswordHashFormatsTest.php @@ -0,0 +1,155 @@ +algorithm; + } +} + +test('new hashes are password_hash() output, never phpass or md5', function () { + $hasher = new ConfigurablePasswordHash(); + $hash = $hasher->HashPassword('correct horse battery staple'); + + expect($hash)->not->toStartWith('$P$') + ->and($hash)->not->toStartWith('$H$') + ->and(password_get_info($hash)['algo'])->not->toBeNull() + ->and($hasher->identify($hash))->toBe(PasswordHash::FORMAT_NATIVE) + ->and($hasher->CheckPassword('correct horse battery staple', $hash))->toBeTrue() + ->and($hasher->CheckPassword('wrong', $hash))->toBeFalse() + ->and($hasher->needsRehash($hash))->toBeFalse(); +}); + +test('bcrypt is the default and is stretched beyond the PHP 8.3 default cost', function () { + $hasher = new ConfigurablePasswordHash(); + $hasher->algorithm = PasswordHash::ALGO_BCRYPT; + + $info = password_get_info($hasher->HashPassword('secret')); + + expect($info['algoName'])->toBe('bcrypt') + ->and($info['options']['cost'])->toBe(PasswordHash::BCRYPT_COST) + ->and(PasswordHash::BCRYPT_COST)->toBeGreaterThan(10); +}); + +test('argon2id is used when selected and supported', function () { + if (!defined('PASSWORD_ARGON2ID')) { + expect(true)->toBeTrue(); // libargon2 unavailable on this build + + return; + } + + $hasher = new ConfigurablePasswordHash(); + $hasher->algorithm = PasswordHash::ALGO_ARGON2ID; + + $hash = $hasher->HashPassword('secret'); + + expect(password_get_info($hash)['algoName'])->toBe('argon2id') + ->and($hasher->CheckPassword('secret', $hash))->toBeTrue() + ->and($hasher->needsRehash($hash))->toBeFalse(); +}); + +test('pre-3.5 algorithm settings fall back to bcrypt instead of failing', function () { + // Those names described the verify-only "v1" scheme; password_hash() has no + // equivalent, and an unknown setting must never stop a password from being hashed. + foreach (['BLOWFISH_Y', 'BLOWFISH_A', 'SHA512', 'SHA256', 'MD5', 'UNCRYPT', '0', ''] as $legacy) { + $hasher = new ConfigurablePasswordHash(); + $hasher->algorithm = $legacy; + + $hash = $hasher->HashPassword('secret'); + + expect(password_get_info($hash)['algoName'])->toBe('bcrypt') + ->and($hasher->CheckPassword('secret', $hash))->toBeTrue(); + } +}); + +test('every hash format the CMS has ever written still verifies', function () { + $hasher = new ConfigurablePasswordHash(); + + $portable = $hasher->HashPasswordPortable('secret'); + $native = $hasher->HashPassword('secret'); + $md5 = md5('secret'); + + expect($portable)->toStartWith('$P$') + ->and($hasher->identify($portable))->toBe(PasswordHash::FORMAT_PORTABLE) + ->and($hasher->CheckPassword('secret', $portable))->toBeTrue() + ->and($hasher->CheckPassword('wrong', $portable))->toBeFalse() + ->and($hasher->identify($md5))->toBe(PasswordHash::FORMAT_MD5) + ->and($hasher->CheckPassword('secret', $md5))->toBeTrue() + ->and($hasher->CheckPassword('wrong', $md5))->toBeFalse() + ->and($hasher->CheckPassword('secret', $native))->toBeTrue(); +}); + +test('a "v1" hash is recognised but left to loginV1, which owns the salt seed', function () { + $hasher = new ConfigurablePasswordHash(); + $v1 = 'uncrypt>' . md5('whatever') . substr(md5('salt'), 0, 8); + + expect($hasher->identify($v1))->toBe(PasswordHash::FORMAT_V1) + ->and($hasher->isUsable($v1))->toBeTrue() + ->and($hasher->needsRehash($v1))->toBeTrue() + ->and($hasher->CheckPassword('whatever', $v1))->toBeFalse(); +}); + +test('a plaintext password column can never authenticate anybody', function () { + $hasher = new ConfigurablePasswordHash(); + + // The exact failure this guards: password === stored value. + expect($hasher->CheckPassword('hunter2', 'hunter2'))->toBeFalse() + ->and($hasher->identify('hunter2'))->toBe(PasswordHash::FORMAT_UNKNOWN) + ->and($hasher->isUsable('hunter2'))->toBeFalse(); +}); + +test('unusable stored values are reported as such instead of silently failing', function () { + $hasher = new ConfigurablePasswordHash(); + + foreach (['', '*', 'hunter2', 'abcdef', str_repeat('z', 32)] as $broken) { + expect($hasher->identify($broken))->toBe(PasswordHash::FORMAT_UNKNOWN) + ->and($hasher->isUsable($broken))->toBeFalse() + ->and($hasher->CheckPassword($broken, $broken))->toBeFalse() + // Nothing to upgrade: recovery is the only way out of an unusable row. + ->and($hasher->needsRehash($broken))->toBeFalse(); + } +}); + +test('legacy formats are always scheduled for rehash on the next successful login', function () { + $hasher = new ConfigurablePasswordHash(); + + expect($hasher->needsRehash($hasher->HashPasswordPortable('secret')))->toBeTrue() + ->and($hasher->needsRehash(md5('secret')))->toBeTrue() + // bcrypt below the configured cost must be restretched too + ->and($hasher->needsRehash(password_hash('secret', PASSWORD_BCRYPT, ['cost' => 4])))->toBeTrue(); +}); + +test('changing the configured algorithm schedules a rehash of current hashes', function () { + if (!defined('PASSWORD_ARGON2ID')) { + expect(true)->toBeTrue(); + + return; + } + + $hasher = new ConfigurablePasswordHash(); + $bcrypt = $hasher->HashPassword('secret'); + + $hasher->algorithm = PasswordHash::ALGO_ARGON2ID; + + expect($hasher->needsRehash($bcrypt))->toBeTrue() + ->and($hasher->CheckPassword('secret', $bcrypt))->toBeTrue(); +}); + +test('hashing never returns a usable-looking value on failure', function () { + $hasher = new ConfigurablePasswordHash(); + + // Over the 4096 byte guard. + $hash = $hasher->HashPassword(str_repeat('a', 5000)); + + expect($hash)->toBe('*') + ->and($hasher->isUsable($hash))->toBeFalse() + ->and($hasher->CheckPassword(str_repeat('a', 5000), $hash))->toBeFalse(); +}); diff --git a/core/tests/Unit/Security/PasswordHashTypeRoutingTest.php b/core/tests/Unit/Security/PasswordHashTypeRoutingTest.php new file mode 100644 index 0000000000..13c4176bf5 --- /dev/null +++ b/core/tests/Unit/Security/PasswordHashTypeRoutingTest.php @@ -0,0 +1,50 @@ +getHashType(md5('secret')))->toBe('md5') + ->and($api->getHashType('uncrypt>' . md5('x') . '12345678'))->toBe('v1') + ->and($api->getHashType('blowfish_y>' . md5('x') . '12345678'))->toBe('v1') + ->and($api->getHashType('$P$Bp.7VsURZ7.kdfjhsdkfjhsdkfjhsd'))->toBe('phpass') + ->and($api->getHashType('$2y$12$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV'))->toBe('phpass') + ->and($api->getHashType('$argon2id$v=19$m=65536,t=4,p=1$c29tZXNhbHQ$hash'))->toBe('phpass'); +}); + +test('unusable stored values are routed to login(), which starts recovery', function () { + $api = new ManagerApi(); + + // Not md5 and not v1 — so they reach login(), where PasswordHash::isUsable() + // detects them and password recovery is started instead of a wrong-password reply. + expect($api->getHashType(''))->toBe('phpass') + ->and($api->getHashType('hunter2'))->toBe('phpass') + ->and($api->getHashType('*'))->toBe('phpass'); +}); + +test('a 32 character password is not mistaken for an md5 hash', function () { + $api = new ManagerApi(); + + // Exactly 32 characters but not hexadecimal: this is what a plaintext row written + // by the old changeWebUserPassword() looked like. Treating it as md5 would compare + // it against md5(input) forever; it must go down the recovery path instead. + $plaintext = str_repeat('zq', 16); + + expect(strlen($plaintext))->toBe(32) + ->and($api->getHashType($plaintext))->toBe('phpass'); +}); + +test('the new algorithms are offered only when PHP can produce them', function () { + $api = new ManagerApi(); + + expect($api->checkHashAlgorithm('BCRYPT'))->toBeTrue() + ->and($api->checkHashAlgorithm('ARGON2ID'))->toBe(defined('PASSWORD_ARGON2ID')) + ->and($api->checkHashAlgorithm('ARGON2I'))->toBe(defined('PASSWORD_ARGON2I')) + ->and($api->checkHashAlgorithm(''))->toBeFalse(); +}); diff --git a/core/tests/Unit/Security/PasswordRecoveryTest.php b/core/tests/Unit/Security/PasswordRecoveryTest.php new file mode 100644 index 0000000000..5cca64a8b1 --- /dev/null +++ b/core/tests/Unit/Security/PasswordRecoveryTest.php @@ -0,0 +1,227 @@ +minutes; + } +} + +test('an unusable stored password starts recovery instead of failing the login forever', function () use ($root) { + $processors = (string) file_get_contents($root . '/functions/processors.php'); + + $guard = strpos($processors, 'if (!$hasher->isUsable($dbasePassword)) {'); + $recovery = strpos($processors, 'startPasswordRecovery($username);'); + $check = strpos($processors, 'if (!$hasher->CheckPassword($givenPassword, $dbasePassword)) {'); + + // The unusable-format branch has to come first: CheckPassword() would answer + // "false" for such a row, which reads as a wrong password and never recovers. + expect($guard)->not->toBeFalse() + ->and($recovery)->not->toBeFalse() + ->and($check)->not->toBeFalse() + ->and($guard)->toBeLessThan($check) + ->and($recovery)->toBeLessThan($check); +}); + +test('a successful login upgrades a hash that no longer matches the settings', function () use ($root) { + $processors = (string) file_get_contents($root . '/functions/processors.php'); + + expect($processors)->toContain('if ($hasher->needsRehash($dbasePassword)) {') + ->and($processors)->toContain('updateNewHash($username, $givenPassword);'); +}); + +test('the recovery exception is reported by the existing login error handling', function () { + // LogInOut::simpleLogin() and the user-manager services only catch + // ServiceActionException; anything else would surface as a fatal error. + expect(is_subclass_of(PasswordRecoveryRequiredException::class, ServiceActionException::class))->toBeTrue(); +}); + +test('the recovery service exposes the whole token lifecycle', function () { + expect(method_exists(PasswordRecoveryService::class, 'issueToken'))->toBeTrue() + ->and(method_exists(PasswordRecoveryService::class, 'findByToken'))->toBeTrue() + ->and(method_exists(PasswordRecoveryService::class, 'clearToken'))->toBeTrue() + ->and(method_exists(PasswordRecoveryService::class, 'ttlSeconds'))->toBeTrue() + ->and(method_exists(PasswordRecoveryService::class, 'startAutomaticRecovery'))->toBeTrue(); +}); + +test('the link lifetime comes from the system settings, and 0 means never expires', function () { + $service = new ConfigurableRecoveryService(); + + $service->minutes = 60; + expect($service->ttlSeconds())->toBe(3600); + + $service->minutes = 15; + expect($service->ttlSeconds())->toBe(900); + + // The whole point of the setting: unlimited lifetime on request. + $service->minutes = 0; + expect($service->ttlSeconds())->toBe(0); + + $service->minutes = '0'; + expect($service->ttlSeconds())->toBe(0); +}); + +test('an unusable lifetime setting falls back to the default rather than to unlimited', function () { + $service = new ConfigurableRecoveryService(); + + // Never let a typo or an unreadable config silently produce eternal links. + foreach ([null, '', 'soon', -5, [], false] as $broken) { + $service->minutes = $broken; + + expect($service->ttlSeconds())->toBe(PasswordRecoveryService::DEFAULT_TTL_MINUTES * 60); + } +}); + +test('the default lifetime is finite and short', function () { + expect(PasswordRecoveryService::DEFAULT_TTL_MINUTES)->toBeGreaterThan(0) + ->and(PasswordRecoveryService::DEFAULT_TTL_MINUTES)->toBeLessThan(60 * 24); +}); + +test('the lifetime setting ships with a factory default and a settings field', function () use ($root) { + $factory = (string) file_get_contents($root . '/factory/settings.php'); + $view = (string) file_get_contents(dirname($root) . '/manager/views/page/system_settings/security.blade.php'); + + expect($factory)->toContain("'pwd_repair_minutes' =>") + ->and($view)->toContain("'name' => 'pwd_repair_minutes',") + ->and($view)->toContain("__('global.pwd_repair_minutes_message')"); +}); + +test('repeated login attempts on a broken row do not send repeated mails', function () use ($root) { + $service = (string) file_get_contents($root . '/src/Services/PasswordRecoveryService.php'); + + $start = strpos($service, 'public function startAutomaticRecovery'); + expect($start)->not->toBeFalse(); + + $body = substr($service, $start, 900); + + expect($body)->toContain('if ($this->hasValidToken($user)) {') + ->and($body)->toContain('return false;'); +}); + +test('an expired recovery token is rejected and cleared before it can log anybody in', function () use ($root) { + $controller = (string) file_get_contents($root . '/src/Controllers/Users/LogInOut.php'); + + $lookup = strpos($controller, '$user = $recovery->findByToken($hash);'); + $reject = strpos($controller, "jsAlert(\\Lang::get('global.login_processor_hash_expired'));"); + $login = strpos($controller, '\\UserManager::hashLogin($_GET)'); + + expect($lookup)->not->toBeFalse() + ->and($reject)->not->toBeFalse() + ->and($login)->not->toBeFalse() + ->and($lookup)->toBeLessThan($login) + ->and($reject)->toBeLessThan($login); +}); + +test('a token issued with an unlimited lifetime stores no deadline and never expires', function () use ($root) { + $service = (string) file_get_contents($root . '/src/Services/PasswordRecoveryService.php'); + + $issue = strpos($service, 'public function issueToken'); + $expired = strpos($service, 'protected function isExpired'); + + expect($issue)->not->toBeFalse() + ->and($expired)->not->toBeFalse() + // 0 seconds of lifetime is stored as "no deadline"... + ->and(substr($service, $issue, 900)) + ->toContain('$ttl > 0 ? date(\'Y-m-d H:i:s\', time() + $ttl) : null') + // ...and "no deadline" is read back as "still valid". + ->and(substr($service, $expired, 700))->toContain('if (empty($validTo)) {') + ->and(substr($service, $expired, 700))->toContain('return false;'); +}); + +test('the migration clears pre-existing tokens so an empty deadline is unambiguous', function () use ($root) { + // Old tokens carry no deadline and would otherwise become eternal the moment + // "no deadline" started to mean "never expires". + $migration = (string) file_get_contents( + $root . '/database/migrations/2026_08_20_000000_add_cachepwd_expiry_to_users.php' + ); + + expect($migration)->toContain("DB::table('users')->where('cachepwd', '<>', '')->update([") + ->and($migration)->toContain("'cachepwd' => ''"); +}); + +test('the recovery mail states the actual lifetime of the link it carries', function () use ($root) { + $service = (string) file_get_contents($root . '/src/Services/PasswordRecoveryService.php'); + + $start = strpos($service, 'protected function validityNotice'); + expect($start)->not->toBeFalse(); + + $body = substr($service, $start, 700); + + // Read from the row, not from the current setting: changing the setting later + // must not make an already delivered mail lie about its own link. + expect($body)->toContain('$user->cachepwd_valid_to') + ->and($body)->toContain("forgot_password_email_valid_unlimited") + ->and($body)->toContain("forgot_password_email_valid_until") + ->and($service)->toContain('$this->validityNotice($user)'); +}); + +test('the users table carries the token expiry on every install path', function () use ($root) { + $migration = $root . '/database/migrations/2026_08_20_000000_add_cachepwd_expiry_to_users.php'; + $mirrored = dirname($root) . '/install/stubs/migrations/2026_08_20_000000_add_cachepwd_expiry_to_users.php'; + $baseline = (string) file_get_contents($root . '/database/migrations/2025_12_25_000000_initial_schema.php'); + + expect(is_file($migration))->toBeTrue() + ->and(is_file($mirrored))->toBeTrue() + ->and(file_get_contents($migration))->toBe(file_get_contents($mirrored)) + // DATETIME, not TIMESTAMP: a long but finite lifetime must not hit the 2038 limit. + ->and($baseline)->toContain("dateTime('cachepwd_valid_to')") + ->and(file_get_contents($migration))->toContain("dateTime('cachepwd_valid_to')"); +}); + +test('the forgot-password form survives an unknown e-mail address', function () use ($root) { + $theme = (string) file_get_contents($root . '/src/ManagerTheme.php'); + + $start = strpos($theme, 'public function repairPassword'); + expect($start)->not->toBeFalse(); + + $body = substr($theme, $start, 1600); + + // The previous version dereferenced $user right after finding it was null. + expect($body)->toContain('if (is_null($attributes) || is_null($user)) {') + ->and($body)->toContain("\\Lang::get('global.could_not_find_user')"); +}); + +test('the recovery messages are translated in every shipped language', function () use ($root) { + // A missing key would show the raw lexicon name to the user on the login screen. + $missing = []; + + foreach ((array) glob($root . '/lang/*/global.php') as $file) { + $_lang = []; + include $file; + + $language = basename(dirname($file)); + + $keys = [ + 'login_processor_password_recovery', + 'login_processor_hash_expired', + // The lifetime setting and what it does — including that 0 means unlimited. + 'pwd_repair_minutes_title', + 'pwd_repair_minutes_message', + // What the recovery mail tells its recipient about the link's validity. + 'forgot_password_email_valid_until', + 'forgot_password_email_valid_unlimited', + ]; + + foreach ($keys as $key) { + if (empty($_lang[$key])) { + $missing[] = $language . '.' . $key; + } + } + } + + expect($missing)->toBe([]) + // Guards the glob itself: an empty sweep would pass vacuously. + ->and(count((array) glob($root . '/lang/*/global.php')))->toBeGreaterThan(20); +}); diff --git a/core/tests/Unit/Security/PasswordStorageTest.php b/core/tests/Unit/Security/PasswordStorageTest.php new file mode 100644 index 0000000000..e91e114b3d --- /dev/null +++ b/core/tests/Unit/Security/PasswordStorageTest.php @@ -0,0 +1,131 @@ +isFile() && $file->getExtension() === 'php') { + $files[] = str_replace('\\', '/', $file->getPathname()); + } + } + } + sort($files); + + return $files; +}; + +$hashesInline = static function (string $line): bool { + return str_contains($line, 'HashPassword(') || str_contains($line, 'password_hash('); +}; + +test('every write to a user password attribute stores a hash', function () use ($phpFiles, $scanRoots, $hashesInline) { + $unhashed = []; + + foreach ($phpFiles($scanRoots) as $file) { + $source = (string) file_get_contents($file); + $lines = explode("\n", $source); + + foreach ($lines as $number => $line) { + // Eloquent attribute write: $user->password = ... + if (preg_match('/\$\w+(?:->\w+)*->password\s*=\s*([^=].*?);/', $line, $matches) !== 1) { + continue; + } + + $value = trim($matches[1]); + + if ($hashesInline($value)) { + continue; + } + + // Assigned from a local variable — follow it back to its own assignment. + if (preg_match('/^(?:\(string\)\s*)?(\$\w+)$/', $value, $variable) === 1) { + $pattern = '/' . preg_quote($variable[1], '/') . '\s*=\s*[^=].*(?:HashPassword\(|password_hash\()/'; + if (preg_match($pattern, $source) === 1) { + continue; + } + } + + $unhashed[] = basename($file) . ':' . ($number + 1) . ' — ' . trim($line); + } + } + + expect($unhashed)->toBe([]); +}); + +test('no source stores md5 as a password', function () use ($phpFiles, $scanRoots) { + $offenders = []; + + foreach ($phpFiles($scanRoots) as $file) { + foreach (explode("\n", (string) file_get_contents($file)) as $number => $line) { + if (preg_match('/(?:->password|\[.password.\]|.password.\s*=>)\s*=?>?\s*(?:\(string\)\s*)?md5\(/', $line) === 1) { + $offenders[] = basename($file) . ':' . ($number + 1) . ' — ' . trim($line); + } + } + } + + expect($offenders)->toBe([]); +}); + +test('the admin seeder hashes the initial password', function () use ($root) { + $seeder = (string) file_get_contents($root . '/database/seeders/AdminUserTableSeeder.php'); + + expect($seeder)->toContain("'password' => evolutionCMS()->getPasswordHash()->HashPassword(\$password),") + ->and($seeder)->not->toContain('md5($password)'); +}); + +test('updateNewHash refuses to persist a failed hash over a working one', function () use ($root) { + $processors = (string) file_get_contents($root . '/functions/processors.php'); + + $guard = strpos($processors, "\$hash === '*'"); + $write = strpos($processors, "\$field['password'] = \$hash;"); + + expect($guard)->not->toBeFalse() + ->and($write)->not->toBeFalse() + ->and($guard)->toBeLessThan($write); +}); + +test('changeWebUserPassword verifies against the stored format and stores a hash', function () use ($root) { + $core = (string) file_get_contents($root . '/src/Core.php'); + + $start = strpos($core, 'public function changeWebUserPassword'); + expect($start)->not->toBeFalse(); + + $body = substr($core, $start, 2600); + + expect($body)->toContain('$this->getPasswordHash()->HashPassword($newPwd)') + ->and($body)->toContain('$this->getManagerApi()->getHashType(') + // The old implementation compared the stored value against md5($oldPwd) whatever + // its real format was, and then wrote $newPwd verbatim. + ->and($body)->not->toContain("'password' => \$newPwd") + ->and($body)->not->toContain("!== md5(\$oldPwd)") + // md5 may still appear — but only as one branch, chosen by the stored format. + ->and($body)->toContain("if (\$hashType === 'md5') {"); +}); + +test('the generic credential comparison cannot be satisfied by a password attribute', function () use ($root) { + $auth = (string) file_get_contents($root . '/src/Services/AuthServices.php'); + + // Auth::attempt() unsets any checked field whose model attribute equals the given + // value. Applied to the password that is a plaintext-row backdoor. + expect($auth)->toContain("if (\$key !== 'password') {") + ->and($auth)->toContain('login($this->user->username, $value, $this->user->password)'); +}); diff --git a/core/tests/Unit/Security/UserManagerContractTest.php b/core/tests/Unit/Security/UserManagerContractTest.php new file mode 100644 index 0000000000..ae025ad081 --- /dev/null +++ b/core/tests/Unit/Security/UserManagerContractTest.php @@ -0,0 +1,100 @@ +toBe([]) + ->and(count($contract))->toBe(count($shipped)); +}); + +test('the contract matches the shipped signatures exactly', function () { + // A stricter signature here — an added type, a dropped default, a return type — + // would make the package's own class unable to satisfy its own contract. + $contract = new ReflectionClass(UserManagerInterface::class); + $shipped = new ReflectionClass(UserManager::class); + + $describe = static function (ReflectionMethod $method): string { + $parts = []; + foreach ($method->getParameters() as $parameter) { + $type = $parameter->getType() ? (string) $parameter->getType() . ' ' : ''; + $default = $parameter->isDefaultValueAvailable() + ? ' = ' . json_encode($parameter->getDefaultValue()) + : ''; + $parts[] = $type . '$' . $parameter->getName() . $default; + } + + return $method->getName() . '(' . implode(', ', $parts) . ')' + . ($method->getReturnType() ? ': ' . $method->getReturnType() : ''); + }; + + $mismatched = []; + foreach ($contract->getMethods() as $method) { + $theirs = $describe($shipped->getMethod($method->getName())); + $ours = $describe($method); + + if ($theirs !== $ours) { + $mismatched[] = $ours . ' != ' . $theirs; + } + } + + expect($mismatched)->toBe([]); +}); + +test('the contract declares no return types, so existing implementations stay valid', function () { + // The package declares none. A return type in the contract would be a breaking + // change for every third-party implementation, not a tightening. + $withReturnTypes = []; + + foreach ((new ReflectionClass(UserManagerInterface::class))->getMethods() as $method) { + if ($method->hasReturnType()) { + $withReturnTypes[] = $method->getName(); + } + } + + expect($withReturnTypes)->toBe([]); +}); + +test('the default implementation satisfies the contract', function () { + // PHP enforces this at class load; the test states the intent and fails loudly if + // the `implements` clause is ever dropped. + expect((new ReflectionClass(PipelineUserManager::class))->implementsInterface(UserManagerInterface::class)) + ->toBeTrue(); +}); + +test('resolving the contract and the string key yields the same instance', function () { + $app = new Container(); + $app->instance('config', new Repository(['cms' => ['auth' => ['pipeline' => []]]])); + + (new PipelineUserManagerServiceProvider($app))->register(); + + $byKey = $app->make('UserManager'); + $byContract = $app->make(UserManagerInterface::class); + + expect($byKey)->toBeInstanceOf(PipelineUserManager::class) + ->and($byContract === $byKey)->toBeTrue() + ->and($byKey instanceof UserManagerInterface)->toBeTrue(); +}); + +test('the contract lives in the core, not in the package it describes', function () { + // So that a site replacing the package still has something to implement. + $path = dirname(__DIR__, 3) . '/src/Interfaces/UserManagerInterface.php'; + + expect(is_file($path))->toBeTrue() + ->and((string) file_get_contents($path))->toContain('namespace EvolutionCMS\Interfaces;'); +}); diff --git a/core/vendor/bin/carbon.bat b/core/vendor/bin/carbon.bat new file mode 100644 index 0000000000..806cf153b7 --- /dev/null +++ b/core/vendor/bin/carbon.bat @@ -0,0 +1,5 @@ +@ECHO OFF +setlocal DISABLEDELAYEDEXPANSION +SET BIN_TARGET=%~dp0/carbon +SET COMPOSER_RUNTIME_BIN_DIR=%~dp0 +php "%BIN_TARGET%" %* diff --git a/core/vendor/bin/patch-type-declarations.bat b/core/vendor/bin/patch-type-declarations.bat new file mode 100644 index 0000000000..2b0707968a --- /dev/null +++ b/core/vendor/bin/patch-type-declarations.bat @@ -0,0 +1,5 @@ +@ECHO OFF +setlocal DISABLEDELAYEDEXPANSION +SET BIN_TARGET=%~dp0/patch-type-declarations +SET COMPOSER_RUNTIME_BIN_DIR=%~dp0 +php "%BIN_TARGET%" %* diff --git a/core/vendor/bin/var-dump-server.bat b/core/vendor/bin/var-dump-server.bat new file mode 100644 index 0000000000..94333da54c --- /dev/null +++ b/core/vendor/bin/var-dump-server.bat @@ -0,0 +1,5 @@ +@ECHO OFF +setlocal DISABLEDELAYEDEXPANSION +SET BIN_TARGET=%~dp0/var-dump-server +SET COMPOSER_RUNTIME_BIN_DIR=%~dp0 +php "%BIN_TARGET%" %* diff --git a/core/vendor/composer/autoload_classmap.php b/core/vendor/composer/autoload_classmap.php index ac6d44bf9c..fd81d68b36 100644 --- a/core/vendor/composer/autoload_classmap.php +++ b/core/vendor/composer/autoload_classmap.php @@ -6,6 +6,7 @@ $baseDir = dirname($vendorDir); return array( + 'AddCachepwdExpiryToUsers' => $baseDir . '/database/migrations/2026_08_20_000000_add_cachepwd_expiry_to_users.php', 'BladeUI\\Icons\\BladeIconsServiceProvider' => $vendorDir . '/blade-ui-kit/blade-icons/src/BladeIconsServiceProvider.php', 'BladeUI\\Icons\\Components\\Icon' => $vendorDir . '/blade-ui-kit/blade-icons/src/Components/Icon.php', 'BladeUI\\Icons\\Components\\Svg' => $vendorDir . '/blade-ui-kit/blade-icons/src/Components/Svg.php', @@ -679,6 +680,7 @@ 'Doctrine\\DBAL\\Platforms\\Keywords\\DB2Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/DB2Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\KeywordList' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/KeywordList.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDB117Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MariaDB117Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDB123Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MariaDB123Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDBKeywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL80Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MySQL80Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL84Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MySQL84Keywords.php', @@ -691,6 +693,7 @@ 'Doctrine\\DBAL\\Platforms\\MariaDB1052Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDB1052Platform.php', 'Doctrine\\DBAL\\Platforms\\MariaDB1060Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDB1060Platform.php', 'Doctrine\\DBAL\\Platforms\\MariaDB110700Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDB110700Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDB120300Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDB120300Platform.php', 'Doctrine\\DBAL\\Platforms\\MariaDBPlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDBPlatform.php', 'Doctrine\\DBAL\\Platforms\\MySQL80Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL80Platform.php', 'Doctrine\\DBAL\\Platforms\\MySQL84Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL84Platform.php', @@ -1114,6 +1117,11 @@ 'Egulias\\EmailValidator\\Warning\\Warning' => $vendorDir . '/egulias/email-validator/src/Warning/Warning.php', 'EvolutionCMS\\AbstractLaravel' => $baseDir . '/src/AbstractLaravel.php', 'EvolutionCMS\\AliasLoader' => $baseDir . '/src/AliasLoader.php', + 'EvolutionCMS\\Auth\\Contracts\\LoginPipe' => $baseDir . '/src/Auth/Contracts/LoginPipe.php', + 'EvolutionCMS\\Auth\\LoginAttempt' => $baseDir . '/src/Auth/LoginAttempt.php', + 'EvolutionCMS\\Auth\\LoginPipeline' => $baseDir . '/src/Auth/LoginPipeline.php', + 'EvolutionCMS\\Auth\\PipelineUserManager' => $baseDir . '/src/Auth/PipelineUserManager.php', + 'EvolutionCMS\\Auth\\Pipes\\EnsureNotThrottled' => $baseDir . '/src/Auth/Pipes/EnsureNotThrottled.php', 'EvolutionCMS\\Bootstrap\\EnvCacheLoader' => $baseDir . '/src/Bootstrap/EnvCacheLoader.php', 'EvolutionCMS\\Console' => $baseDir . '/src/Console.php', 'EvolutionCMS\\Console\\ClearCacheFullCommand' => $baseDir . '/src/Console/ClearCacheFullCommand.php', @@ -1205,6 +1213,7 @@ 'EvolutionCMS\\Events\\VendorTagPublished' => $baseDir . '/src/Events/VendorTagPublished.php', 'EvolutionCMS\\ExceptionHandler' => $baseDir . '/src/ExceptionHandler.php', 'EvolutionCMS\\Exceptions\\InvalidFieldException' => $baseDir . '/src/Exceptions/InvalidFieldException.php', + 'EvolutionCMS\\Exceptions\\PasswordRecoveryRequiredException' => $baseDir . '/src/Exceptions/PasswordRecoveryRequiredException.php', 'EvolutionCMS\\Exceptions\\ServiceActionException' => $baseDir . '/src/Exceptions/ServiceActionException.php', 'EvolutionCMS\\Exceptions\\ServiceValidationException' => $baseDir . '/src/Exceptions/ServiceValidationException.php', 'EvolutionCMS\\Exceptions\\TableNotDefinedException' => $baseDir . '/src/Exceptions/TableNotDefinedException.php', @@ -1246,6 +1255,7 @@ 'EvolutionCMS\\Interfaces\\PhpCompatInterface' => $baseDir . '/src/Interfaces/PhpCompatInterface.php', 'EvolutionCMS\\Interfaces\\ServiceProviderInterface' => $baseDir . '/src/Interfaces/ServiceProviderInterface.php', 'EvolutionCMS\\Interfaces\\TracyPanel' => $baseDir . '/src/Interfaces/TracyPanel.php', + 'EvolutionCMS\\Interfaces\\UserManagerInterface' => $baseDir . '/src/Interfaces/UserManagerInterface.php', 'EvolutionCMS\\Legacy\\Cache' => $baseDir . '/src/Legacy/Cache.php', 'EvolutionCMS\\Legacy\\Categories' => $baseDir . '/src/Legacy/Categories.php', 'EvolutionCMS\\Legacy\\DeprecatedCore' => $baseDir . '/src/Legacy/DeprecatedCore.php', @@ -1339,6 +1349,7 @@ 'EvolutionCMS\\Providers\\PasswordHashServiceProvider' => $baseDir . '/src/Providers/PasswordHashServiceProvider.php', 'EvolutionCMS\\Providers\\PhpCompatServiceProvider' => $baseDir . '/src/Providers/PhpCompatServiceProvider.php', 'EvolutionCMS\\Providers\\PhxServiceProvider' => $baseDir . '/src/Providers/PhxServiceProvider.php', + 'EvolutionCMS\\Providers\\PipelineUserManagerServiceProvider' => $baseDir . '/src/Providers/PipelineUserManagerServiceProvider.php', 'EvolutionCMS\\Providers\\RoutingServiceProvider' => $baseDir . '/src/Providers/RoutingServiceProvider.php', 'EvolutionCMS\\Providers\\SessionServiceProvider' => $baseDir . '/src/Providers/SessionServiceProvider.php', 'EvolutionCMS\\Providers\\SystemTasksServiceProvider' => $baseDir . '/src/Providers/SystemTasksServiceProvider.php', @@ -1351,6 +1362,7 @@ 'EvolutionCMS\\Services\\ComposerVersionSynchronizer' => $baseDir . '/src/Services/ComposerVersionSynchronizer.php', 'EvolutionCMS\\Services\\ConfigService' => $baseDir . '/src/Services/ConfigService.php', 'EvolutionCMS\\Services\\DatabaseBackupService' => $baseDir . '/src/Services/DatabaseBackupService.php', + 'EvolutionCMS\\Services\\PasswordRecoveryService' => $baseDir . '/src/Services/PasswordRecoveryService.php', 'EvolutionCMS\\Services\\Store\\CatalogService' => $baseDir . '/src/Services/Store/CatalogService.php', 'EvolutionCMS\\Services\\Store\\InstalledStateService' => $baseDir . '/src/Services/Store/InstalledStateService.php', 'EvolutionCMS\\Services\\Store\\LegacyDeleteService' => $baseDir . '/src/Services/Store/LegacyDeleteService.php', @@ -1383,6 +1395,7 @@ 'EvolutionCMS\\Support\\Formatter\\HtmlFormatter' => $baseDir . '/src/Support/Formatter/HtmlFormatter.php', 'EvolutionCMS\\Support\\Formatter\\SqlFormatter' => $baseDir . '/src/Support/Formatter/SqlFormatter.php', 'EvolutionCMS\\Support\\MailTestMailer' => $baseDir . '/src/Support/MailTestMailer.php', + 'EvolutionCMS\\Support\\MailTestSmtp' => $baseDir . '/src/Support/MailTestSmtp.php', 'EvolutionCMS\\Support\\MakeTable' => $baseDir . '/src/Support/MakeTable.php', 'EvolutionCMS\\Support\\Menu' => $baseDir . '/src/Support/Menu.php', 'EvolutionCMS\\Support\\MoveDocumentTargetGuard' => $baseDir . '/src/Support/MoveDocumentTargetGuard.php', @@ -1481,6 +1494,7 @@ 'GuzzleHttp\\Handler\\CurlVersion' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/CurlVersion.php', 'GuzzleHttp\\Handler\\EasyHandle' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/EasyHandle.php', 'GuzzleHttp\\Handler\\HeaderProcessor' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php', + 'GuzzleHttp\\Handler\\HostValidator' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/HostValidator.php', 'GuzzleHttp\\Handler\\MockHandler' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/MockHandler.php', 'GuzzleHttp\\Handler\\Proxy' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/Proxy.php', 'GuzzleHttp\\Handler\\ProxyEnvironment' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/ProxyEnvironment.php', @@ -3134,6 +3148,7 @@ 'Predis\\Command\\Argument\\Geospatial\\FromMember' => $vendorDir . '/predis/predis/src/Command/Argument/Geospatial/FromMember.php', 'Predis\\Command\\Argument\\Search\\AggregateArguments' => $vendorDir . '/predis/predis/src/Command/Argument/Search/AggregateArguments.php', 'Predis\\Command\\Argument\\Search\\AlterArguments' => $vendorDir . '/predis/predis/src/Command/Argument/Search/AlterArguments.php', + 'Predis\\Command\\Argument\\Search\\CollectArguments' => $vendorDir . '/predis/predis/src/Command/Argument/Search/CollectArguments.php', 'Predis\\Command\\Argument\\Search\\CommonArguments' => $vendorDir . '/predis/predis/src/Command/Argument/Search/CommonArguments.php', 'Predis\\Command\\Argument\\Search\\CreateArguments' => $vendorDir . '/predis/predis/src/Command/Argument/Search/CreateArguments.php', 'Predis\\Command\\Argument\\Search\\CursorArguments' => $vendorDir . '/predis/predis/src/Command/Argument/Search/CursorArguments.php', @@ -3177,7 +3192,9 @@ 'Predis\\Command\\Argument\\TimeSeries\\InfoArguments' => $vendorDir . '/predis/predis/src/Command/Argument/TimeSeries/InfoArguments.php', 'Predis\\Command\\Argument\\TimeSeries\\MGetArguments' => $vendorDir . '/predis/predis/src/Command/Argument/TimeSeries/MGetArguments.php', 'Predis\\Command\\Argument\\TimeSeries\\MRangeArguments' => $vendorDir . '/predis/predis/src/Command/Argument/TimeSeries/MRangeArguments.php', + 'Predis\\Command\\Argument\\TimeSeries\\NRangeArguments' => $vendorDir . '/predis/predis/src/Command/Argument/TimeSeries/NRangeArguments.php', 'Predis\\Command\\Argument\\TimeSeries\\RangeArguments' => $vendorDir . '/predis/predis/src/Command/Argument/TimeSeries/RangeArguments.php', + 'Predis\\Command\\Argument\\TimeSeries\\ReadArguments' => $vendorDir . '/predis/predis/src/Command/Argument/TimeSeries/ReadArguments.php', 'Predis\\Command\\Command' => $vendorDir . '/predis/predis/src/Command/Command.php', 'Predis\\Command\\CommandInterface' => $vendorDir . '/predis/predis/src/Command/CommandInterface.php', 'Predis\\Command\\Container\\ACL' => $vendorDir . '/predis/predis/src/Command/Container/ACL.php', @@ -3187,6 +3204,7 @@ 'Predis\\Command\\Container\\ContainerFactory' => $vendorDir . '/predis/predis/src/Command/Container/ContainerFactory.php', 'Predis\\Command\\Container\\ContainerInterface' => $vendorDir . '/predis/predis/src/Command/Container/ContainerInterface.php', 'Predis\\Command\\Container\\FUNCTIONS' => $vendorDir . '/predis/predis/src/Command/Container/FUNCTIONS.php', + 'Predis\\Command\\Container\\HIMPORT' => $vendorDir . '/predis/predis/src/Command/Container/HIMPORT.php', 'Predis\\Command\\Container\\HOTKEYS' => $vendorDir . '/predis/predis/src/Command/Container/HOTKEYS.php', 'Predis\\Command\\Container\\Json\\JSONDEBUG' => $vendorDir . '/predis/predis/src/Command/Container/Json/JSONDEBUG.php', 'Predis\\Command\\Container\\Search\\FTCONFIG' => $vendorDir . '/predis/predis/src/Command/Container/Search/FTCONFIG.php', @@ -3233,6 +3251,7 @@ 'Predis\\Command\\Redis\\BITOP' => $vendorDir . '/predis/predis/src/Command/Redis/BITOP.php', 'Predis\\Command\\Redis\\BITPOS' => $vendorDir . '/predis/predis/src/Command/Redis/BITPOS.php', 'Predis\\Command\\Redis\\BLMOVE' => $vendorDir . '/predis/predis/src/Command/Redis/BLMOVE.php', + 'Predis\\Command\\Redis\\BLMOVEM' => $vendorDir . '/predis/predis/src/Command/Redis/BLMOVEM.php', 'Predis\\Command\\Redis\\BLMPOP' => $vendorDir . '/predis/predis/src/Command/Redis/BLMPOP.php', 'Predis\\Command\\Redis\\BLPOP' => $vendorDir . '/predis/predis/src/Command/Redis/BLPOP.php', 'Predis\\Command\\Redis\\BRPOP' => $vendorDir . '/predis/predis/src/Command/Redis/BRPOP.php', @@ -3319,6 +3338,7 @@ 'Predis\\Command\\Redis\\HGETALL' => $vendorDir . '/predis/predis/src/Command/Redis/HGETALL.php', 'Predis\\Command\\Redis\\HGETDEL' => $vendorDir . '/predis/predis/src/Command/Redis/HGETDEL.php', 'Predis\\Command\\Redis\\HGETEX' => $vendorDir . '/predis/predis/src/Command/Redis/HGETEX.php', + 'Predis\\Command\\Redis\\HIMPORT' => $vendorDir . '/predis/predis/src/Command/Redis/HIMPORT.php', 'Predis\\Command\\Redis\\HINCRBY' => $vendorDir . '/predis/predis/src/Command/Redis/HINCRBY.php', 'Predis\\Command\\Redis\\HINCRBYFLOAT' => $vendorDir . '/predis/predis/src/Command/Redis/HINCRBYFLOAT.php', 'Predis\\Command\\Redis\\HKEYS' => $vendorDir . '/predis/predis/src/Command/Redis/HKEYS.php', @@ -3374,6 +3394,7 @@ 'Predis\\Command\\Redis\\LINSERT' => $vendorDir . '/predis/predis/src/Command/Redis/LINSERT.php', 'Predis\\Command\\Redis\\LLEN' => $vendorDir . '/predis/predis/src/Command/Redis/LLEN.php', 'Predis\\Command\\Redis\\LMOVE' => $vendorDir . '/predis/predis/src/Command/Redis/LMOVE.php', + 'Predis\\Command\\Redis\\LMOVEM' => $vendorDir . '/predis/predis/src/Command/Redis/LMOVEM.php', 'Predis\\Command\\Redis\\LMPOP' => $vendorDir . '/predis/predis/src/Command/Redis/LMPOP.php', 'Predis\\Command\\Redis\\LPOP' => $vendorDir . '/predis/predis/src/Command/Redis/LPOP.php', 'Predis\\Command\\Redis\\LPUSH' => $vendorDir . '/predis/predis/src/Command/Redis/LPUSH.php', @@ -3420,6 +3441,7 @@ 'Predis\\Command\\Redis\\SCARD' => $vendorDir . '/predis/predis/src/Command/Redis/SCARD.php', 'Predis\\Command\\Redis\\SCRIPT' => $vendorDir . '/predis/predis/src/Command/Redis/SCRIPT.php', 'Predis\\Command\\Redis\\SDIFF' => $vendorDir . '/predis/predis/src/Command/Redis/SDIFF.php', + 'Predis\\Command\\Redis\\SDIFFCARD' => $vendorDir . '/predis/predis/src/Command/Redis/SDIFFCARD.php', 'Predis\\Command\\Redis\\SDIFFSTORE' => $vendorDir . '/predis/predis/src/Command/Redis/SDIFFSTORE.php', 'Predis\\Command\\Redis\\SELECT' => $vendorDir . '/predis/predis/src/Command/Redis/SELECT.php', 'Predis\\Command\\Redis\\SENTINEL' => $vendorDir . '/predis/predis/src/Command/Redis/SENTINEL.php', @@ -3450,11 +3472,13 @@ 'Predis\\Command\\Redis\\SUBSCRIBE' => $vendorDir . '/predis/predis/src/Command/Redis/SUBSCRIBE.php', 'Predis\\Command\\Redis\\SUBSTR' => $vendorDir . '/predis/predis/src/Command/Redis/SUBSTR.php', 'Predis\\Command\\Redis\\SUNION' => $vendorDir . '/predis/predis/src/Command/Redis/SUNION.php', + 'Predis\\Command\\Redis\\SUNIONCARD' => $vendorDir . '/predis/predis/src/Command/Redis/SUNIONCARD.php', 'Predis\\Command\\Redis\\SUNIONSTORE' => $vendorDir . '/predis/predis/src/Command/Redis/SUNIONSTORE.php', 'Predis\\Command\\Redis\\SUNSUBSCRIBE' => $vendorDir . '/predis/predis/src/Command/Redis/SUNSUBSCRIBE.php', 'Predis\\Command\\Redis\\Search\\FTAGGREGATE' => $vendorDir . '/predis/predis/src/Command/Redis/Search/FTAGGREGATE.php', 'Predis\\Command\\Redis\\Search\\FTALIASADD' => $vendorDir . '/predis/predis/src/Command/Redis/Search/FTALIASADD.php', 'Predis\\Command\\Redis\\Search\\FTALIASDEL' => $vendorDir . '/predis/predis/src/Command/Redis/Search/FTALIASDEL.php', + 'Predis\\Command\\Redis\\Search\\FTALIASLIST' => $vendorDir . '/predis/predis/src/Command/Redis/Search/FTALIASLIST.php', 'Predis\\Command\\Redis\\Search\\FTALIASUPDATE' => $vendorDir . '/predis/predis/src/Command/Redis/Search/FTALIASUPDATE.php', 'Predis\\Command\\Redis\\Search\\FTALTER' => $vendorDir . '/predis/predis/src/Command/Redis/Search/FTALTER.php', 'Predis\\Command\\Redis\\Search\\FTCONFIG' => $vendorDir . '/predis/predis/src/Command/Redis/Search/FTCONFIG.php', @@ -3510,8 +3534,12 @@ 'Predis\\Command\\Redis\\TimeSeries\\TSMGET' => $vendorDir . '/predis/predis/src/Command/Redis/TimeSeries/TSMGET.php', 'Predis\\Command\\Redis\\TimeSeries\\TSMRANGE' => $vendorDir . '/predis/predis/src/Command/Redis/TimeSeries/TSMRANGE.php', 'Predis\\Command\\Redis\\TimeSeries\\TSMREVRANGE' => $vendorDir . '/predis/predis/src/Command/Redis/TimeSeries/TSMREVRANGE.php', + 'Predis\\Command\\Redis\\TimeSeries\\TSNRANGE' => $vendorDir . '/predis/predis/src/Command/Redis/TimeSeries/TSNRANGE.php', + 'Predis\\Command\\Redis\\TimeSeries\\TSNREVRANGE' => $vendorDir . '/predis/predis/src/Command/Redis/TimeSeries/TSNREVRANGE.php', 'Predis\\Command\\Redis\\TimeSeries\\TSQUERYINDEX' => $vendorDir . '/predis/predis/src/Command/Redis/TimeSeries/TSQUERYINDEX.php', + 'Predis\\Command\\Redis\\TimeSeries\\TSQUERYLABELS' => $vendorDir . '/predis/predis/src/Command/Redis/TimeSeries/TSQUERYLABELS.php', 'Predis\\Command\\Redis\\TimeSeries\\TSRANGE' => $vendorDir . '/predis/predis/src/Command/Redis/TimeSeries/TSRANGE.php', + 'Predis\\Command\\Redis\\TimeSeries\\TSREAD' => $vendorDir . '/predis/predis/src/Command/Redis/TimeSeries/TSREAD.php', 'Predis\\Command\\Redis\\TimeSeries\\TSREVRANGE' => $vendorDir . '/predis/predis/src/Command/Redis/TimeSeries/TSREVRANGE.php', 'Predis\\Command\\Redis\\TopK\\TOPKADD' => $vendorDir . '/predis/predis/src/Command/Redis/TopK/TOPKADD.php', 'Predis\\Command\\Redis\\TopK\\TOPKINCRBY' => $vendorDir . '/predis/predis/src/Command/Redis/TopK/TOPKINCRBY.php', @@ -3555,6 +3583,7 @@ 'Predis\\Command\\Redis\\XREAD' => $vendorDir . '/predis/predis/src/Command/Redis/XREAD.php', 'Predis\\Command\\Redis\\XREADGROUP' => $vendorDir . '/predis/predis/src/Command/Redis/XREADGROUP.php', 'Predis\\Command\\Redis\\XREADGROUP_CLAIM' => $vendorDir . '/predis/predis/src/Command/Redis/XREADGROUP_CLAIM.php', + 'Predis\\Command\\Redis\\XREAD_V4' => $vendorDir . '/predis/predis/src/Command/Redis/XREAD_V4.php', 'Predis\\Command\\Redis\\XREVRANGE' => $vendorDir . '/predis/predis/src/Command/Redis/XREVRANGE.php', 'Predis\\Command\\Redis\\XSETID' => $vendorDir . '/predis/predis/src/Command/Redis/XSETID.php', 'Predis\\Command\\Redis\\XTRIM' => $vendorDir . '/predis/predis/src/Command/Redis/XTRIM.php', @@ -3592,6 +3621,7 @@ 'Predis\\Command\\Redis\\ZUNIONSTORE' => $vendorDir . '/predis/predis/src/Command/Redis/ZUNIONSTORE.php', 'Predis\\Command\\ScriptCommand' => $vendorDir . '/predis/predis/src/Command/ScriptCommand.php', 'Predis\\Command\\Traits\\Aggregate' => $vendorDir . '/predis/predis/src/Command/Traits/Aggregate.php', + 'Predis\\Command\\Traits\\Approx' => $vendorDir . '/predis/predis/src/Command/Traits/Approx.php', 'Predis\\Command\\Traits\\BitByte' => $vendorDir . '/predis/predis/src/Command/Traits/BitByte.php', 'Predis\\Command\\Traits\\BloomFilters\\BucketSize' => $vendorDir . '/predis/predis/src/Command/Traits/BloomFilters/BucketSize.php', 'Predis\\Command\\Traits\\BloomFilters\\Capacity' => $vendorDir . '/predis/predis/src/Command/Traits/BloomFilters/Capacity.php', @@ -3637,6 +3667,7 @@ 'Predis\\Configuration\\Option\\Commands' => $vendorDir . '/predis/predis/src/Configuration/Option/Commands.php', 'Predis\\Configuration\\Option\\Connections' => $vendorDir . '/predis/predis/src/Configuration/Option/Connections.php', 'Predis\\Configuration\\Option\\Exceptions' => $vendorDir . '/predis/predis/src/Configuration/Option/Exceptions.php', + 'Predis\\Configuration\\Option\\Himport' => $vendorDir . '/predis/predis/src/Configuration/Option/Himport.php', 'Predis\\Configuration\\Option\\Prefix' => $vendorDir . '/predis/predis/src/Configuration/Option/Prefix.php', 'Predis\\Configuration\\Option\\Replication' => $vendorDir . '/predis/predis/src/Configuration/Option/Replication.php', 'Predis\\Configuration\\Option\\UpstreamDriver' => $vendorDir . '/predis/predis/src/Configuration/Option/UpstreamDriver.php', @@ -3681,6 +3712,9 @@ 'Predis\\Consumer\\Push\\PushNotificationException' => $vendorDir . '/predis/predis/src/Consumer/Push/PushNotificationException.php', 'Predis\\Consumer\\Push\\PushResponse' => $vendorDir . '/predis/predis/src/Consumer/Push/PushResponse.php', 'Predis\\Consumer\\Push\\PushResponseInterface' => $vendorDir . '/predis/predis/src/Consumer/Push/PushResponseInterface.php', + 'Predis\\Himport\\FieldsetNotPreparedException' => $vendorDir . '/predis/predis/src/Himport/FieldsetNotPreparedException.php', + 'Predis\\Himport\\FieldsetRegistry' => $vendorDir . '/predis/predis/src/Himport/FieldsetRegistry.php', + 'Predis\\Himport\\HimportOptions' => $vendorDir . '/predis/predis/src/Himport/HimportOptions.php', 'Predis\\Monitor\\Consumer' => $vendorDir . '/predis/predis/src/Monitor/Consumer.php', 'Predis\\NotSupportedException' => $vendorDir . '/predis/predis/src/NotSupportedException.php', 'Predis\\Pipeline\\Atomic' => $vendorDir . '/predis/predis/src/Pipeline/Atomic.php', diff --git a/core/vendor/composer/autoload_static.php b/core/vendor/composer/autoload_static.php index d9f790ae66..955f102107 100644 --- a/core/vendor/composer/autoload_static.php +++ b/core/vendor/composer/autoload_static.php @@ -683,6 +683,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 ); public static $classMap = array ( + 'AddCachepwdExpiryToUsers' => __DIR__ . '/../..' . '/database/migrations/2026_08_20_000000_add_cachepwd_expiry_to_users.php', 'BladeUI\\Icons\\BladeIconsServiceProvider' => __DIR__ . '/..' . '/blade-ui-kit/blade-icons/src/BladeIconsServiceProvider.php', 'BladeUI\\Icons\\Components\\Icon' => __DIR__ . '/..' . '/blade-ui-kit/blade-icons/src/Components/Icon.php', 'BladeUI\\Icons\\Components\\Svg' => __DIR__ . '/..' . '/blade-ui-kit/blade-icons/src/Components/Svg.php', @@ -1356,6 +1357,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Doctrine\\DBAL\\Platforms\\Keywords\\DB2Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/DB2Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\KeywordList' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/KeywordList.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDB117Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MariaDB117Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDB123Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MariaDB123Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDBKeywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL80Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MySQL80Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL84Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MySQL84Keywords.php', @@ -1368,6 +1370,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Doctrine\\DBAL\\Platforms\\MariaDB1052Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDB1052Platform.php', 'Doctrine\\DBAL\\Platforms\\MariaDB1060Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDB1060Platform.php', 'Doctrine\\DBAL\\Platforms\\MariaDB110700Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDB110700Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDB120300Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDB120300Platform.php', 'Doctrine\\DBAL\\Platforms\\MariaDBPlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDBPlatform.php', 'Doctrine\\DBAL\\Platforms\\MySQL80Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL80Platform.php', 'Doctrine\\DBAL\\Platforms\\MySQL84Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL84Platform.php', @@ -1791,6 +1794,11 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Egulias\\EmailValidator\\Warning\\Warning' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/Warning.php', 'EvolutionCMS\\AbstractLaravel' => __DIR__ . '/../..' . '/src/AbstractLaravel.php', 'EvolutionCMS\\AliasLoader' => __DIR__ . '/../..' . '/src/AliasLoader.php', + 'EvolutionCMS\\Auth\\Contracts\\LoginPipe' => __DIR__ . '/../..' . '/src/Auth/Contracts/LoginPipe.php', + 'EvolutionCMS\\Auth\\LoginAttempt' => __DIR__ . '/../..' . '/src/Auth/LoginAttempt.php', + 'EvolutionCMS\\Auth\\LoginPipeline' => __DIR__ . '/../..' . '/src/Auth/LoginPipeline.php', + 'EvolutionCMS\\Auth\\PipelineUserManager' => __DIR__ . '/../..' . '/src/Auth/PipelineUserManager.php', + 'EvolutionCMS\\Auth\\Pipes\\EnsureNotThrottled' => __DIR__ . '/../..' . '/src/Auth/Pipes/EnsureNotThrottled.php', 'EvolutionCMS\\Bootstrap\\EnvCacheLoader' => __DIR__ . '/../..' . '/src/Bootstrap/EnvCacheLoader.php', 'EvolutionCMS\\Console' => __DIR__ . '/../..' . '/src/Console.php', 'EvolutionCMS\\Console\\ClearCacheFullCommand' => __DIR__ . '/../..' . '/src/Console/ClearCacheFullCommand.php', @@ -1882,6 +1890,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'EvolutionCMS\\Events\\VendorTagPublished' => __DIR__ . '/../..' . '/src/Events/VendorTagPublished.php', 'EvolutionCMS\\ExceptionHandler' => __DIR__ . '/../..' . '/src/ExceptionHandler.php', 'EvolutionCMS\\Exceptions\\InvalidFieldException' => __DIR__ . '/../..' . '/src/Exceptions/InvalidFieldException.php', + 'EvolutionCMS\\Exceptions\\PasswordRecoveryRequiredException' => __DIR__ . '/../..' . '/src/Exceptions/PasswordRecoveryRequiredException.php', 'EvolutionCMS\\Exceptions\\ServiceActionException' => __DIR__ . '/../..' . '/src/Exceptions/ServiceActionException.php', 'EvolutionCMS\\Exceptions\\ServiceValidationException' => __DIR__ . '/../..' . '/src/Exceptions/ServiceValidationException.php', 'EvolutionCMS\\Exceptions\\TableNotDefinedException' => __DIR__ . '/../..' . '/src/Exceptions/TableNotDefinedException.php', @@ -1923,6 +1932,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'EvolutionCMS\\Interfaces\\PhpCompatInterface' => __DIR__ . '/../..' . '/src/Interfaces/PhpCompatInterface.php', 'EvolutionCMS\\Interfaces\\ServiceProviderInterface' => __DIR__ . '/../..' . '/src/Interfaces/ServiceProviderInterface.php', 'EvolutionCMS\\Interfaces\\TracyPanel' => __DIR__ . '/../..' . '/src/Interfaces/TracyPanel.php', + 'EvolutionCMS\\Interfaces\\UserManagerInterface' => __DIR__ . '/../..' . '/src/Interfaces/UserManagerInterface.php', 'EvolutionCMS\\Legacy\\Cache' => __DIR__ . '/../..' . '/src/Legacy/Cache.php', 'EvolutionCMS\\Legacy\\Categories' => __DIR__ . '/../..' . '/src/Legacy/Categories.php', 'EvolutionCMS\\Legacy\\DeprecatedCore' => __DIR__ . '/../..' . '/src/Legacy/DeprecatedCore.php', @@ -2016,6 +2026,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'EvolutionCMS\\Providers\\PasswordHashServiceProvider' => __DIR__ . '/../..' . '/src/Providers/PasswordHashServiceProvider.php', 'EvolutionCMS\\Providers\\PhpCompatServiceProvider' => __DIR__ . '/../..' . '/src/Providers/PhpCompatServiceProvider.php', 'EvolutionCMS\\Providers\\PhxServiceProvider' => __DIR__ . '/../..' . '/src/Providers/PhxServiceProvider.php', + 'EvolutionCMS\\Providers\\PipelineUserManagerServiceProvider' => __DIR__ . '/../..' . '/src/Providers/PipelineUserManagerServiceProvider.php', 'EvolutionCMS\\Providers\\RoutingServiceProvider' => __DIR__ . '/../..' . '/src/Providers/RoutingServiceProvider.php', 'EvolutionCMS\\Providers\\SessionServiceProvider' => __DIR__ . '/../..' . '/src/Providers/SessionServiceProvider.php', 'EvolutionCMS\\Providers\\SystemTasksServiceProvider' => __DIR__ . '/../..' . '/src/Providers/SystemTasksServiceProvider.php', @@ -2028,6 +2039,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'EvolutionCMS\\Services\\ComposerVersionSynchronizer' => __DIR__ . '/../..' . '/src/Services/ComposerVersionSynchronizer.php', 'EvolutionCMS\\Services\\ConfigService' => __DIR__ . '/../..' . '/src/Services/ConfigService.php', 'EvolutionCMS\\Services\\DatabaseBackupService' => __DIR__ . '/../..' . '/src/Services/DatabaseBackupService.php', + 'EvolutionCMS\\Services\\PasswordRecoveryService' => __DIR__ . '/../..' . '/src/Services/PasswordRecoveryService.php', 'EvolutionCMS\\Services\\Store\\CatalogService' => __DIR__ . '/../..' . '/src/Services/Store/CatalogService.php', 'EvolutionCMS\\Services\\Store\\InstalledStateService' => __DIR__ . '/../..' . '/src/Services/Store/InstalledStateService.php', 'EvolutionCMS\\Services\\Store\\LegacyDeleteService' => __DIR__ . '/../..' . '/src/Services/Store/LegacyDeleteService.php', @@ -2060,6 +2072,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'EvolutionCMS\\Support\\Formatter\\HtmlFormatter' => __DIR__ . '/../..' . '/src/Support/Formatter/HtmlFormatter.php', 'EvolutionCMS\\Support\\Formatter\\SqlFormatter' => __DIR__ . '/../..' . '/src/Support/Formatter/SqlFormatter.php', 'EvolutionCMS\\Support\\MailTestMailer' => __DIR__ . '/../..' . '/src/Support/MailTestMailer.php', + 'EvolutionCMS\\Support\\MailTestSmtp' => __DIR__ . '/../..' . '/src/Support/MailTestSmtp.php', 'EvolutionCMS\\Support\\MakeTable' => __DIR__ . '/../..' . '/src/Support/MakeTable.php', 'EvolutionCMS\\Support\\Menu' => __DIR__ . '/../..' . '/src/Support/Menu.php', 'EvolutionCMS\\Support\\MoveDocumentTargetGuard' => __DIR__ . '/../..' . '/src/Support/MoveDocumentTargetGuard.php', @@ -2158,6 +2171,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'GuzzleHttp\\Handler\\CurlVersion' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/CurlVersion.php', 'GuzzleHttp\\Handler\\EasyHandle' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/EasyHandle.php', 'GuzzleHttp\\Handler\\HeaderProcessor' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php', + 'GuzzleHttp\\Handler\\HostValidator' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/HostValidator.php', 'GuzzleHttp\\Handler\\MockHandler' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/MockHandler.php', 'GuzzleHttp\\Handler\\Proxy' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/Proxy.php', 'GuzzleHttp\\Handler\\ProxyEnvironment' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/ProxyEnvironment.php', @@ -3811,6 +3825,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Predis\\Command\\Argument\\Geospatial\\FromMember' => __DIR__ . '/..' . '/predis/predis/src/Command/Argument/Geospatial/FromMember.php', 'Predis\\Command\\Argument\\Search\\AggregateArguments' => __DIR__ . '/..' . '/predis/predis/src/Command/Argument/Search/AggregateArguments.php', 'Predis\\Command\\Argument\\Search\\AlterArguments' => __DIR__ . '/..' . '/predis/predis/src/Command/Argument/Search/AlterArguments.php', + 'Predis\\Command\\Argument\\Search\\CollectArguments' => __DIR__ . '/..' . '/predis/predis/src/Command/Argument/Search/CollectArguments.php', 'Predis\\Command\\Argument\\Search\\CommonArguments' => __DIR__ . '/..' . '/predis/predis/src/Command/Argument/Search/CommonArguments.php', 'Predis\\Command\\Argument\\Search\\CreateArguments' => __DIR__ . '/..' . '/predis/predis/src/Command/Argument/Search/CreateArguments.php', 'Predis\\Command\\Argument\\Search\\CursorArguments' => __DIR__ . '/..' . '/predis/predis/src/Command/Argument/Search/CursorArguments.php', @@ -3854,7 +3869,9 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Predis\\Command\\Argument\\TimeSeries\\InfoArguments' => __DIR__ . '/..' . '/predis/predis/src/Command/Argument/TimeSeries/InfoArguments.php', 'Predis\\Command\\Argument\\TimeSeries\\MGetArguments' => __DIR__ . '/..' . '/predis/predis/src/Command/Argument/TimeSeries/MGetArguments.php', 'Predis\\Command\\Argument\\TimeSeries\\MRangeArguments' => __DIR__ . '/..' . '/predis/predis/src/Command/Argument/TimeSeries/MRangeArguments.php', + 'Predis\\Command\\Argument\\TimeSeries\\NRangeArguments' => __DIR__ . '/..' . '/predis/predis/src/Command/Argument/TimeSeries/NRangeArguments.php', 'Predis\\Command\\Argument\\TimeSeries\\RangeArguments' => __DIR__ . '/..' . '/predis/predis/src/Command/Argument/TimeSeries/RangeArguments.php', + 'Predis\\Command\\Argument\\TimeSeries\\ReadArguments' => __DIR__ . '/..' . '/predis/predis/src/Command/Argument/TimeSeries/ReadArguments.php', 'Predis\\Command\\Command' => __DIR__ . '/..' . '/predis/predis/src/Command/Command.php', 'Predis\\Command\\CommandInterface' => __DIR__ . '/..' . '/predis/predis/src/Command/CommandInterface.php', 'Predis\\Command\\Container\\ACL' => __DIR__ . '/..' . '/predis/predis/src/Command/Container/ACL.php', @@ -3864,6 +3881,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Predis\\Command\\Container\\ContainerFactory' => __DIR__ . '/..' . '/predis/predis/src/Command/Container/ContainerFactory.php', 'Predis\\Command\\Container\\ContainerInterface' => __DIR__ . '/..' . '/predis/predis/src/Command/Container/ContainerInterface.php', 'Predis\\Command\\Container\\FUNCTIONS' => __DIR__ . '/..' . '/predis/predis/src/Command/Container/FUNCTIONS.php', + 'Predis\\Command\\Container\\HIMPORT' => __DIR__ . '/..' . '/predis/predis/src/Command/Container/HIMPORT.php', 'Predis\\Command\\Container\\HOTKEYS' => __DIR__ . '/..' . '/predis/predis/src/Command/Container/HOTKEYS.php', 'Predis\\Command\\Container\\Json\\JSONDEBUG' => __DIR__ . '/..' . '/predis/predis/src/Command/Container/Json/JSONDEBUG.php', 'Predis\\Command\\Container\\Search\\FTCONFIG' => __DIR__ . '/..' . '/predis/predis/src/Command/Container/Search/FTCONFIG.php', @@ -3910,6 +3928,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Predis\\Command\\Redis\\BITOP' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/BITOP.php', 'Predis\\Command\\Redis\\BITPOS' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/BITPOS.php', 'Predis\\Command\\Redis\\BLMOVE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/BLMOVE.php', + 'Predis\\Command\\Redis\\BLMOVEM' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/BLMOVEM.php', 'Predis\\Command\\Redis\\BLMPOP' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/BLMPOP.php', 'Predis\\Command\\Redis\\BLPOP' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/BLPOP.php', 'Predis\\Command\\Redis\\BRPOP' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/BRPOP.php', @@ -3996,6 +4015,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Predis\\Command\\Redis\\HGETALL' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/HGETALL.php', 'Predis\\Command\\Redis\\HGETDEL' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/HGETDEL.php', 'Predis\\Command\\Redis\\HGETEX' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/HGETEX.php', + 'Predis\\Command\\Redis\\HIMPORT' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/HIMPORT.php', 'Predis\\Command\\Redis\\HINCRBY' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/HINCRBY.php', 'Predis\\Command\\Redis\\HINCRBYFLOAT' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/HINCRBYFLOAT.php', 'Predis\\Command\\Redis\\HKEYS' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/HKEYS.php', @@ -4051,6 +4071,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Predis\\Command\\Redis\\LINSERT' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/LINSERT.php', 'Predis\\Command\\Redis\\LLEN' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/LLEN.php', 'Predis\\Command\\Redis\\LMOVE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/LMOVE.php', + 'Predis\\Command\\Redis\\LMOVEM' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/LMOVEM.php', 'Predis\\Command\\Redis\\LMPOP' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/LMPOP.php', 'Predis\\Command\\Redis\\LPOP' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/LPOP.php', 'Predis\\Command\\Redis\\LPUSH' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/LPUSH.php', @@ -4097,6 +4118,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Predis\\Command\\Redis\\SCARD' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/SCARD.php', 'Predis\\Command\\Redis\\SCRIPT' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/SCRIPT.php', 'Predis\\Command\\Redis\\SDIFF' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/SDIFF.php', + 'Predis\\Command\\Redis\\SDIFFCARD' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/SDIFFCARD.php', 'Predis\\Command\\Redis\\SDIFFSTORE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/SDIFFSTORE.php', 'Predis\\Command\\Redis\\SELECT' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/SELECT.php', 'Predis\\Command\\Redis\\SENTINEL' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/SENTINEL.php', @@ -4127,11 +4149,13 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Predis\\Command\\Redis\\SUBSCRIBE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/SUBSCRIBE.php', 'Predis\\Command\\Redis\\SUBSTR' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/SUBSTR.php', 'Predis\\Command\\Redis\\SUNION' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/SUNION.php', + 'Predis\\Command\\Redis\\SUNIONCARD' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/SUNIONCARD.php', 'Predis\\Command\\Redis\\SUNIONSTORE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/SUNIONSTORE.php', 'Predis\\Command\\Redis\\SUNSUBSCRIBE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/SUNSUBSCRIBE.php', 'Predis\\Command\\Redis\\Search\\FTAGGREGATE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/Search/FTAGGREGATE.php', 'Predis\\Command\\Redis\\Search\\FTALIASADD' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/Search/FTALIASADD.php', 'Predis\\Command\\Redis\\Search\\FTALIASDEL' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/Search/FTALIASDEL.php', + 'Predis\\Command\\Redis\\Search\\FTALIASLIST' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/Search/FTALIASLIST.php', 'Predis\\Command\\Redis\\Search\\FTALIASUPDATE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/Search/FTALIASUPDATE.php', 'Predis\\Command\\Redis\\Search\\FTALTER' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/Search/FTALTER.php', 'Predis\\Command\\Redis\\Search\\FTCONFIG' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/Search/FTCONFIG.php', @@ -4187,8 +4211,12 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Predis\\Command\\Redis\\TimeSeries\\TSMGET' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/TimeSeries/TSMGET.php', 'Predis\\Command\\Redis\\TimeSeries\\TSMRANGE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/TimeSeries/TSMRANGE.php', 'Predis\\Command\\Redis\\TimeSeries\\TSMREVRANGE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/TimeSeries/TSMREVRANGE.php', + 'Predis\\Command\\Redis\\TimeSeries\\TSNRANGE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/TimeSeries/TSNRANGE.php', + 'Predis\\Command\\Redis\\TimeSeries\\TSNREVRANGE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/TimeSeries/TSNREVRANGE.php', 'Predis\\Command\\Redis\\TimeSeries\\TSQUERYINDEX' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/TimeSeries/TSQUERYINDEX.php', + 'Predis\\Command\\Redis\\TimeSeries\\TSQUERYLABELS' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/TimeSeries/TSQUERYLABELS.php', 'Predis\\Command\\Redis\\TimeSeries\\TSRANGE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/TimeSeries/TSRANGE.php', + 'Predis\\Command\\Redis\\TimeSeries\\TSREAD' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/TimeSeries/TSREAD.php', 'Predis\\Command\\Redis\\TimeSeries\\TSREVRANGE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/TimeSeries/TSREVRANGE.php', 'Predis\\Command\\Redis\\TopK\\TOPKADD' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/TopK/TOPKADD.php', 'Predis\\Command\\Redis\\TopK\\TOPKINCRBY' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/TopK/TOPKINCRBY.php', @@ -4232,6 +4260,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Predis\\Command\\Redis\\XREAD' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/XREAD.php', 'Predis\\Command\\Redis\\XREADGROUP' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/XREADGROUP.php', 'Predis\\Command\\Redis\\XREADGROUP_CLAIM' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/XREADGROUP_CLAIM.php', + 'Predis\\Command\\Redis\\XREAD_V4' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/XREAD_V4.php', 'Predis\\Command\\Redis\\XREVRANGE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/XREVRANGE.php', 'Predis\\Command\\Redis\\XSETID' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/XSETID.php', 'Predis\\Command\\Redis\\XTRIM' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/XTRIM.php', @@ -4269,6 +4298,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Predis\\Command\\Redis\\ZUNIONSTORE' => __DIR__ . '/..' . '/predis/predis/src/Command/Redis/ZUNIONSTORE.php', 'Predis\\Command\\ScriptCommand' => __DIR__ . '/..' . '/predis/predis/src/Command/ScriptCommand.php', 'Predis\\Command\\Traits\\Aggregate' => __DIR__ . '/..' . '/predis/predis/src/Command/Traits/Aggregate.php', + 'Predis\\Command\\Traits\\Approx' => __DIR__ . '/..' . '/predis/predis/src/Command/Traits/Approx.php', 'Predis\\Command\\Traits\\BitByte' => __DIR__ . '/..' . '/predis/predis/src/Command/Traits/BitByte.php', 'Predis\\Command\\Traits\\BloomFilters\\BucketSize' => __DIR__ . '/..' . '/predis/predis/src/Command/Traits/BloomFilters/BucketSize.php', 'Predis\\Command\\Traits\\BloomFilters\\Capacity' => __DIR__ . '/..' . '/predis/predis/src/Command/Traits/BloomFilters/Capacity.php', @@ -4314,6 +4344,7 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Predis\\Configuration\\Option\\Commands' => __DIR__ . '/..' . '/predis/predis/src/Configuration/Option/Commands.php', 'Predis\\Configuration\\Option\\Connections' => __DIR__ . '/..' . '/predis/predis/src/Configuration/Option/Connections.php', 'Predis\\Configuration\\Option\\Exceptions' => __DIR__ . '/..' . '/predis/predis/src/Configuration/Option/Exceptions.php', + 'Predis\\Configuration\\Option\\Himport' => __DIR__ . '/..' . '/predis/predis/src/Configuration/Option/Himport.php', 'Predis\\Configuration\\Option\\Prefix' => __DIR__ . '/..' . '/predis/predis/src/Configuration/Option/Prefix.php', 'Predis\\Configuration\\Option\\Replication' => __DIR__ . '/..' . '/predis/predis/src/Configuration/Option/Replication.php', 'Predis\\Configuration\\Option\\UpstreamDriver' => __DIR__ . '/..' . '/predis/predis/src/Configuration/Option/UpstreamDriver.php', @@ -4358,6 +4389,9 @@ class ComposerStaticInit925fea465a58fa69f06ccf2629003e87 'Predis\\Consumer\\Push\\PushNotificationException' => __DIR__ . '/..' . '/predis/predis/src/Consumer/Push/PushNotificationException.php', 'Predis\\Consumer\\Push\\PushResponse' => __DIR__ . '/..' . '/predis/predis/src/Consumer/Push/PushResponse.php', 'Predis\\Consumer\\Push\\PushResponseInterface' => __DIR__ . '/..' . '/predis/predis/src/Consumer/Push/PushResponseInterface.php', + 'Predis\\Himport\\FieldsetNotPreparedException' => __DIR__ . '/..' . '/predis/predis/src/Himport/FieldsetNotPreparedException.php', + 'Predis\\Himport\\FieldsetRegistry' => __DIR__ . '/..' . '/predis/predis/src/Himport/FieldsetRegistry.php', + 'Predis\\Himport\\HimportOptions' => __DIR__ . '/..' . '/predis/predis/src/Himport/HimportOptions.php', 'Predis\\Monitor\\Consumer' => __DIR__ . '/..' . '/predis/predis/src/Monitor/Consumer.php', 'Predis\\NotSupportedException' => __DIR__ . '/..' . '/predis/predis/src/NotSupportedException.php', 'Predis\\Pipeline\\Atomic' => __DIR__ . '/..' . '/predis/predis/src/Pipeline/Atomic.php', diff --git a/core/vendor/composer/installed.json b/core/vendor/composer/installed.json index 35e86b0de4..4f3c68b2ed 100644 --- a/core/vendor/composer/installed.json +++ b/core/vendor/composer/installed.json @@ -898,17 +898,17 @@ }, { "name": "doctrine/dbal", - "version": "4.4.3", - "version_normalized": "4.4.3.0", + "version": "4.4.4", + "version_normalized": "4.4.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "61e730f1658814821a85f2402c945f3883407dec" + "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/61e730f1658814821a85f2402c945f3883407dec", - "reference": "61e730f1658814821a85f2402c945f3883407dec", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce", + "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce", "shasum": "" }, "require": { @@ -933,7 +933,7 @@ "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." }, - "time": "2026-03-20T08:52:12+00:00", + "time": "2026-07-21T14:34:40+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -987,7 +987,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.4.3" + "source": "https://github.com/doctrine/dbal/tree/4.4.4" }, "funding": [ { @@ -1585,22 +1585,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.15.1", - "version_normalized": "7.15.1.0", + "version": "7.15.3", + "version_normalized": "7.15.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "61443dfb33c62f308ee8add20f45b4d6e4bf8d2f" + "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/61443dfb33c62f308ee8add20f45b4d6e4bf8d2f", - "reference": "61443dfb33c62f308ee8add20f45b4d6e4bf8d2f", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc", + "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.5.1", + "guzzlehttp/promises": "^2.5.2", "guzzlehttp/psr7": "^2.13", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", @@ -1624,7 +1624,7 @@ "ext-intl": "Required for Internationalized Domain Name (IDN) support", "psr/log": "Required for using the Log middleware" }, - "time": "2026-07-18T11:23:11+00:00", + "time": "2026-08-05T19:48:21+00:00", "type": "library", "extra": { "bamarni-bin": { @@ -1696,7 +1696,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.15.1" + "source": "https://github.com/guzzle/guzzle/tree/7.15.3" }, "funding": [ { @@ -1716,17 +1716,17 @@ }, { "name": "guzzlehttp/promises", - "version": "2.5.1", - "version_normalized": "2.5.1.0", + "version": "2.5.2", + "version_normalized": "2.5.2.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29" + "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/9ad1e4fc607446a055b95870c7f668e93b5cff29", - "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29", + "url": "https://api.github.com/repos/guzzle/promises/zipball/2823687acff28b2dbe67b2508a6b300e2c3fa4ce", + "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce", "shasum": "" }, "require": { @@ -1737,7 +1737,7 @@ "bamarni/composer-bin-plugin": "^1.8.2", "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, - "time": "2026-07-08T15:48:39+00:00", + "time": "2026-08-05T19:30:54+00:00", "type": "library", "extra": { "bamarni-bin": { @@ -1783,7 +1783,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.5.1" + "source": "https://github.com/guzzle/promises/tree/2.5.2" }, "funding": [ { @@ -2014,8 +2014,8 @@ }, { "name": "illuminate/bus", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/bus.git", @@ -2070,8 +2070,8 @@ }, { "name": "illuminate/cache", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/cache.git", @@ -2135,8 +2135,8 @@ }, { "name": "illuminate/collections", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", @@ -2198,8 +2198,8 @@ }, { "name": "illuminate/conditionable", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", @@ -2247,8 +2247,8 @@ }, { "name": "illuminate/config", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/config.git", @@ -2298,17 +2298,17 @@ }, { "name": "illuminate/console", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/console.git", - "reference": "91732d7ae739c86dd9055c4dc850c2b4efccb47f" + "reference": "ce37b63fb90f036dbd34263d84725f79c6d6f221" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/console/zipball/91732d7ae739c86dd9055c4dc850c2b4efccb47f", - "reference": "91732d7ae739c86dd9055c4dc850c2b4efccb47f", + "url": "https://api.github.com/repos/illuminate/console/zipball/ce37b63fb90f036dbd34263d84725f79c6d6f221", + "reference": "ce37b63fb90f036dbd34263d84725f79c6d6f221", "shasum": "" }, "require": { @@ -2334,7 +2334,7 @@ "illuminate/filesystem": "Required to use the generator command (^12.0).", "illuminate/queue": "Required to use closures for scheduled jobs (^12.0)." }, - "time": "2026-03-21T11:33:57+00:00", + "time": "2026-07-29T14:35:23+00:00", "type": "library", "extra": { "branch-alias": { @@ -2367,8 +2367,8 @@ }, { "name": "illuminate/container", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/container.git", @@ -2432,8 +2432,8 @@ }, { "name": "illuminate/contracts", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", @@ -2483,17 +2483,17 @@ }, { "name": "illuminate/database", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/database.git", - "reference": "13ea191cbbee8cc615188c990ec26428df89fd29" + "reference": "6d539459fb55732a13e7f962afeffa9dca2a1cce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/database/zipball/13ea191cbbee8cc615188c990ec26428df89fd29", - "reference": "13ea191cbbee8cc615188c990ec26428df89fd29", + "url": "https://api.github.com/repos/illuminate/database/zipball/6d539459fb55732a13e7f962afeffa9dca2a1cce", + "reference": "6d539459fb55732a13e7f962afeffa9dca2a1cce", "shasum": "" }, "require": { @@ -2520,7 +2520,7 @@ "illuminate/pagination": "Required to paginate the result set (^12.0).", "symfony/finder": "Required to use Eloquent model factories (^7.2)." }, - "time": "2026-06-19T13:46:51+00:00", + "time": "2026-08-14T15:00:48+00:00", "type": "library", "extra": { "branch-alias": { @@ -2559,8 +2559,8 @@ }, { "name": "illuminate/events", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/events.git", @@ -2617,17 +2617,17 @@ }, { "name": "illuminate/filesystem", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/filesystem.git", - "reference": "3b5ad9b385595d735689ebc2bfe701567cc4f1c3" + "reference": "dc0b7440f12753060b574fa8c20c2db259fa37c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/filesystem/zipball/3b5ad9b385595d735689ebc2bfe701567cc4f1c3", - "reference": "3b5ad9b385595d735689ebc2bfe701567cc4f1c3", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/dc0b7440f12753060b574fa8c20c2db259fa37c9", + "reference": "dc0b7440f12753060b574fa8c20c2db259fa37c9", "shasum": "" }, "require": { @@ -2651,7 +2651,7 @@ "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", "symfony/mime": "Required to enable support for guessing extensions (^7.2)." }, - "time": "2026-06-02T13:54:37+00:00", + "time": "2026-08-12T13:55:25+00:00", "type": "library", "extra": { "branch-alias": { @@ -2687,17 +2687,17 @@ }, { "name": "illuminate/http", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/http.git", - "reference": "15f6ff6d3cc237500575827c3d106c63ceb48984" + "reference": "5f4f82ed0ed550a01f5d95c4ddbbad409fb0d560" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/http/zipball/15f6ff6d3cc237500575827c3d106c63ceb48984", - "reference": "15f6ff6d3cc237500575827c3d106c63ceb48984", + "url": "https://api.github.com/repos/illuminate/http/zipball/5f4f82ed0ed550a01f5d95c4ddbbad409fb0d560", + "reference": "5f4f82ed0ed550a01f5d95c4ddbbad409fb0d560", "shasum": "" }, "require": { @@ -2719,7 +2719,7 @@ "suggest": { "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image()." }, - "time": "2026-05-27T23:28:12+00:00", + "time": "2026-08-06T14:28:07+00:00", "type": "library", "extra": { "branch-alias": { @@ -2752,8 +2752,8 @@ }, { "name": "illuminate/log", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/log.git", @@ -2808,8 +2808,8 @@ }, { "name": "illuminate/macroable", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -2857,8 +2857,8 @@ }, { "name": "illuminate/pagination", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/pagination.git", @@ -2910,8 +2910,8 @@ }, { "name": "illuminate/pipeline", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/pipeline.git", @@ -2965,8 +2965,8 @@ }, { "name": "illuminate/queue", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/queue.git", @@ -3035,8 +3035,8 @@ }, { "name": "illuminate/redis", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/redis.git", @@ -3092,8 +3092,8 @@ }, { "name": "illuminate/reflection", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/reflection.git", @@ -3146,17 +3146,17 @@ }, { "name": "illuminate/routing", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/routing.git", - "reference": "f6c107d75cefff875aff53e0aa6a32c2341432a9" + "reference": "5d3cc9beca82c359f16e6c1d072903087e428ca1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/routing/zipball/f6c107d75cefff875aff53e0aa6a32c2341432a9", - "reference": "f6c107d75cefff875aff53e0aa6a32c2341432a9", + "url": "https://api.github.com/repos/illuminate/routing/zipball/5d3cc9beca82c359f16e6c1d072903087e428ca1", + "reference": "5d3cc9beca82c359f16e6c1d072903087e428ca1", "shasum": "" }, "require": { @@ -3182,7 +3182,7 @@ "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." }, - "time": "2026-05-05T20:47:39+00:00", + "time": "2026-08-05T14:58:10+00:00", "type": "library", "extra": { "branch-alias": { @@ -3215,8 +3215,8 @@ }, { "name": "illuminate/session", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/session.git", @@ -3275,17 +3275,17 @@ }, { "name": "illuminate/support", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "bc7bc2db3e635697f81ea123511ae25b014a63fb" + "reference": "7d7a7d2dc5084aa4b1fd8b0a42094e1d3fb51e00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/bc7bc2db3e635697f81ea123511ae25b014a63fb", - "reference": "bc7bc2db3e635697f81ea123511ae25b014a63fb", + "url": "https://api.github.com/repos/illuminate/support/zipball/7d7a7d2dc5084aa4b1fd8b0a42094e1d3fb51e00", + "reference": "7d7a7d2dc5084aa4b1fd8b0a42094e1d3fb51e00", "shasum": "" }, "require": { @@ -3321,7 +3321,7 @@ "symfony/var-dumper": "Required to use the dd function (^7.2).", "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.6.1)." }, - "time": "2026-07-13T16:08:36+00:00", + "time": "2026-08-06T14:28:07+00:00", "type": "library", "extra": { "branch-alias": { @@ -3358,8 +3358,8 @@ }, { "name": "illuminate/translation", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/translation.git", @@ -3412,17 +3412,17 @@ }, { "name": "illuminate/validation", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/validation.git", - "reference": "297cff9c69885b1a9125ee4f26f77ef570abb2b6" + "reference": "ce2bb8d019e2510d1892b0c87f9086ea802c28e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/validation/zipball/297cff9c69885b1a9125ee4f26f77ef570abb2b6", - "reference": "297cff9c69885b1a9125ee4f26f77ef570abb2b6", + "url": "https://api.github.com/repos/illuminate/validation/zipball/ce2bb8d019e2510d1892b0c87f9086ea802c28e4", + "reference": "ce2bb8d019e2510d1892b0c87f9086ea802c28e4", "shasum": "" }, "require": { @@ -3445,7 +3445,7 @@ "illuminate/database": "Required to use the database presence verifier (^12.0).", "ramsey/uuid": "Required to use Validator::validateUuid() (^4.7)." }, - "time": "2026-05-17T14:42:21+00:00", + "time": "2026-08-18T12:21:48+00:00", "type": "library", "extra": { "branch-alias": { @@ -3478,8 +3478,8 @@ }, { "name": "illuminate/view", - "version": "v12.64.0", - "version_normalized": "12.64.0.0", + "version": "v12.67.0", + "version_normalized": "12.67.0.0", "source": { "type": "git", "url": "https://github.com/illuminate/view.git", @@ -3673,17 +3673,17 @@ }, { "name": "laravel/prompts", - "version": "v0.3.21", - "version_normalized": "0.3.21.0", + "version": "v0.3.23", + "version_normalized": "0.3.23.0", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "7753c65c281c2550c7c183f14e18062073b7d821" + "reference": "b7b4c35e5bc47450f6b6238c6cc9c47ba19b2221" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/7753c65c281c2550c7c183f14e18062073b7d821", - "reference": "7753c65c281c2550c7c183f14e18062073b7d821", + "url": "https://api.github.com/repos/laravel/prompts/zipball/b7b4c35e5bc47450f6b6238c6cc9c47ba19b2221", + "reference": "b7b4c35e5bc47450f6b6238c6cc9c47ba19b2221", "shasum": "" }, "require": { @@ -3706,7 +3706,7 @@ "suggest": { "ext-pcntl": "Required for the spinner to be animated." }, - "time": "2026-06-26T00:11:25+00:00", + "time": "2026-08-11T18:58:24+00:00", "type": "library", "extra": { "branch-alias": { @@ -3729,23 +3729,23 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.21" + "source": "https://github.com/laravel/prompts/tree/v0.3.23" }, "install-path": "../laravel/prompts" }, { "name": "laravel/serializable-closure", - "version": "v2.0.13", - "version_normalized": "2.0.13.0", + "version": "v2.0.15", + "version_normalized": "2.0.15.0", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", - "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/dccd8bcb851bb03fcc005df650b708b57cc52661", + "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661", "shasum": "" }, "require": { @@ -3758,7 +3758,7 @@ "phpstan/phpstan": "^2.0", "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" }, - "time": "2026-04-16T14:03:50+00:00", + "time": "2026-07-21T16:49:22+00:00", "type": "library", "extra": { "branch-alias": { @@ -4178,17 +4178,17 @@ }, { "name": "nesbot/carbon", - "version": "3.13.1", - "version_normalized": "3.13.1.0", + "version": "3.13.2", + "version_normalized": "3.13.2.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "2937ad3d1d2c506fd2bc97d571438a95641f44e2" + "reference": "a1c54919f5fff9800cd03c32bd01defd5a4061cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/2937ad3d1d2c506fd2bc97d571438a95641f44e2", - "reference": "2937ad3d1d2c506fd2bc97d571438a95641f44e2", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/a1c54919f5fff9800cd03c32bd01defd5a4061cb", + "reference": "a1c54919f5fff9800cd03c32bd01defd5a4061cb", "shasum": "" }, "require": { @@ -4214,7 +4214,7 @@ "phpunit/phpunit": "^10.5.53", "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" }, - "time": "2026-07-09T18:23:49+00:00", + "time": "2026-08-08T11:40:35+00:00", "bin": [ "bin/carbon" ], @@ -4539,17 +4539,17 @@ }, { "name": "predis/predis", - "version": "v3.5.1", - "version_normalized": "3.5.1.0", + "version": "v3.6.0", + "version_normalized": "3.6.0.0", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "5c996db191ee2d9bafe651f454b1fca16754271b" + "reference": "2ff20c08bb63697245ffee3f198d1673086c302d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/5c996db191ee2d9bafe651f454b1fca16754271b", - "reference": "5c996db191ee2d9bafe651f454b1fca16754271b", + "url": "https://api.github.com/repos/predis/predis/zipball/2ff20c08bb63697245ffee3f198d1673086c302d", + "reference": "2ff20c08bb63697245ffee3f198d1673086c302d", "shasum": "" }, "require": { @@ -4565,7 +4565,7 @@ "suggest": { "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" }, - "time": "2026-06-11T16:56:53+00:00", + "time": "2026-08-14T23:07:56+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -4593,7 +4593,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v3.5.1" + "source": "https://github.com/predis/predis/tree/v3.6.0" }, "funding": [ { @@ -5861,17 +5861,17 @@ }, { "name": "secondnetwork/blade-tabler-icons", - "version": "v3.44.0", - "version_normalized": "3.44.0.0", + "version": "v3.46.0", + "version_normalized": "3.46.0.0", "source": { "type": "git", "url": "https://github.com/secondnetwork/blade-tabler-icons.git", - "reference": "856ad75e2c0704096bf4a708b6464620e41d1a34" + "reference": "aedaebc18f382a9d0a1870e9ad394c5c89abd72b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/secondnetwork/blade-tabler-icons/zipball/856ad75e2c0704096bf4a708b6464620e41d1a34", - "reference": "856ad75e2c0704096bf4a708b6464620e41d1a34", + "url": "https://api.github.com/repos/secondnetwork/blade-tabler-icons/zipball/aedaebc18f382a9d0a1870e9ad394c5c89abd72b", + "reference": "aedaebc18f382a9d0a1870e9ad394c5c89abd72b", "shasum": "" }, "require": { @@ -5883,7 +5883,7 @@ "orchestra/testbench": "^6.0|^7.0|^9.0|^10.0", "phpunit/phpunit": "^9.0|^10.0|^11.0|^12.0" }, - "time": "2026-05-11T14:36:11+00:00", + "time": "2026-07-30T13:49:44+00:00", "type": "library", "extra": { "laravel": { @@ -5916,7 +5916,7 @@ ], "support": { "issues": "https://github.com/secondnetwork/blade-tabler-icons/issues", - "source": "https://github.com/secondnetwork/blade-tabler-icons/tree/v3.44.0" + "source": "https://github.com/secondnetwork/blade-tabler-icons/tree/v3.46.0" }, "install-path": "../secondnetwork/blade-tabler-icons" }, @@ -5989,23 +5989,23 @@ }, { "name": "seld/phar-utils", - "version": "1.2.1", - "version_normalized": "1.2.1.0", + "version": "1.2.2", + "version_normalized": "1.2.2.0", "source": { "type": "git", "url": "https://github.com/Seldaek/phar-utils.git", - "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c" + "reference": "990bbd0e92caa216d52eca0935f6e35e589bfaa5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", - "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/990bbd0e92caa216d52eca0935f6e35e589bfaa5", + "reference": "990bbd0e92caa216d52eca0935f6e35e589bfaa5", "shasum": "" }, "require": { "php": ">=5.3" }, - "time": "2022-08-31T10:31:18+00:00", + "time": "2026-08-01T12:48:55+00:00", "type": "library", "extra": { "branch-alias": { @@ -6034,7 +6034,7 @@ ], "support": { "issues": "https://github.com/Seldaek/phar-utils/issues", - "source": "https://github.com/Seldaek/phar-utils/tree/1.2.1" + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.2" }, "install-path": "../seld/phar-utils" }, @@ -6269,17 +6269,17 @@ }, { "name": "symfony/console", - "version": "v7.4.14", - "version_normalized": "7.4.14.0", + "version": "v7.4.16", + "version_normalized": "7.4.16.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87" + "reference": "f4c69c9aed03abf933b294257d618bdd9b30a06d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87", - "reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87", + "url": "https://api.github.com/repos/symfony/console/zipball/f4c69c9aed03abf933b294257d618bdd9b30a06d", + "reference": "f4c69c9aed03abf933b294257d618bdd9b30a06d", "shasum": "" }, "require": { @@ -6312,7 +6312,7 @@ "symfony/stopwatch": "^6.4|^7.0|^8.0", "symfony/var-dumper": "^6.4|^7.0|^8.0" }, - "time": "2026-06-16T11:50:14+00:00", + "time": "2026-07-31T12:37:14+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -6346,7 +6346,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.14" + "source": "https://github.com/symfony/console/tree/v7.4.16" }, "funding": [ { @@ -6444,17 +6444,17 @@ }, { "name": "symfony/error-handler", - "version": "v7.4.14", - "version_normalized": "7.4.14.0", + "version": "v7.4.15", + "version_normalized": "7.4.15.0", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "4e1a093b481f323e6e326451f9760c3868430673" + "reference": "d49f6a19f326db41ae7103bdc38e3eb35a791261" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/4e1a093b481f323e6e326451f9760c3868430673", - "reference": "4e1a093b481f323e6e326451f9760c3868430673", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/d49f6a19f326db41ae7103bdc38e3eb35a791261", + "reference": "d49f6a19f326db41ae7103bdc38e3eb35a791261", "shasum": "" }, "require": { @@ -6474,7 +6474,7 @@ "symfony/serializer": "^6.4|^7.0|^8.0", "symfony/webpack-encore-bundle": "^1.0|^2.0" }, - "time": "2026-06-05T06:22:21+00:00", + "time": "2026-07-21T15:13:06+00:00", "bin": [ "Resources/bin/patch-type-declarations" ], @@ -6505,7 +6505,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.4.14" + "source": "https://github.com/symfony/error-handler/tree/v7.4.15" }, "funding": [ { @@ -6529,17 +6529,17 @@ }, { "name": "symfony/event-dispatcher", - "version": "v7.4.14", - "version_normalized": "7.4.14.0", + "version": "v7.4.15", + "version_normalized": "7.4.15.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "51fe3d170227be8d1772214b82ae506e15ed78ff" + "reference": "336e7f3b9e95aba04f93ea9143920c2186abfbb9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/51fe3d170227be8d1772214b82ae506e15ed78ff", - "reference": "51fe3d170227be8d1772214b82ae506e15ed78ff", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/336e7f3b9e95aba04f93ea9143920c2186abfbb9", + "reference": "336e7f3b9e95aba04f93ea9143920c2186abfbb9", "shasum": "" }, "require": { @@ -6565,7 +6565,7 @@ "symfony/service-contracts": "^2.5|^3", "symfony/stopwatch": "^6.4|^7.0|^8.0" }, - "time": "2026-06-06T11:10:32+00:00", + "time": "2026-07-21T15:13:06+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -6593,7 +6593,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.14" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.15" }, "funding": [ { @@ -6700,17 +6700,17 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.11", - "version_normalized": "7.4.11.0", + "version": "v7.4.15", + "version_normalized": "7.4.15.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" + "reference": "ff16a16bf87fdf264638b8f6995b3515975e3c79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", - "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/ff16a16bf87fdf264638b8f6995b3515975e3c79", + "reference": "ff16a16bf87fdf264638b8f6995b3515975e3c79", "shasum": "" }, "require": { @@ -6721,7 +6721,7 @@ "require-dev": { "symfony/process": "^6.4|^7.0|^8.0" }, - "time": "2026-05-11T16:38:44+00:00", + "time": "2026-07-22T07:36:05+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -6749,7 +6749,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.11" + "source": "https://github.com/symfony/filesystem/tree/v7.4.15" }, "funding": [ { @@ -6844,17 +6844,17 @@ }, { "name": "symfony/http-foundation", - "version": "v7.4.14", - "version_normalized": "7.4.14.0", + "version": "v7.4.16", + "version_normalized": "7.4.16.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "06db5ae1552177bf8572f8908839f12e3c06aed3" + "reference": "b676451bb638e99a7d34d8a2be90406822e301eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/06db5ae1552177bf8572f8908839f12e3c06aed3", - "reference": "06db5ae1552177bf8572f8908839f12e3c06aed3", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b676451bb638e99a7d34d8a2be90406822e301eb", + "reference": "b676451bb638e99a7d34d8a2be90406822e301eb", "shasum": "" }, "require": { @@ -6877,7 +6877,7 @@ "symfony/mime": "^6.4|^7.0|^8.0", "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, - "time": "2026-06-11T07:31:44+00:00", + "time": "2026-08-07T11:50:27+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -6905,7 +6905,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.14" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.16" }, "funding": [ { @@ -6929,17 +6929,17 @@ }, { "name": "symfony/http-kernel", - "version": "v7.4.14", - "version_normalized": "7.4.14.0", + "version": "v7.4.16", + "version_normalized": "7.4.16.0", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "e99af79b1e776646eda0e1c23b7b45c184ff99be" + "reference": "f5e728670fa2218ae8be8ea91f2b44b7d6e5304c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/e99af79b1e776646eda0e1c23b7b45c184ff99be", - "reference": "e99af79b1e776646eda0e1c23b7b45c184ff99be", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f5e728670fa2218ae8be8ea91f2b44b7d6e5304c", + "reference": "f5e728670fa2218ae8be8ea91f2b44b7d6e5304c", "shasum": "" }, "require": { @@ -6997,9 +6997,9 @@ "symfony/validator": "^6.4|^7.0|^8.0", "symfony/var-dumper": "^6.4|^7.0|^8.0", "symfony/var-exporter": "^6.4|^7.0|^8.0", - "twig/twig": "^3.12" + "twig/twig": "^3.12|^4.0" }, - "time": "2026-06-27T09:14:35+00:00", + "time": "2026-08-07T18:00:13+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -7027,7 +7027,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.14" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.16" }, "funding": [ { @@ -7051,17 +7051,17 @@ }, { "name": "symfony/mime", - "version": "v7.4.13", - "version_normalized": "7.4.13.0", + "version": "v7.4.16", + "version_normalized": "7.4.16.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "a845722765c4f6b2ce88beaf4f4479975b186770" + "reference": "20094b76a7106dbe978d31cd3bd7aa1ed248d0e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/a845722765c4f6b2ce88beaf4f4479975b186770", - "reference": "a845722765c4f6b2ce88beaf4f4479975b186770", + "url": "https://api.github.com/repos/symfony/mime/zipball/20094b76a7106dbe978d31cd3bd7aa1ed248d0e5", + "reference": "20094b76a7106dbe978d31cd3bd7aa1ed248d0e5", "shasum": "" }, "require": { @@ -7087,7 +7087,7 @@ "symfony/property-info": "^6.4|^7.0|^8.0", "symfony/serializer": "^6.4.3|^7.0.3|^8.0" }, - "time": "2026-05-23T16:22:37+00:00", + "time": "2026-08-07T14:56:57+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -7119,7 +7119,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.13" + "source": "https://github.com/symfony/mime/tree/v7.4.16" }, "funding": [ { @@ -7229,17 +7229,17 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.38.1", - "version_normalized": "1.38.1.0", + "version": "v1.41.0", + "version_normalized": "1.41.0.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", "shasum": "" }, "require": { @@ -7248,7 +7248,7 @@ "suggest": { "ext-intl": "For best performance" }, - "time": "2026-05-26T05:58:03+00:00", + "time": "2026-07-28T08:25:59+00:00", "type": "library", "extra": { "thanks": { @@ -7290,7 +7290,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" }, "funding": [ { @@ -7663,23 +7663,23 @@ }, { "name": "symfony/polyfill-php85", - "version": "v1.38.1", - "version_normalized": "1.38.1.0", + "version": "v1.41.0", + "version_normalized": "1.41.0.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", "shasum": "" }, "require": { "php": ">=7.2" }, - "time": "2026-05-26T02:25:22+00:00", + "time": "2026-07-01T12:47:55+00:00", "type": "library", "extra": { "thanks": { @@ -7722,7 +7722,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" }, "funding": [ { @@ -7814,17 +7814,17 @@ }, { "name": "symfony/routing", - "version": "v7.4.13", - "version_normalized": "7.4.13.0", + "version": "v7.4.15", + "version_normalized": "7.4.15.0", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d" + "reference": "80c0a93d3f8e7499f716204a1fb38ead942a7a2b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/3a162171bb008e5e0f15dce6581373a4c0e8390d", - "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d", + "url": "https://api.github.com/repos/symfony/routing/zipball/80c0a93d3f8e7499f716204a1fb38ead942a7a2b", + "reference": "80c0a93d3f8e7499f716204a1fb38ead942a7a2b", "shasum": "" }, "require": { @@ -7844,7 +7844,7 @@ "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/yaml": "^6.4|^7.0|^8.0" }, - "time": "2026-05-24T11:20:33+00:00", + "time": "2026-07-21T15:13:06+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -7878,7 +7878,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.13" + "source": "https://github.com/symfony/routing/tree/v7.4.15" }, "funding": [ { @@ -7992,17 +7992,17 @@ }, { "name": "symfony/string", - "version": "v7.4.13", - "version_normalized": "7.4.13.0", + "version": "v7.4.15", + "version_normalized": "7.4.15.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde" + "reference": "e394af32256bf9e7bf80849d95e589167c10097b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", - "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "url": "https://api.github.com/repos/symfony/string/zipball/e394af32256bf9e7bf80849d95e589167c10097b", + "reference": "e394af32256bf9e7bf80849d95e589167c10097b", "shasum": "" }, "require": { @@ -8023,7 +8023,7 @@ "symfony/translation-contracts": "^2.5|^3.0", "symfony/var-exporter": "^6.4|^7.0|^8.0" }, - "time": "2026-05-23T15:23:29+00:00", + "time": "2026-07-28T07:33:02+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -8062,7 +8062,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.13" + "source": "https://github.com/symfony/string/tree/v7.4.15" }, "funding": [ { @@ -8086,17 +8086,17 @@ }, { "name": "symfony/translation", - "version": "v7.4.14", - "version_normalized": "7.4.14.0", + "version": "v7.4.16", + "version_normalized": "7.4.16.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "a1af4dacb24eb7ef4f1ca71b94da8ddbce572281" + "reference": "501e0ff4553c744209ca1a68790d8a4541563710" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/a1af4dacb24eb7ef4f1ca71b94da8ddbce572281", - "reference": "a1af4dacb24eb7ef4f1ca71b94da8ddbce572281", + "url": "https://api.github.com/repos/symfony/translation/zipball/501e0ff4553c744209ca1a68790d8a4541563710", + "reference": "501e0ff4553c744209ca1a68790d8a4541563710", "shasum": "" }, "require": { @@ -8134,7 +8134,7 @@ "symfony/service-contracts": "^2.5|^3", "symfony/yaml": "^6.4|^7.0|^8.0" }, - "time": "2026-06-06T09:33:19+00:00", + "time": "2026-07-30T12:37:26+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -8165,7 +8165,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.4.14" + "source": "https://github.com/symfony/translation/tree/v7.4.16" }, "funding": [ { @@ -8274,17 +8274,17 @@ }, { "name": "symfony/var-dumper", - "version": "v7.4.14", - "version_normalized": "7.4.14.0", + "version": "v7.4.15", + "version_normalized": "7.4.15.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358" + "reference": "04ba4add636a95ff437af3a5a9499bb1d6c6d4bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358", - "reference": "9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/04ba4add636a95ff437af3a5a9499bb1d6c6d4bd", + "reference": "04ba4add636a95ff437af3a5a9499bb1d6c6d4bd", "shasum": "" }, "require": { @@ -8300,9 +8300,9 @@ "symfony/http-kernel": "^6.4|^7.0|^8.0", "symfony/process": "^6.4|^7.0|^8.0", "symfony/uid": "^6.4|^7.0|^8.0", - "twig/twig": "^3.12" + "twig/twig": "^3.12|^4.0" }, - "time": "2026-06-08T20:24:16+00:00", + "time": "2026-07-21T15:13:06+00:00", "bin": [ "Resources/bin/var-dump-server" ], @@ -8340,7 +8340,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.14" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.15" }, "funding": [ { @@ -8364,17 +8364,17 @@ }, { "name": "tracy/tracy", - "version": "v2.12.0", - "version_normalized": "2.12.0.0", + "version": "v2.12.1", + "version_normalized": "2.12.1.0", "source": { "type": "git", "url": "https://github.com/nette/tracy.git", - "reference": "535eda4871fd04f2756c8d95f6bdfa43706d79be" + "reference": "90d24872cd97202f79281aac7eb09c27ec48bf69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/tracy/zipball/535eda4871fd04f2756c8d95f6bdfa43706d79be", - "reference": "535eda4871fd04f2756c8d95f6bdfa43706d79be", + "url": "https://api.github.com/repos/nette/tracy/zipball/90d24872cd97202f79281aac7eb09c27ec48bf69", + "reference": "90d24872cd97202f79281aac7eb09c27ec48bf69", "shasum": "" }, "require": { @@ -8397,7 +8397,7 @@ "phpstan/phpstan": "^2.1@stable", "psr/log": "^1.0 || ^2.0 || ^3.0" }, - "time": "2026-05-04T00:23:53+00:00", + "time": "2026-08-18T10:56:11+00:00", "type": "library", "extra": { "branch-alias": { @@ -8441,7 +8441,7 @@ ], "support": { "issues": "https://github.com/nette/tracy/issues", - "source": "https://github.com/nette/tracy/tree/v2.12.0" + "source": "https://github.com/nette/tracy/tree/v2.12.1" }, "install-path": "../tracy/tracy" }, diff --git a/core/vendor/composer/installed.php b/core/vendor/composer/installed.php index ba7a74c149..86abca575e 100644 --- a/core/vendor/composer/installed.php +++ b/core/vendor/composer/installed.php @@ -119,9 +119,9 @@ 'dev_requirement' => false, ), 'doctrine/dbal' => array( - 'pretty_version' => '4.4.3', - 'version' => '4.4.3.0', - 'reference' => '61e730f1658814821a85f2402c945f3883407dec', + 'pretty_version' => '4.4.4', + 'version' => '4.4.4.0', + 'reference' => 'fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce', 'type' => 'library', 'install_path' => __DIR__ . '/../doctrine/dbal', 'aliases' => array(), @@ -218,18 +218,18 @@ 'dev_requirement' => false, ), 'guzzlehttp/guzzle' => array( - 'pretty_version' => '7.15.1', - 'version' => '7.15.1.0', - 'reference' => '61443dfb33c62f308ee8add20f45b4d6e4bf8d2f', + 'pretty_version' => '7.15.3', + 'version' => '7.15.3.0', + 'reference' => 'ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/guzzle', 'aliases' => array(), 'dev_requirement' => false, ), 'guzzlehttp/promises' => array( - 'pretty_version' => '2.5.1', - 'version' => '2.5.1.0', - 'reference' => '9ad1e4fc607446a055b95870c7f668e93b5cff29', + 'pretty_version' => '2.5.2', + 'version' => '2.5.2.0', + 'reference' => '2823687acff28b2dbe67b2508a6b300e2c3fa4ce', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/promises', 'aliases' => array(), @@ -254,8 +254,8 @@ 'dev_requirement' => false, ), 'illuminate/bus' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => '9e70f409c744e0ba6b301fa608bb5b3c8c38669a', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/bus', @@ -263,8 +263,8 @@ 'dev_requirement' => false, ), 'illuminate/cache' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => '2689356ab45bc53ffe796d1b1241babae59c69f6', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/cache', @@ -272,8 +272,8 @@ 'dev_requirement' => false, ), 'illuminate/collections' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => '83313b009c4afb6f02dbc090bdb67809756eefa2', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/collections', @@ -281,8 +281,8 @@ 'dev_requirement' => false, ), 'illuminate/conditionable' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => 'ec677967c1f2faf90b8428919124d2184a4c9b49', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/conditionable', @@ -290,8 +290,8 @@ 'dev_requirement' => false, ), 'illuminate/config' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => '7adbf5cc27081d4613e1fa2f4f53e2a4fc91ad53', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/config', @@ -299,17 +299,17 @@ 'dev_requirement' => false, ), 'illuminate/console' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', - 'reference' => '91732d7ae739c86dd9055c4dc850c2b4efccb47f', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', + 'reference' => 'ce37b63fb90f036dbd34263d84725f79c6d6f221', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/console', 'aliases' => array(), 'dev_requirement' => false, ), 'illuminate/container' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => '9abe9dba6b3f5220205ce143069c975a601e4294', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/container', @@ -317,8 +317,8 @@ 'dev_requirement' => false, ), 'illuminate/contracts' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => 'c16fd7ba7d8e6b8f336639ceb6b7e1ff6ed0efb5', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/contracts', @@ -326,17 +326,17 @@ 'dev_requirement' => false, ), 'illuminate/database' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', - 'reference' => '13ea191cbbee8cc615188c990ec26428df89fd29', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', + 'reference' => '6d539459fb55732a13e7f962afeffa9dca2a1cce', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/database', 'aliases' => array(), 'dev_requirement' => false, ), 'illuminate/events' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => 'ff22aa6017bdbd90ace660f50638ed9c580ba555', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/events', @@ -344,26 +344,26 @@ 'dev_requirement' => false, ), 'illuminate/filesystem' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', - 'reference' => '3b5ad9b385595d735689ebc2bfe701567cc4f1c3', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', + 'reference' => 'dc0b7440f12753060b574fa8c20c2db259fa37c9', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/filesystem', 'aliases' => array(), 'dev_requirement' => false, ), 'illuminate/http' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', - 'reference' => '15f6ff6d3cc237500575827c3d106c63ceb48984', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', + 'reference' => '5f4f82ed0ed550a01f5d95c4ddbbad409fb0d560', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/http', 'aliases' => array(), 'dev_requirement' => false, ), 'illuminate/log' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => '0309c15e63d07c05218b622caa514f3ed6b226a6', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/log', @@ -371,8 +371,8 @@ 'dev_requirement' => false, ), 'illuminate/macroable' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => 'e295d62d89dcdb87e2b1bd70dd14d074a0ed73cc', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/macroable', @@ -380,8 +380,8 @@ 'dev_requirement' => false, ), 'illuminate/pagination' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => '8327d828676654053906a771abf5eea5426354ec', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/pagination', @@ -389,8 +389,8 @@ 'dev_requirement' => false, ), 'illuminate/pipeline' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => 'b6a14c20d69a44bf0a6fba664a00d23ca71770ee', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/pipeline', @@ -398,8 +398,8 @@ 'dev_requirement' => false, ), 'illuminate/queue' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => '64171cef7be5f3ac660484de502e48c566b6feca', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/queue', @@ -407,8 +407,8 @@ 'dev_requirement' => false, ), 'illuminate/redis' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => 'e46d4c3806d78d4d7733cd9b9bfb2fef30f4c8cb', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/redis', @@ -416,8 +416,8 @@ 'dev_requirement' => false, ), 'illuminate/reflection' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => '348cf5da9de89b596d7723be6425fb048e2bf4bb', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/reflection', @@ -425,17 +425,17 @@ 'dev_requirement' => false, ), 'illuminate/routing' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', - 'reference' => 'f6c107d75cefff875aff53e0aa6a32c2341432a9', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', + 'reference' => '5d3cc9beca82c359f16e6c1d072903087e428ca1', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/routing', 'aliases' => array(), 'dev_requirement' => false, ), 'illuminate/session' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => '98802e67dd5e059c0b978b3fe8f5f0a3ac17ec4e', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/session', @@ -443,17 +443,17 @@ 'dev_requirement' => false, ), 'illuminate/support' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', - 'reference' => 'bc7bc2db3e635697f81ea123511ae25b014a63fb', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', + 'reference' => '7d7a7d2dc5084aa4b1fd8b0a42094e1d3fb51e00', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/support', 'aliases' => array(), 'dev_requirement' => false, ), 'illuminate/translation' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => '4b555f3ca37523d1064d08e99774ab32fd0cc228', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/translation', @@ -461,17 +461,17 @@ 'dev_requirement' => false, ), 'illuminate/validation' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', - 'reference' => '297cff9c69885b1a9125ee4f26f77ef570abb2b6', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', + 'reference' => 'ce2bb8d019e2510d1892b0c87f9086ea802c28e4', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/validation', 'aliases' => array(), 'dev_requirement' => false, ), 'illuminate/view' => array( - 'pretty_version' => 'v12.64.0', - 'version' => '12.64.0.0', + 'pretty_version' => 'v12.67.0', + 'version' => '12.67.0.0', 'reference' => '83bf8a22bb61145bcc1b8912103f6fb85078e35c', 'type' => 'library', 'install_path' => __DIR__ . '/../illuminate/view', @@ -497,18 +497,18 @@ 'dev_requirement' => false, ), 'laravel/prompts' => array( - 'pretty_version' => 'v0.3.21', - 'version' => '0.3.21.0', - 'reference' => '7753c65c281c2550c7c183f14e18062073b7d821', + 'pretty_version' => 'v0.3.23', + 'version' => '0.3.23.0', + 'reference' => 'b7b4c35e5bc47450f6b6238c6cc9c47ba19b2221', 'type' => 'library', 'install_path' => __DIR__ . '/../laravel/prompts', 'aliases' => array(), 'dev_requirement' => false, ), 'laravel/serializable-closure' => array( - 'pretty_version' => 'v2.0.13', - 'version' => '2.0.13.0', - 'reference' => 'b566ee0dd251f3c4078bed003a7ce015f5ea6dce', + 'pretty_version' => 'v2.0.15', + 'version' => '2.0.15.0', + 'reference' => 'dccd8bcb851bb03fcc005df650b708b57cc52661', 'type' => 'library', 'install_path' => __DIR__ . '/../laravel/serializable-closure', 'aliases' => array(), @@ -566,9 +566,9 @@ ), ), 'nesbot/carbon' => array( - 'pretty_version' => '3.13.1', - 'version' => '3.13.1.0', - 'reference' => '2937ad3d1d2c506fd2bc97d571438a95641f44e2', + 'pretty_version' => '3.13.2', + 'version' => '3.13.2.0', + 'reference' => 'a1c54919f5fff9800cd03c32bd01defd5a4061cb', 'type' => 'library', 'install_path' => __DIR__ . '/../nesbot/carbon', 'aliases' => array(), @@ -602,9 +602,9 @@ 'dev_requirement' => false, ), 'predis/predis' => array( - 'pretty_version' => 'v3.5.1', - 'version' => '3.5.1.0', - 'reference' => '5c996db191ee2d9bafe651f454b1fca16754271b', + 'pretty_version' => 'v3.6.0', + 'version' => '3.6.0.0', + 'reference' => '2ff20c08bb63697245ffee3f198d1673086c302d', 'type' => 'library', 'install_path' => __DIR__ . '/../predis/predis', 'aliases' => array(), @@ -837,9 +837,9 @@ 'dev_requirement' => false, ), 'secondnetwork/blade-tabler-icons' => array( - 'pretty_version' => 'v3.44.0', - 'version' => '3.44.0.0', - 'reference' => '856ad75e2c0704096bf4a708b6464620e41d1a34', + 'pretty_version' => 'v3.46.0', + 'version' => '3.46.0.0', + 'reference' => 'aedaebc18f382a9d0a1870e9ad394c5c89abd72b', 'type' => 'library', 'install_path' => __DIR__ . '/../secondnetwork/blade-tabler-icons', 'aliases' => array(), @@ -855,9 +855,9 @@ 'dev_requirement' => false, ), 'seld/phar-utils' => array( - 'pretty_version' => '1.2.1', - 'version' => '1.2.1.0', - 'reference' => 'ea2f4014f163c1be4c601b9b7bd6af81ba8d701c', + 'pretty_version' => '1.2.2', + 'version' => '1.2.2.0', + 'reference' => '990bbd0e92caa216d52eca0935f6e35e589bfaa5', 'type' => 'library', 'install_path' => __DIR__ . '/../seld/phar-utils', 'aliases' => array(), @@ -897,9 +897,9 @@ 'dev_requirement' => false, ), 'symfony/console' => array( - 'pretty_version' => 'v7.4.14', - 'version' => '7.4.14.0', - 'reference' => '92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87', + 'pretty_version' => 'v7.4.16', + 'version' => '7.4.16.0', + 'reference' => 'f4c69c9aed03abf933b294257d618bdd9b30a06d', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/console', 'aliases' => array(), @@ -915,18 +915,18 @@ 'dev_requirement' => false, ), 'symfony/error-handler' => array( - 'pretty_version' => 'v7.4.14', - 'version' => '7.4.14.0', - 'reference' => '4e1a093b481f323e6e326451f9760c3868430673', + 'pretty_version' => 'v7.4.15', + 'version' => '7.4.15.0', + 'reference' => 'd49f6a19f326db41ae7103bdc38e3eb35a791261', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/error-handler', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/event-dispatcher' => array( - 'pretty_version' => 'v7.4.14', - 'version' => '7.4.14.0', - 'reference' => '51fe3d170227be8d1772214b82ae506e15ed78ff', + 'pretty_version' => 'v7.4.15', + 'version' => '7.4.15.0', + 'reference' => '336e7f3b9e95aba04f93ea9143920c2186abfbb9', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/event-dispatcher', 'aliases' => array(), @@ -948,9 +948,9 @@ ), ), 'symfony/filesystem' => array( - 'pretty_version' => 'v7.4.11', - 'version' => '7.4.11.0', - 'reference' => 'd721ea61b4a5fba8c5b6e7c1feda19efea144b50', + 'pretty_version' => 'v7.4.15', + 'version' => '7.4.15.0', + 'reference' => 'ff16a16bf87fdf264638b8f6995b3515975e3c79', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/filesystem', 'aliases' => array(), @@ -966,27 +966,27 @@ 'dev_requirement' => false, ), 'symfony/http-foundation' => array( - 'pretty_version' => 'v7.4.14', - 'version' => '7.4.14.0', - 'reference' => '06db5ae1552177bf8572f8908839f12e3c06aed3', + 'pretty_version' => 'v7.4.16', + 'version' => '7.4.16.0', + 'reference' => 'b676451bb638e99a7d34d8a2be90406822e301eb', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/http-foundation', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/http-kernel' => array( - 'pretty_version' => 'v7.4.14', - 'version' => '7.4.14.0', - 'reference' => 'e99af79b1e776646eda0e1c23b7b45c184ff99be', + 'pretty_version' => 'v7.4.16', + 'version' => '7.4.16.0', + 'reference' => 'f5e728670fa2218ae8be8ea91f2b44b7d6e5304c', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/http-kernel', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/mime' => array( - 'pretty_version' => 'v7.4.13', - 'version' => '7.4.13.0', - 'reference' => 'a845722765c4f6b2ce88beaf4f4479975b186770', + 'pretty_version' => 'v7.4.16', + 'version' => '7.4.16.0', + 'reference' => '20094b76a7106dbe978d31cd3bd7aa1ed248d0e5', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/mime', 'aliases' => array(), @@ -1002,9 +1002,9 @@ 'dev_requirement' => false, ), 'symfony/polyfill-intl-grapheme' => array( - 'pretty_version' => 'v1.38.1', - 'version' => '1.38.1.0', - 'reference' => 'e9247d281d694a5120554d9afaf54e070e88a603', + 'pretty_version' => 'v1.41.0', + 'version' => '1.41.0.0', + 'reference' => 'bb899c1db0aa8127dc3afe8cda4a67eb24915f8d', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-intl-grapheme', 'aliases' => array(), @@ -1077,9 +1077,9 @@ 'dev_requirement' => false, ), 'symfony/polyfill-php85' => array( - 'pretty_version' => 'v1.38.1', - 'version' => '1.38.1.0', - 'reference' => 'ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1', + 'pretty_version' => 'v1.41.0', + 'version' => '1.41.0.0', + 'reference' => '255fab485aaa1006ed411040c42aecd7b5302d7a', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-php85', 'aliases' => array(), @@ -1095,9 +1095,9 @@ 'dev_requirement' => false, ), 'symfony/routing' => array( - 'pretty_version' => 'v7.4.13', - 'version' => '7.4.13.0', - 'reference' => '3a162171bb008e5e0f15dce6581373a4c0e8390d', + 'pretty_version' => 'v7.4.15', + 'version' => '7.4.15.0', + 'reference' => '80c0a93d3f8e7499f716204a1fb38ead942a7a2b', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/routing', 'aliases' => array(), @@ -1113,18 +1113,18 @@ 'dev_requirement' => false, ), 'symfony/string' => array( - 'pretty_version' => 'v7.4.13', - 'version' => '7.4.13.0', - 'reference' => '961683010db3b27ec6ebcd7308e6e1ee8fa7ffde', + 'pretty_version' => 'v7.4.15', + 'version' => '7.4.15.0', + 'reference' => 'e394af32256bf9e7bf80849d95e589167c10097b', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/string', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/translation' => array( - 'pretty_version' => 'v7.4.14', - 'version' => '7.4.14.0', - 'reference' => 'a1af4dacb24eb7ef4f1ca71b94da8ddbce572281', + 'pretty_version' => 'v7.4.16', + 'version' => '7.4.16.0', + 'reference' => '501e0ff4553c744209ca1a68790d8a4541563710', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/translation', 'aliases' => array(), @@ -1146,18 +1146,18 @@ ), ), 'symfony/var-dumper' => array( - 'pretty_version' => 'v7.4.14', - 'version' => '7.4.14.0', - 'reference' => '9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358', + 'pretty_version' => 'v7.4.15', + 'version' => '7.4.15.0', + 'reference' => '04ba4add636a95ff437af3a5a9499bb1d6c6d4bd', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/var-dumper', 'aliases' => array(), 'dev_requirement' => false, ), 'tracy/tracy' => array( - 'pretty_version' => 'v2.12.0', - 'version' => '2.12.0.0', - 'reference' => '535eda4871fd04f2756c8d95f6bdfa43706d79be', + 'pretty_version' => 'v2.12.1', + 'version' => '2.12.1.0', + 'reference' => '90d24872cd97202f79281aac7eb09c27ec48bf69', 'type' => 'library', 'install_path' => __DIR__ . '/../tracy/tracy', 'aliases' => array(), diff --git a/core/vendor/doctrine/dbal/src/Connection.php b/core/vendor/doctrine/dbal/src/Connection.php index cd501c3ba1..0e523708c0 100644 --- a/core/vendor/doctrine/dbal/src/Connection.php +++ b/core/vendor/doctrine/dbal/src/Connection.php @@ -277,11 +277,9 @@ public function setAutoCommit(bool $autoCommit): void $this->autoCommit = $autoCommit; // Commit all currently active transactions if any when switching auto-commit mode. - if ($this->_conn === null || $this->transactionNestingLevel === 0) { - return; + if ($this->_conn !== null && $this->transactionNestingLevel !== 0) { + $this->commitAll(); } - - $this->commitAll(); } /** @@ -1099,11 +1097,9 @@ private function updateTransactionStateAfterCommit(): void --$this->transactionNestingLevel; } - if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) { - return; + if ($this->autoCommit === false && $this->transactionNestingLevel === 0) { + $this->beginTransaction(); } - - $this->beginTransaction(); } /** @@ -1188,11 +1184,9 @@ public function releaseSavepoint(string $savepoint): void throw SavepointsNotSupported::new(); } - if (! $platform->supportsReleaseSavepoints()) { - return; + if ($platform->supportsReleaseSavepoints()) { + $this->executeStatement($platform->releaseSavePoint($savepoint)); } - - $this->executeStatement($platform->releaseSavePoint($savepoint)); } /** diff --git a/core/vendor/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php b/core/vendor/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php index 8f22fc7559..dca9c9eafe 100644 --- a/core/vendor/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php +++ b/core/vendor/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php @@ -78,7 +78,10 @@ public function convert(Exception $exception, ?Query $query): DriverException return new ConnectionException($exception, $query); } - if (str_contains($exception->getMessage(), 'terminating connection')) { + if ( + str_contains($exception->getMessage(), 'terminating connection') + || str_contains($exception->getMessage(), 'server closed the connection') + ) { return new ConnectionLost($exception, $query); } diff --git a/core/vendor/doctrine/dbal/src/Driver/AbstractMySQLDriver.php b/core/vendor/doctrine/dbal/src/Driver/AbstractMySQLDriver.php index e3497a0610..bd2e8ae2a5 100644 --- a/core/vendor/doctrine/dbal/src/Driver/AbstractMySQLDriver.php +++ b/core/vendor/doctrine/dbal/src/Driver/AbstractMySQLDriver.php @@ -13,6 +13,7 @@ use Doctrine\DBAL\Platforms\MariaDB1052Platform; use Doctrine\DBAL\Platforms\MariaDB1060Platform; use Doctrine\DBAL\Platforms\MariaDB110700Platform; +use Doctrine\DBAL\Platforms\MariaDB120300Platform; use Doctrine\DBAL\Platforms\MariaDBPlatform; use Doctrine\DBAL\Platforms\MySQL80Platform; use Doctrine\DBAL\Platforms\MySQL84Platform; @@ -39,6 +40,10 @@ public function getDatabasePlatform(ServerVersionProvider $versionProvider): Abs $version = $versionProvider->getServerVersion(); if (stripos($version, 'mariadb') !== false) { $mariaDbVersion = $this->getMariaDbMysqlVersionNumber($version); + if (version_compare($mariaDbVersion, '12.3.0', '>=')) { + return new MariaDB120300Platform(); + } + if (version_compare($mariaDbVersion, '11.7.0', '>=')) { return new MariaDB110700Platform(); } diff --git a/core/vendor/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php b/core/vendor/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php index 0079cf1286..8d35e8f0da 100644 --- a/core/vendor/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php +++ b/core/vendor/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php @@ -102,11 +102,9 @@ private static function renderParams(array $params): string foreach ($params as $key => $value) { $string = self::renderValue($value); - if ($string === '') { - continue; + if ($string !== '') { + $chunks[] = sprintf('(%s=%s)', $key, $string); } - - $chunks[] = sprintf('(%s=%s)', $key, $string); } return implode('', $chunks); diff --git a/core/vendor/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php b/core/vendor/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php index f1feb4cd42..cc0ebd1b12 100644 --- a/core/vendor/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php +++ b/core/vendor/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php @@ -68,11 +68,9 @@ public static function fromConnectionParameters(#[SensitiveParameter] 'password' => 'PWD', ] as $dbalParam => $dsnParam ) { - if (! isset($params[$dbalParam])) { - continue; + if (isset($params[$dbalParam])) { + $dsnParams[$dsnParam] = $params[$dbalParam]; } - - $dsnParams[$dsnParam] = $params[$dbalParam]; } return self::fromArray($dsnParams); diff --git a/core/vendor/doctrine/dbal/src/Driver/Mysqli/Driver.php b/core/vendor/doctrine/dbal/src/Driver/Mysqli/Driver.php index 9855e565da..b90f035fb4 100644 --- a/core/vendor/doctrine/dbal/src/Driver/Mysqli/Driver.php +++ b/core/vendor/doctrine/dbal/src/Driver/Mysqli/Driver.php @@ -108,10 +108,8 @@ private function compilePostInitializers( #[SensitiveParameter] array $params, ): Generator { - if (! isset($params['charset'])) { - return; + if (isset($params['charset'])) { + yield new Charset($params['charset']); } - - yield new Charset($params['charset']); } } diff --git a/core/vendor/doctrine/dbal/src/Driver/Mysqli/Initializer/Charset.php b/core/vendor/doctrine/dbal/src/Driver/Mysqli/Initializer/Charset.php index d02c76841b..677d87043d 100644 --- a/core/vendor/doctrine/dbal/src/Driver/Mysqli/Initializer/Charset.php +++ b/core/vendor/doctrine/dbal/src/Driver/Mysqli/Initializer/Charset.php @@ -23,10 +23,8 @@ public function initialize(mysqli $connection): void throw InvalidCharset::upcast($e, $this->charset); } - if ($success) { - return; + if (! $success) { + throw InvalidCharset::fromCharset($connection, $this->charset); } - - throw InvalidCharset::fromCharset($connection, $this->charset); } } diff --git a/core/vendor/doctrine/dbal/src/Driver/PgSQL/Connection.php b/core/vendor/doctrine/dbal/src/Driver/PgSQL/Connection.php index 5d7a40d896..043c2de4b8 100644 --- a/core/vendor/doctrine/dbal/src/Driver/PgSQL/Connection.php +++ b/core/vendor/doctrine/dbal/src/Driver/PgSQL/Connection.php @@ -32,11 +32,9 @@ public function __construct(private readonly PgSqlConnection $connection) public function __destruct() { // @phpstan-ignore isset.initializedProperty - if (! isset($this->connection)) { - return; + if (isset($this->connection)) { + @pg_close($this->connection); } - - @pg_close($this->connection); } public function prepare(string $sql): Statement diff --git a/core/vendor/doctrine/dbal/src/Driver/PgSQL/Result.php b/core/vendor/doctrine/dbal/src/Driver/PgSQL/Result.php index ae1ce40dd0..38e61b42ba 100644 --- a/core/vendor/doctrine/dbal/src/Driver/PgSQL/Result.php +++ b/core/vendor/doctrine/dbal/src/Driver/PgSQL/Result.php @@ -41,11 +41,9 @@ public function __construct(PgSqlResult $result) public function __destruct() { - if (! isset($this->result)) { - return; + if (isset($this->result)) { + $this->free(); } - - $this->free(); } /** {@inheritDoc} */ diff --git a/core/vendor/doctrine/dbal/src/Driver/SQLSrv/Exception/Error.php b/core/vendor/doctrine/dbal/src/Driver/SQLSrv/Exception/Error.php index e9adb05286..49b4ad0afd 100644 --- a/core/vendor/doctrine/dbal/src/Driver/SQLSrv/Exception/Error.php +++ b/core/vendor/doctrine/dbal/src/Driver/SQLSrv/Exception/Error.php @@ -24,11 +24,9 @@ public static function new(): self $message .= 'SQLSTATE [' . $error['SQLSTATE'] . ', ' . $error['code'] . ']: ' . $error['message'] . "\n"; $sqlState ??= $error['SQLSTATE']; - if ($code !== 0) { - continue; + if ($code === 0) { + $code = $error['code']; } - - $code = $error['code']; } if ($message === '') { diff --git a/core/vendor/doctrine/dbal/src/Driver/SQLSrv/Statement.php b/core/vendor/doctrine/dbal/src/Driver/SQLSrv/Statement.php index dc7827a50f..98faaf2fa4 100644 --- a/core/vendor/doctrine/dbal/src/Driver/SQLSrv/Statement.php +++ b/core/vendor/doctrine/dbal/src/Driver/SQLSrv/Statement.php @@ -59,11 +59,9 @@ public function __construct( private readonly mixed $conn, private string $sql, ) { - if (stripos($sql, 'INSERT INTO ') !== 0) { - return; + if (stripos($sql, 'INSERT INTO ') === 0) { + $this->sql .= self::LAST_INSERT_ID_SQL; } - - $this->sql .= self::LAST_INSERT_ID_SQL; } public function bindValue(int|string $param, mixed $value, ParameterType $type): void diff --git a/core/vendor/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php b/core/vendor/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php index 6d602c1a5b..04e903f397 100644 --- a/core/vendor/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php +++ b/core/vendor/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php @@ -395,7 +395,6 @@ public function getAlterTableSQL(TableDiff $diff): array $droppedIndexes = $this->indexIndexesByLowerCaseName($diff->getDroppedIndexes()); $addedIndexes = $this->indexIndexesByLowerCaseName($diff->getAddedIndexes()); - $diffModified = false; $noLongerPrimaryKeyColumns = []; @@ -564,11 +563,9 @@ private function getPreAlterTableAlterIndexForeignKeySQL(TableDiff $diff): array $primaryKeyColumns = []; foreach ($primaryKey->getColumns() as $columnName) { - if (! $table->hasColumn($columnName)) { - continue; + if ($table->hasColumn($columnName)) { + $primaryKeyColumns[] = $table->getColumn($columnName); } - - $primaryKeyColumns[] = $table->getColumn($columnName); } if (count($primaryKeyColumns) === 0) { diff --git a/core/vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php b/core/vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php index f29f8fef70..bad3590b62 100644 --- a/core/vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php +++ b/core/vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php @@ -940,11 +940,9 @@ private function buildCreateTableSQL(Table $table, bool $createForeignKeys): arr foreach ($table->getColumns() as $column) { $comment = $column->getComment(); - if ($comment === '') { - continue; + if ($comment !== '') { + $sql[] = $this->getCommentOnColumnSQL($tableName, $column->getQuotedName($this), $comment); } - - $sql[] = $this->getCommentOnColumnSQL($tableName, $column->getQuotedName($this), $comment); } } @@ -1631,11 +1629,9 @@ public function getCheckDeclarationSQL(array $definition): string $constraints[] = 'CHECK (' . $def['name'] . ' >= ' . $def['min'] . ')'; } - if (! isset($def['max'])) { - continue; + if (isset($def['max'])) { + $constraints[] = 'CHECK (' . $def['name'] . ' <= ' . $def['max'] . ')'; } - - $constraints[] = 'CHECK (' . $def['name'] . ' <= ' . $def['max'] . ')'; } } @@ -1877,11 +1873,9 @@ public function convertBooleans(mixed $item): mixed { if (is_array($item)) { foreach ($item as $k => $value) { - if (! is_bool($value)) { - continue; + if (is_bool($value)) { + $item[$k] = (int) $value; } - - $item[$k] = (int) $value; } } elseif (is_bool($item)) { $item = (int) $item; diff --git a/core/vendor/doctrine/dbal/src/Platforms/Keywords/MariaDB123Keywords.php b/core/vendor/doctrine/dbal/src/Platforms/Keywords/MariaDB123Keywords.php new file mode 100644 index 0000000000..f82248bc8e --- /dev/null +++ b/core/vendor/doctrine/dbal/src/Platforms/Keywords/MariaDB123Keywords.php @@ -0,0 +1,32 @@ +getModifiedForeignKeys(); foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) { - if (in_array($foreignKey, $modifiedForeignKeys, true)) { - continue; + if (! in_array($foreignKey, $modifiedForeignKeys, true)) { + $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableName); } - - $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableName); } return $sql; @@ -100,11 +98,9 @@ private function getPostAlterTableRenameIndexForeignKeySQL(TableDiff $diff): arr $modifiedForeignKeys = $diff->getModifiedForeignKeys(); foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) { - if (in_array($foreignKey, $modifiedForeignKeys, true)) { - continue; + if (! in_array($foreignKey, $modifiedForeignKeys, true)) { + $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName); } - - $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName); } return $sql; diff --git a/core/vendor/doctrine/dbal/src/Platforms/MySQL/MySQLMetadataProvider.php b/core/vendor/doctrine/dbal/src/Platforms/MySQL/MySQLMetadataProvider.php index 7720164bd1..697cf41a48 100644 --- a/core/vendor/doctrine/dbal/src/Platforms/MySQL/MySQLMetadataProvider.php +++ b/core/vendor/doctrine/dbal/src/Platforms/MySQL/MySQLMetadataProvider.php @@ -155,6 +155,8 @@ private function getTableColumns(?string $tableName): iterable $params[] = $tableName; } + // On MySQL < 8.0 and MariaDB, information_schema compares object names case-insensitively and so + // conflates table names that differ only in case. The binary comparison prevents that. $sql = sprintf( <<<'SQL' SELECT c.TABLE_NAME, @@ -174,6 +176,7 @@ private function getTableColumns(?string $tableName): iterable FROM information_schema.COLUMNS c INNER JOIN information_schema.TABLES t ON t.TABLE_NAME = c.TABLE_NAME + AND CONVERT(t.TABLE_NAME USING binary) = CONVERT(c.TABLE_NAME USING binary) WHERE %s AND t.TABLE_TYPE = 'BASE TABLE' ORDER BY c.TABLE_NAME, @@ -473,6 +476,8 @@ private function getPrimaryKeyConstraintColumns(?string $tableName): iterable $params[] = $tableName; } + // On MySQL < 8.0 and MariaDB, information_schema compares object names case-insensitively and so + // conflates table names that differ only in case. The binary comparison prevents that. $sql = sprintf( <<<'SQL' SELECT tc.TABLE_NAME, @@ -481,6 +486,7 @@ private function getPrimaryKeyConstraintColumns(?string $tableName): iterable FROM information_schema.TABLE_CONSTRAINTS tc INNER JOIN information_schema.KEY_COLUMN_USAGE kcu ON kcu.TABLE_NAME = tc.TABLE_NAME + AND CONVERT(kcu.TABLE_NAME USING binary) = CONVERT(tc.TABLE_NAME USING binary) AND kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME WHERE %s AND tc.CONSTRAINT_TYPE = 'PRIMARY KEY' @@ -540,6 +546,8 @@ private function getForeignKeyConstraintColumns(?string $tableName): iterable $params[] = $tableName; } + // On MySQL < 8.0 and MariaDB, information_schema compares object names case-insensitively and so + // conflates table names that differ only in case. The binary comparison prevents that. $sql = sprintf( <<<'SQL' SELECT k.TABLE_NAME, @@ -553,6 +561,7 @@ private function getForeignKeyConstraintColumns(?string $tableName): iterable INNER JOIN information_schema.REFERENTIAL_CONSTRAINTS c ON c.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND c.TABLE_NAME = k.TABLE_NAME + AND CONVERT(c.TABLE_NAME USING binary) = CONVERT(k.TABLE_NAME USING binary) WHERE %s AND k.REFERENCED_COLUMN_NAME IS NOT NULL ORDER BY k.TABLE_NAME, diff --git a/core/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php b/core/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php index 325a5fecc2..160a49772a 100644 --- a/core/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php +++ b/core/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php @@ -208,11 +208,9 @@ protected function _getCreateTableSQL(string $name, array $columns, array $optio ' ADD' . $this->getDefaultConstraintDeclarationSQL($column); } - if (empty($column['comment']) && ! is_numeric($column['comment'])) { - continue; + if (! empty($column['comment']) || is_numeric($column['comment'])) { + $commentsSql[] = $this->getCreateColumnCommentSQL($name, $column['name'], $column['comment']); } - - $commentsSql[] = $this->getCreateColumnCommentSQL($name, $column['name'], $column['comment']); } $columnListSql = $this->getColumnDeclarationListSQL($columns); @@ -1080,11 +1078,9 @@ public function convertBooleans(mixed $item): mixed { if (is_array($item)) { foreach ($item as $key => $value) { - if (! is_bool($value) && ! is_numeric($value)) { - continue; + if (is_bool($value) || is_numeric($value)) { + $item[$key] = (int) (bool) $value; } - - $item[$key] = (int) (bool) $value; } } elseif (is_bool($item) || is_numeric($item)) { $item = (int) (bool) $item; diff --git a/core/vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php b/core/vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php index 6fcce9a285..6fcf04883f 100644 --- a/core/vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php +++ b/core/vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php @@ -511,11 +511,9 @@ protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff): array $sql = []; foreach ($this->getIndexesInAlteredTable($diff) as $index) { - if ($index->isPrimary()) { - continue; + if (! $index->isPrimary()) { + $sql[] = $this->getCreateIndexSQL($index, $table->getQuotedName($this)); } - - $sql[] = $this->getCreateIndexSQL($index, $table->getQuotedName($this)); } return $sql; @@ -662,11 +660,9 @@ public function getAlterTableSQL(TableDiff $diff): array $newColumn, ); - if (! isset($newColumnNames[$oldColumnName])) { - continue; + if (isset($newColumnNames[$oldColumnName])) { + $newColumnNames[$oldColumnName] = $newColumn->getQuotedName($this); } - - $newColumnNames[$oldColumnName] = $newColumn->getQuotedName($this); } foreach ($diff->getAddedColumns() as $column) { @@ -766,7 +762,9 @@ private function getSimpleAlterTableSQL(TableDiff $diff): array|false $type = $definition['type']; switch (true) { - case isset($definition['columnDefinition']) || $definition['autoincrement']: + case isset($definition['columnDefinition']): + case $definition['autoincrement']: + case $definition['comment'] !== '': case $type instanceof Types\DateTimeType && $definition['default'] === $this->getCurrentTimestampSQL(): case $type instanceof Types\DateType && $definition['default'] === $this->getCurrentDateSQL(): case $type instanceof Types\TimeType && $definition['default'] === $this->getCurrentTimeSQL(): @@ -827,11 +825,9 @@ private function getIndexesInAlteredTable(TableDiff $diff): array foreach ($indexes as $key => $index) { $indexName = $index->getName(); foreach ($diff->getRenamedIndexes() as $oldIndexName => $renamedIndex) { - if (strtolower($indexName) !== strtolower($oldIndexName)) { - continue; + if (strtolower($indexName) === strtolower($oldIndexName)) { + unset($indexes[$key]); } - - unset($indexes[$key]); } $changed = false; @@ -844,11 +840,9 @@ private function getIndexesInAlteredTable(TableDiff $diff): array } $indexColumns[] = $nameMap[$normalizedColumnName]; - if ($columnName === $nameMap[$normalizedColumnName]) { - continue; + if ($columnName !== $nameMap[$normalizedColumnName]) { + $changed = true; } - - $changed = true; } if (! $changed) { @@ -867,11 +861,9 @@ private function getIndexesInAlteredTable(TableDiff $diff): array foreach ($diff->getDroppedIndexes() as $index) { $indexName = $index->getName(); - if ($indexName === '') { - continue; + if ($indexName !== '') { + unset($indexes[strtolower($indexName)]); } - - unset($indexes[strtolower($indexName)]); } foreach ( @@ -911,11 +903,9 @@ private function getForeignKeysInAlteredTable(TableDiff $diff): array } $localColumns[] = $nameMap[$normalizedColumnName]; - if ($columnName === $nameMap[$normalizedColumnName]) { - continue; + if ($columnName !== $nameMap[$normalizedColumnName]) { + $changed = true; } - - $changed = true; } if (! $changed) { @@ -934,11 +924,9 @@ private function getForeignKeysInAlteredTable(TableDiff $diff): array foreach ($diff->getDroppedForeignKeys() as $constraint) { $constraintName = $constraint->getName(); - if ($constraintName === '') { - continue; + if ($constraintName !== '') { + unset($foreignKeys[strtolower($constraintName)]); } - - unset($foreignKeys[strtolower($constraintName)]); } foreach (array_merge($diff->getModifiedForeignKeys(), $diff->getAddedForeignKeys()) as $constraint) { @@ -960,11 +948,9 @@ private function getPrimaryIndexInAlteredTable(TableDiff $diff): array $primaryIndex = []; foreach ($this->getIndexesInAlteredTable($diff) as $index) { - if (! $index->isPrimary()) { - continue; + if ($index->isPrimary()) { + $primaryIndex = [$index->getName() => $index]; } - - $primaryIndex = [$index->getName() => $index]; } return $primaryIndex; diff --git a/core/vendor/doctrine/dbal/src/Query/QueryBuilder.php b/core/vendor/doctrine/dbal/src/Query/QueryBuilder.php index a800c2fd48..77e0b972b2 100644 --- a/core/vendor/doctrine/dbal/src/Query/QueryBuilder.php +++ b/core/vendor/doctrine/dbal/src/Query/QueryBuilder.php @@ -801,10 +801,10 @@ public function from(string $table, ?string $alias = null): self * ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1'); * * - * @param string $fromAlias The alias that points to a from clause. - * @param string $join The table name to join. - * @param string $alias The alias of the join table. - * @param string $condition The condition for the join. + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string|null $condition The condition for the join. * * @return $this This QueryBuilder instance. */ @@ -823,10 +823,10 @@ public function join(string $fromAlias, string $join, string $alias, ?string $co * ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); * * - * @param string $fromAlias The alias that points to a from clause. - * @param string $join The table name to join. - * @param string $alias The alias of the join table. - * @param string $condition The condition for the join. + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string|null $condition The condition for the join. * * @return $this This QueryBuilder instance. */ @@ -849,10 +849,10 @@ public function innerJoin(string $fromAlias, string $join, string $alias, ?strin * ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); * * - * @param string $fromAlias The alias that points to a from clause. - * @param string $join The table name to join. - * @param string $alias The alias of the join table. - * @param string $condition The condition for the join. + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string|null $condition The condition for the join. * * @return $this This QueryBuilder instance. */ @@ -875,10 +875,10 @@ public function leftJoin(string $fromAlias, string $join, string $alias, ?string * ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); * * - * @param string $fromAlias The alias that points to a from clause. - * @param string $join The table name to join. - * @param string $alias The alias of the join table. - * @param string $condition The condition for the join. + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string|null $condition The condition for the join. * * @return $this This QueryBuilder instance. */ @@ -1219,8 +1219,8 @@ private function appendToPredicate( * Specifies an ordering for the query results. * Replaces any previously specified orderings, if any. * - * @param string $sort The ordering expression. - * @param string $order The ordering direction. + * @param string $sort The ordering expression. + * @param string|null $order The ordering direction. * * @return $this This QueryBuilder instance. */ @@ -1242,8 +1242,8 @@ public function orderBy(string $sort, ?string $order = null): self /** * Adds an ordering to the query results. * - * @param string $sort The ordering expression. - * @param string $order The ordering direction. + * @param string $sort The ordering expression. + * @param string|null $order The ordering direction. * * @return $this This QueryBuilder instance. */ @@ -1603,11 +1603,9 @@ public function __clone() } foreach ($this->params as $name => $param) { - if (! is_object($param)) { - continue; + if (is_object($param)) { + $this->params[$name] = clone $param; } - - $this->params[$name] = clone $param; } } diff --git a/core/vendor/doctrine/dbal/src/Schema/AbstractSchemaManager.php b/core/vendor/doctrine/dbal/src/Schema/AbstractSchemaManager.php index c65c3e526c..134c335af1 100644 --- a/core/vendor/doctrine/dbal/src/Schema/AbstractSchemaManager.php +++ b/core/vendor/doctrine/dbal/src/Schema/AbstractSchemaManager.php @@ -596,11 +596,9 @@ public function introspectTableNames(): array $tableNames = []; foreach ($this->createSchemaProvider()->getAllTableNames() as $tableName) { - if (! $this->testTableName($tableName, $filter)) { - continue; + if ($this->testTableName($tableName, $filter)) { + $tableNames[] = $tableName; } - - $tableNames[] = $tableName; } return $tableNames; @@ -619,11 +617,9 @@ public function introspectTables(): array $tables = []; foreach ($this->createSchemaProvider()->getAllTables() as $table) { - if (! $this->testTableName($table->getObjectName(), $filter)) { - continue; + if ($this->testTableName($table->getObjectName(), $filter)) { + $tables[] = $table; } - - $tables[] = $table; } return $tables; diff --git a/core/vendor/doctrine/dbal/src/Schema/Collections/OptionallyUnqualifiedNamedObjectSet.php b/core/vendor/doctrine/dbal/src/Schema/Collections/OptionallyUnqualifiedNamedObjectSet.php index e93b2c12cd..8884456b69 100644 --- a/core/vendor/doctrine/dbal/src/Schema/Collections/OptionallyUnqualifiedNamedObjectSet.php +++ b/core/vendor/doctrine/dbal/src/Schema/Collections/OptionallyUnqualifiedNamedObjectSet.php @@ -156,11 +156,9 @@ private function removeByKey(string $key): void unset($this->elementPositionsByKey[$key]); foreach ($this->elementPositionsByKey as $elementKey => $elementPosition) { - if ($elementPosition <= $position) { - continue; + if ($elementPosition > $position) { + $this->elementPositionsByKey[$elementKey]--; } - - $this->elementPositionsByKey[$elementKey]--; } } diff --git a/core/vendor/doctrine/dbal/src/Schema/Comparator.php b/core/vendor/doctrine/dbal/src/Schema/Comparator.php index 2f0a949bbb..02cd6f81d9 100644 --- a/core/vendor/doctrine/dbal/src/Schema/Comparator.php +++ b/core/vendor/doctrine/dbal/src/Schema/Comparator.php @@ -39,19 +39,15 @@ public function compareSchemas(Schema $oldSchema, Schema $newSchema): SchemaDiff $droppedSequences = []; foreach ($newSchema->getNamespaces() as $newNamespace) { - if ($oldSchema->hasNamespace($newNamespace)) { - continue; + if (! $oldSchema->hasNamespace($newNamespace)) { + $createdSchemas[] = $newNamespace; } - - $createdSchemas[] = $newNamespace; } foreach ($oldSchema->getNamespaces() as $oldNamespace) { - if ($newSchema->hasNamespace($oldNamespace)) { - continue; + if (! $newSchema->hasNamespace($oldNamespace)) { + $droppedSchemas[] = $oldNamespace; } - - $droppedSchemas[] = $oldNamespace; } foreach ($newSchema->getTables() as $newTable) { @@ -75,11 +71,9 @@ public function compareSchemas(Schema $oldSchema, Schema $newSchema): SchemaDiff $oldTableName = $oldTable->getShortestName($oldSchema->getName()); $oldTable = $oldSchema->getTable($oldTableName); - if ($newSchema->hasTable($oldTableName)) { - continue; + if (! $newSchema->hasTable($oldTableName)) { + $droppedTables[] = $oldTable; } - - $droppedTables[] = $oldTable; } foreach ($newSchema->getSequences() as $newSequence) { @@ -102,11 +96,9 @@ public function compareSchemas(Schema $oldSchema, Schema $newSchema): SchemaDiff $oldSequenceName = $oldSequence->getShortestName($oldSchema->getName()); - if ($newSchema->hasSequence($oldSequenceName)) { - continue; + if (! $newSchema->hasSequence($oldSequenceName)) { + $droppedSequences[] = $oldSequence; } - - $droppedSequences[] = $oldSequence; } return new SchemaDiff( @@ -173,11 +165,9 @@ public function compareTables(Table $oldTable, Table $newTable): TableDiff foreach ($newColumns as $newColumn) { $newColumnName = strtolower($newColumn->getName()); - if ($oldTable->hasColumn($newColumnName)) { - continue; + if (! $oldTable->hasColumn($newColumnName)) { + $addedColumns[$newColumnName] = $newColumn; } - - $addedColumns[$newColumnName] = $newColumn; } // See if there are any removed columns in the new table @@ -193,11 +183,9 @@ public function compareTables(Table $oldTable, Table $newTable): TableDiff $newColumn = $newTable->getColumn($oldColumnName); - if ($this->columnsEqual($oldColumn, $newColumn)) { - continue; + if (! $this->columnsEqual($oldColumn, $newColumn)) { + $modifiedColumns[$oldColumnName] = new ColumnDiff($oldColumn, $newColumn); } - - $modifiedColumns[$oldColumnName] = new ColumnDiff($oldColumn, $newColumn); } $renamedColumnNames = $newTable->getRenamedColumns(); @@ -231,11 +219,12 @@ public function compareTables(Table $oldTable, Table $newTable): TableDiff foreach ($newIndexes as $newIndex) { $newIndexName = $newIndex->getName(); - if (($newIndex->isPrimary() && $oldTable->getPrimaryKey() !== null) || $oldTable->hasIndex($newIndexName)) { - continue; + if ( + (! $newIndex->isPrimary() || $oldTable->getPrimaryKey() === null) + && ! $oldTable->hasIndex($newIndexName) + ) { + $addedIndexes[] = $newIndex; } - - $addedIndexes[] = $newIndex; } // See if there are any removed indexes in the new table @@ -327,11 +316,9 @@ private function detectRenamedColumns(array &$modifiedColumns, array &$addedColu foreach ($addedColumns as $addedColumnName => $addedColumn) { foreach ($removedColumns as $removedColumn) { - if (! $this->columnsEqual($addedColumn, $removedColumn)) { - continue; + if ($this->columnsEqual($addedColumn, $removedColumn)) { + $candidatesByName[$addedColumnName][] = [$removedColumn, $addedColumn]; } - - $candidatesByName[$addedColumnName][] = [$removedColumn, $addedColumn]; } } @@ -370,7 +357,8 @@ private function detectRenamedColumns(array &$modifiedColumns, array &$addedColu */ private function detectRenamedIndexes(array &$addedIndexes, array &$removedIndexes): array { - $candidatesByName = []; + $candidatesByName = []; + $removedIndexMatchCount = []; // Gather possible rename candidates by comparing each added and removed index based on semantics. foreach ($addedIndexes as $addedIndexKey => $addedIndex) { @@ -380,6 +368,8 @@ private function detectRenamedIndexes(array &$addedIndexes, array &$removedIndex } $candidatesByName[$addedIndex->getName()][] = [$removedIndexKey, $addedIndexKey]; + + $removedIndexMatchCount[$removedIndexKey] = ($removedIndexMatchCount[$removedIndexKey] ?? 0) + 1; } } @@ -396,13 +386,14 @@ private function detectRenamedIndexes(array &$addedIndexes, array &$removedIndex [$removedIndexKey, $addedIndexKey] = $candidates[0]; - $removedIndex = $removedIndexes[$removedIndexKey]; - $removedIndexName = strtolower($removedIndex->getName()); - - if (isset($renamedIndexes[$removedIndexName])) { + // Likewise, a removed index that matches more than one added index is ambiguous. + if ($removedIndexMatchCount[$removedIndexKey] > 1) { continue; } + $removedIndex = $removedIndexes[$removedIndexKey]; + $removedIndexName = strtolower($removedIndex->getName()); + $addedIndex = $addedIndexes[$addedIndexKey]; $renamedIndexes[$removedIndexName] = $addedIndex; diff --git a/core/vendor/doctrine/dbal/src/Schema/Identifier.php b/core/vendor/doctrine/dbal/src/Schema/Identifier.php index 5382026b22..bf27dc83f8 100644 --- a/core/vendor/doctrine/dbal/src/Schema/Identifier.php +++ b/core/vendor/doctrine/dbal/src/Schema/Identifier.php @@ -28,11 +28,9 @@ public function __construct(string $identifier, bool $quote = false) { parent::__construct($identifier); - if (! $quote || $this->_quoted) { - return; + if ($quote && ! $this->_quoted) { + $this->_setName('"' . $this->getName() . '"'); } - - $this->_setName('"' . $this->getName() . '"'); } protected function getNameParser(): GenericNameParser diff --git a/core/vendor/doctrine/dbal/src/Schema/IndexEditor.php b/core/vendor/doctrine/dbal/src/Schema/IndexEditor.php index 8f6ca52e4c..eb9b2da565 100644 --- a/core/vendor/doctrine/dbal/src/Schema/IndexEditor.php +++ b/core/vendor/doctrine/dbal/src/Schema/IndexEditor.php @@ -144,18 +144,20 @@ public function create(): Index } $columnNames = $lengths = $options = $flags = []; - foreach ($this->columns as $i => $column) { + $hasLength = false; + + foreach ($this->columns as $column) { $columnNames[] = $column->getColumnName()->toString(); - $length = $column->getLength(); - if ($length === null) { - continue; - } + $length = $column->getLength(); + $lengths[] = $length; - $lengths[$i] = $column->getLength(); + if ($length !== null) { + $hasLength = true; + } } - if (count($lengths) !== 0) { + if ($hasLength) { $options['lengths'] = $lengths; } diff --git a/core/vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php b/core/vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php index 8a8cba834b..7b23fe0e34 100644 --- a/core/vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php +++ b/core/vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php @@ -176,11 +176,9 @@ protected function _getPortableTableForeignKeysList(array $rows): array $list[$id]['local'][] = $row['from']; - if ($row['to'] === null) { - continue; + if ($row['to'] !== null) { + $list[$id]['foreign'][] = $row['to']; } - - $list[$id]['foreign'][] = $row['to']; } foreach ($list as $id => $value) { @@ -467,11 +465,9 @@ protected function fetchTableColumns(string $databaseName, ?string $tableName = $sqlByTable[$tableName] ??= $this->getCreateTableSQL($tableName); - if ($row['pk'] === 0 || $row['pk'] === '0' || $row['type'] !== 'INTEGER') { - continue; + if ($row['pk'] !== 0 && $row['pk'] !== '0' && $row['type'] === 'INTEGER') { + $pkColumnNamesByTable[$tableName][] = $row['name']; } - - $pkColumnNamesByTable[$tableName][] = $row['name']; } foreach ($rows as $row) { @@ -593,11 +589,9 @@ protected function fetchTableOptionsByTable(string $databaseName, ?string $table foreach ($tables as $table) { $comment = $this->parseTableCommentFromSQL($table, $this->getCreateTableSQL($table)); - if ($comment === null) { - continue; + if ($comment !== null) { + $tableOptions[$table]['comment'] = $comment; } - - $tableOptions[$table]['comment'] = $comment; } /** @phpstan-ignore return.type */ diff --git a/core/vendor/doctrine/dbal/src/Schema/Table.php b/core/vendor/doctrine/dbal/src/Schema/Table.php index e41fcc6a5f..de0d3d6be8 100644 --- a/core/vendor/doctrine/dbal/src/Schema/Table.php +++ b/core/vendor/doctrine/dbal/src/Schema/Table.php @@ -833,11 +833,9 @@ protected function _addIndex(Index $index): self continue; } - if (! $this->_indexes[$implicitIndexName]->isFulfilledBy($index)) { - continue; + if ($this->_indexes[$implicitIndexName]->isFulfilledBy($index)) { + $replacedImplicitIndexNames[$implicitIndexName] = true; } - - $replacedImplicitIndexNames[$implicitIndexName] = true; } if ($this->_primaryKeyName !== null && $index->isPrimary()) { @@ -1183,11 +1181,9 @@ private function getForeignKeyConstraintNamesByLocalColumnName(string $columnNam $names = []; foreach ($this->_fkConstraints as $name => $constraint) { - if (! in_array($columnName, $constraint->getLocalColumns(), true)) { - continue; + if (in_array($columnName, $constraint->getLocalColumns(), true)) { + $names[] = $name; } - - $names[] = $name; } return $names; @@ -1199,11 +1195,9 @@ private function getUniqueConstraintNamesByColumnName(string $columnName): array $names = []; foreach ($this->uniqueConstraints as $name => $constraint) { - if (! in_array($columnName, $constraint->getColumns(), true)) { - continue; + if (in_array($columnName, $constraint->getColumns(), true)) { + $names[] = $name; } - - $names[] = $name; } return $names; diff --git a/core/vendor/doctrine/dbal/src/Schema/TableEditor.php b/core/vendor/doctrine/dbal/src/Schema/TableEditor.php index feeb49ba1e..bc921a7b49 100644 --- a/core/vendor/doctrine/dbal/src/Schema/TableEditor.php +++ b/core/vendor/doctrine/dbal/src/Schema/TableEditor.php @@ -398,11 +398,9 @@ public function setPrimaryKeyConstraint(?PrimaryKeyConstraint $primaryKeyConstra $this->primaryKeyConstraint = $primaryKeyConstraint; foreach ($this->indexes->toList() as $index) { - if (! $index->isPrimary()) { - continue; + if ($index->isPrimary()) { + $this->indexes->remove($index->getObjectName()); } - - $this->indexes->remove($index->getObjectName()); } return $this; diff --git a/core/vendor/doctrine/dbal/src/Tools/DsnParser.php b/core/vendor/doctrine/dbal/src/Tools/DsnParser.php index 9a8fb0fe22..55db137028 100644 --- a/core/vendor/doctrine/dbal/src/Tools/DsnParser.php +++ b/core/vendor/doctrine/dbal/src/Tools/DsnParser.php @@ -50,11 +50,9 @@ public function parse( } foreach ($url as $param => $value) { - if (! is_string($value)) { - continue; + if (is_string($value)) { + $url[$param] = rawurldecode($value); } - - $url[$param] = rawurldecode($value); } $params = []; diff --git a/core/vendor/guzzlehttp/guzzle/CHANGELOG.md b/core/vendor/guzzlehttp/guzzle/CHANGELOG.md index 5cfa467d2d..efea7bde6a 100644 --- a/core/vendor/guzzlehttp/guzzle/CHANGELOG.md +++ b/core/vendor/guzzlehttp/guzzle/CHANGELOG.md @@ -3,6 +3,35 @@ Please refer to [UPGRADING](UPGRADING.md) guide for upgrading to a major version. +## 7.15.3 - 2026-08-05 + +### Changed + +- Adjusted `guzzlehttp/promises` version constraint to `^2.5.2` + +### Fixed + +- Fail a cURL multi handler wait with an attributable error when the transfer is no longer tracked +- Fix `StreamHandler` resolving numeric IPv4 hosts differently from cURL handlers on macOS and Windows +- Fix `StreamHandler` TLS peer names and proxy authorities for numeric IPv4 hosts on all platforms +- Settle a cURL multi handler transfer displaced by a request reusing its native handle ID + + +## 7.15.2 - 2026-07-26 + +### Security + +- Reject non-printable-ASCII and percent-escaped URI hosts and `Host` headers (GHSA-v5mv-p594-2x33) +- Reject request URI hosts that contain a URI authority delimiter (GHSA-v5mv-p594-2x33) +- Reject numeric-looking URI hosts with trailing dots, read as IPv4 addresses (GHSA-v5mv-p594-2x33) +- Treat numeric-in-any-base and percent-escaped cookie domains as exact-match-only (GHSA-f7vp-7xgx-4w4r) +- Regenerate a derived `Host` header after client URI rewrites (GHSA-v5mv-p594-2x33) + +### Fixed + +- Preserve `RequestException` when the stream handler rejects a request before opening a stream + + ## 7.15.1 - 2026-07-18 ### Security diff --git a/core/vendor/guzzlehttp/guzzle/composer.json b/core/vendor/guzzlehttp/guzzle/composer.json index 3ae66e6445..8850d1b5bd 100644 --- a/core/vendor/guzzlehttp/guzzle/composer.json +++ b/core/vendor/guzzlehttp/guzzle/composer.json @@ -53,7 +53,7 @@ "require": { "php": "^7.2.5 || ^8.0", "ext-json": "*", - "guzzlehttp/promises": "^2.5.1", + "guzzlehttp/promises": "^2.5.2", "guzzlehttp/psr7": "^2.13", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", diff --git a/core/vendor/guzzlehttp/guzzle/src/Client.php b/core/vendor/guzzlehttp/guzzle/src/Client.php index 28a8e154ff..3b10e1c426 100644 --- a/core/vendor/guzzlehttp/guzzle/src/Client.php +++ b/core/vendor/guzzlehttp/guzzle/src/Client.php @@ -163,7 +163,7 @@ public function sendAsync(RequestInterface $request, array $options = []): Promi $options = $this->prepareDefaults($options); return $this->transfer( - $request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')), + $request->withUri($this->buildUri($request->getUri(), $options), self::shouldPreserveHost($request)), $options ); } @@ -309,6 +309,30 @@ private function buildUri(UriInterface $uri, array $config): UriInterface return $uri; } + /** + * Whether to preserve an existing Host header when the URI changes. + * + * A header matching the current URI carries no explicit override and is + * regenerated after base URI resolution or IDN conversion. Other values + * are preserved as deliberate overrides, as PSR-7 requires. + */ + private static function shouldPreserveHost(RequestInterface $request): bool + { + if (!$request->hasHeader('Host')) { + return false; + } + + $uri = $request->getUri(); + $host = $uri->getHost(); + $port = $uri->getPort(); + + if ($port !== null) { + $host .= ':'.$port; + } + + return $host !== $request->getHeaderLine('Host'); + } + /** * Configures the default options for a client. */ diff --git a/core/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php b/core/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php index 29133e77b2..92a05836a5 100644 --- a/core/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php +++ b/core/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php @@ -2,6 +2,7 @@ namespace GuzzleHttp\Cookie; +use GuzzleHttp\Handler\HostValidator; use GuzzleHttp\Psr7; /** @@ -512,6 +513,12 @@ public function matchesDomain(string $domain): bool return true; } + // A percent-escaped cookie domain can decode to another host spelling. + // Keep it exact-match-only to avoid extending that host's cookie scope. + if (\strpos($cookieDomain, '%') !== false) { + return false; + } + // IP literals and numeric hosts are exact-match-only per RFC 6265. // Only the exact match above may succeed for those cookie domains. if (self::isIpAddressOrNumericHost($cookieDomain)) { @@ -548,7 +555,14 @@ private static function isIpAddressOrNumericHost(string $host): bool $labels = \explode('.', $host); $last = (string) \end($labels); - return $last !== '' && \ctype_digit($last); + if ($last !== '' && \ctype_digit($last)) { + return true; + } + + // Apply the transport's decimal, octal and hexadecimal inet_aton-style + // grammar. Omitting range checks conservatively holds some names to an + // exact match. + return HostValidator::isNumericIpv4Host(\rtrim($host, '.')); } /** diff --git a/core/vendor/guzzlehttp/guzzle/src/Handler/CurlHandler.php b/core/vendor/guzzlehttp/guzzle/src/Handler/CurlHandler.php index 90be9dcb08..b433fddcf5 100644 --- a/core/vendor/guzzlehttp/guzzle/src/Handler/CurlHandler.php +++ b/core/vendor/guzzlehttp/guzzle/src/Handler/CurlHandler.php @@ -70,6 +70,8 @@ public function __construct(array $options = []) public function __invoke(RequestInterface $request, array $options): PromiseInterface { + HostValidator::assertRequestHost($request); + if (isset($options['delay'])) { \usleep($options['delay'] * 1000); } diff --git a/core/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php b/core/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php index 94cf9b517b..17a622f824 100644 --- a/core/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php +++ b/core/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php @@ -345,6 +345,8 @@ public function __destruct() public function __invoke(RequestInterface $request, array $options): PromiseInterface { + HostValidator::assertRequestHost($request); + if ($this->connectionCapsApplied && \defined('CURLOPT_SHARE') && isset($options['curl']) @@ -376,18 +378,38 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte $waitToken = new \stdClass(); + $promise = null; $promise = new Promise( - function () use ($id, $waitToken): void { - if ($this->multiExecDepth > 0) { - // Waiting cannot drive native cURL while a callback has - // the multi handle busy; fail the wait promptly instead - // of self-deadlocking. - $this->failNestedWait($id, $waitToken); - + function () use ($id, $waitToken, $easy, &$promise): void { + // Waiting cannot drive native cURL while a callback has the + // multi handle busy; fail the wait promptly instead of + // self-deadlocking. + $idReused = $this->multiExecDepth > 0 + ? $this->failNestedWait($id, $waitToken) + : $this->executeUntil($id, $waitToken); + + // Settling can be queued, and guzzlehttp/promises drains the + // queue before deciding a wait function achieved nothing. + P\Utils::queue()->run(); + + // Never null: assigned below before any wait can invoke this. + /** @var Promise $promise */ + if (!P\Is::pending($promise)) { return; } - $this->executeUntil($id, $waitToken); + // Neither path guarantees the transfer settled, and returning + // while pending makes guzzlehttp/promises reject with a bare + // string naming nothing. + $stalled = $idReused + ? 'its native cURL handle ID was reused by another request' + : 'its entry was removed without settling'; + + $message = \sprintf('Waiting on cURL multi handler transfer %d cannot make progress (%s).', $id, $stalled); + + // The entry is gone or belongs to another request, so + // attribute from this easy handle. + $promise->reject(new RequestException($message, $easy->request, $easy->response)); }, function () use ($id, $waitToken) { return $this->cancel($id, $waitToken); @@ -1084,8 +1106,11 @@ public function execute(): void * The native cURL handle ID can be reused by a request created from a * completion callback, so the wait token guards against waiting on an * unrelated transfer that inherited the ID. + * + * @return bool Whether another request had reused the native cURL handle + * ID by the time the loop stopped */ - private function executeUntil(int $id, object $waitToken): void + private function executeUntil(int $id, object $waitToken): bool { $queue = P\Utils::queue(); @@ -1098,9 +1123,15 @@ private function executeUntil(int $id, object $waitToken): void $this->tickFor($id, $waitToken); } + // Sample before the drain below, which can add or remove an entry + // under this ID and so rewrite the answer. + $idReused = isset($this->handles[$id]); + if (!$queue->isEmpty()) { $queue->run(); } + + return $idReused; } /** @@ -1122,6 +1153,17 @@ private function addRequest(array $entry): void $easy = $entry['easy']; $id = (int) $easy->handle; $entry['attached'] = false; + + $displaced = $this->handles[$id] ?? null; + if ($displaced !== null) { + // Never silently discard a tracked entry; settle it first. + unset($this->handles[$id], $this->delays[$id], $this->deferredAdds[$id]); + if (P\Is::pending($displaced['deferred'])) { + $message = \sprintf('cURL multi handler transfer %d was displaced by another request that reused its native cURL handle ID.', $id); + $displaced['deferred']->reject(new RequestException($message, $displaced['easy']->request, $displaced['easy']->response)); + } + } + $this->handles[$id] = $entry; if (!empty($easy->options['delay'])) { @@ -1159,11 +1201,15 @@ private function discardPendingRequest(int $id, array $entry, \Throwable $failur /** * Fails a synchronous wait attempted from inside a cURL callback, where * native execution cannot progress until the callback returns. + * + * @return bool Whether another request had reused the native cURL handle + * ID, which only matters when no transfer was left to fail */ - private function failNestedWait(int $id, object $token): void + private function failNestedWait(int $id, object $token): bool { if (!$this->hasRequest($id, $token)) { - return; + // Nothing left to fail, so report which way the entry went. + return isset($this->handles[$id]); } $entry = $this->handles[$id]; @@ -1178,6 +1224,8 @@ private function failNestedWait(int $id, object $token): void } $entry['deferred']->reject($failure); + + return false; } /** diff --git a/core/vendor/guzzlehttp/guzzle/src/Handler/HostValidator.php b/core/vendor/guzzlehttp/guzzle/src/Handler/HostValidator.php new file mode 100644 index 0000000000..bf8cc3d315 --- /dev/null +++ b/core/vendor/guzzlehttp/guzzle/src/Handler/HostValidator.php @@ -0,0 +1,198 @@ +getUri()->getHost(); + + self::assertUriHostValue($host, $request); + self::assertNoAuthorityDelimiter($host, $request); + self::assertNotADottedAddress($host, $request); + + foreach ($request->getHeader('Host') as $value) { + self::assertHostHeaderValue((string) $value, $request); + } + } + + /** + * @throws RequestException + */ + private static function assertUriHostValue(string $value, RequestInterface $request): void + { + if (!self::isPrintableAscii($value)) { + throw new RequestException(\sprintf('The request URI host "%s" must contain only printable ASCII characters, because a handler can otherwise connect to a host that differs from the one the request names. An internationalized host name has an A-label form that this rule accepts.', self::escape($value)), $request); + } + + if (\strpos($value, '%') !== false) { + throw new RequestException(\sprintf('The request URI host "%s" must not contain a percent escape, because a handler can decode it and then connect to a host that differs from the one the request names.', self::escape($value)), $request); + } + } + + /** + * The Host header is sent rather than reparsed for the connection, so its + * diagnostics describe a request authority the caller did not write. + * + * @throws RequestException + */ + private static function assertHostHeaderValue(string $value, RequestInterface $request): void + { + if (!self::isPrintableAscii($value)) { + throw new RequestException(\sprintf('The request Host header "%s" must contain only printable ASCII characters, because an intermediary or an origin server can otherwise read it as an authority that differs from the one the request names. An internationalized host name has an A-label form that this rule accepts.', self::escape($value)), $request); + } + + if (\strpos($value, '%') !== false) { + throw new RequestException(\sprintf('The request Host header "%s" must not contain a percent escape, because an intermediary or an origin server can decode it and then read it as an authority that differs from the one the request names.', self::escape($value)), $request); + } + } + + /** + * Matches the accepted shape positively so a PCRE failure rejects. + */ + private static function isPrintableAscii(string $value): bool + { + return \preg_match('/\A[\x21-\x7E]*\z/D', $value) === 1; + } + + /** + * Rejects a delimiter the transport could treat as the end of the URI host. + * + * This mirrors GuzzleHttp\Psr7\Uri::assertValidHost() and only affects + * third-party UriInterface values. Host headers may carry a port and are + * sent verbatim. + * + * @throws RequestException + */ + private static function assertNoAuthorityDelimiter(string $host, RequestInterface $request): void + { + $message = 'The request URI host "%s" must not contain a URI authority delimiter, because a handler reparses the URI and can then connect to a host that differs from the one the request names.'; + + // Match the accepted shape positively so a PCRE engine failure rejects. + if (\preg_match('/\A[^\/?#@\\\\]*\z/D', $host) !== 1) { + throw new RequestException(\sprintf($message, self::escape($host)), $request); + } + + if (\strpos($host, '[') !== false || \strpos($host, ']') !== false) { + if (\strpos($host, '[') !== 0 || \substr($host, -1) !== ']') { + throw new RequestException(\sprintf($message, self::escape($host)), $request); + } + + return; + } + + if (\strpos($host, ':') !== false) { + throw new RequestException(\sprintf($message, self::escape($host)), $request); + } + } + + /** + * Rejects one to four numeric-looking parts followed by trailing dots. + * + * libcurl 8.21.0 drops a trailing dot from inet_aton-style numeric hosts + * before connecting, while other validators treat the input as a name. + * Testing the shape also rejects some out-of-range values that transports + * keep as names; isNumericIpv4Host() explains that fail-closed tradeoff. + * Plain numeric shorthand stays accepted. + * + * @throws RequestException + */ + private static function assertNotADottedAddress(string $host, RequestInterface $request): void + { + if (\substr($host, -1) !== '.') { + return; + } + + if (!self::isNumericIpv4Host(\rtrim($host, '.'))) { + return; + } + + throw new RequestException(\sprintf('The request URI host "%s" must not be written as one to four decimal, octal or hexadecimal parts followed by one or more trailing dots, because a handler can read that spelling as an IPv4 address and connect to that address while the rest of the process reads a name.', self::escape($host)), $request); + } + + /** + * Reports whether a value has the transport's inet_aton-style shape: one + * to four decimal, 0-prefixed octal, or 0x-prefixed hexadecimal parts. + * + * Range and 32-bit overflow checks are deliberately omitted. This may + * reject a trailing-dot spelling the transport reads as a name, but avoids + * missing one it resolves as an address. No PCRE is used. + */ + public static function isNumericIpv4Host(string $host): bool + { + if ($host === '') { + return false; + } + + $parts = \explode('.', $host); + + if (\count($parts) > 4) { + return false; + } + + foreach ($parts as $part) { + if (!self::isNumericIpv4Part($part)) { + return false; + } + } + + return true; + } + + private static function isNumericIpv4Part(string $part): bool + { + if ($part === '') { + return false; + } + + if ($part[0] === '0' && isset($part[1]) && ($part[1] === 'x' || $part[1] === 'X')) { + return \strlen($part) > 2 && \strspn($part, '0123456789abcdefABCDEF', 2) === \strlen($part) - 2; + } + + $digits = $part[0] === '0' ? '01234567' : '0123456789'; + + return \strspn($part, $digits) === \strlen($part); + } + + /** + * Escapes non-printable bytes as uppercase \xNN for safe diagnostics. + * Printable delimiters and dots stay visible. The result is not a + * reversible encoding. + */ + private static function escape(string $value): string + { + $escaped = ''; + + for ($offset = 0, $length = \strlen($value); $offset < $length; ++$offset) { + $byte = \ord($value[$offset]); + $escaped .= $byte >= 0x21 && $byte <= 0x7E ? $value[$offset] : \sprintf('\\x%02X', $byte); + } + + return $escaped; + } +} diff --git a/core/vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php b/core/vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php index 95e60079e1..cabf84c9b0 100644 --- a/core/vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php +++ b/core/vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php @@ -4,6 +4,7 @@ use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Exception\TransferException; use GuzzleHttp\Multiplexing; use GuzzleHttp\Promise as P; use GuzzleHttp\Promise\FulfilledPromise; @@ -187,11 +188,10 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte } catch (\InvalidArgumentException $e) { throw $e; } catch (\Exception $e) { - // Determine if the error was a networking error. - if (self::isConnectionError($e->getMessage())) { - $e = new ConnectException($e->getMessage(), $request, $e); - } else { - $e = $e instanceof RequestException ? $e : new RequestException($e->getMessage(), $request, null, $e); + if (!$e instanceof TransferException) { + $e = self::isConnectionError($e->getMessage()) + ? new ConnectException($e->getMessage(), $request, $e) + : new RequestException($e->getMessage(), $request, null, $e); } $this->invokeStats($options, $request, $startTime, null, $e); @@ -428,6 +428,8 @@ private function createStream(RequestInterface $request, array $options) throw new RequestException('URI must include a scheme and host. Use an absolute URI, a network-path reference starting with //, or configure a base_uri.', $request); } + HostValidator::assertRequestHost($request); + // HTTP/1.1 streams using the PHP stream wrapper require a // Connection: close header if ($request->getProtocolVersion() === '1.1' @@ -528,6 +530,18 @@ private function resolveHost(RequestInterface $request, array $options): UriInte $uri = $request->getUri(); $host = $uri->getHost(); + + // Fold a numeric IPv4 spelling to the dotted quad libcurl connects + // to, rather than leaving it to the platform resolver: macOS reads + // the zero-padded 0177 as decimal 177 where glibc, musl and FreeBSD + // read octal 127. The Host header is serialized from the request and + // stays as written; the TLS peer name follows the same fold. + $canonicalHost = self::canonicalConnectionHost($host); + if ($canonicalHost !== $host) { + $uri = $uri->withHost($canonicalHost); + $host = $canonicalHost; + } + $hostForIpCheck = $host !== '' && $host[0] === '[' && \substr($host, -1) === ']' ? \substr($host, 1, -1) : $host; @@ -553,6 +567,84 @@ private function resolveHost(RequestInterface $request, array $options): UriInte return $uri; } + /** + * Returns a numeric IPv4 spelling folded to the dotted quad libcurl's + * ipv4_normalize() produces, and every other host unchanged. + */ + private static function canonicalConnectionHost(string $host): string + { + $binary = self::numericIpv4ToBinary($host); + if ($binary === null) { + return $host; + } + + return (string) \inet_ntop($binary); + } + + /** + * Returns the four-byte binary form of a host that a transport reads as a + * numeric IPv4 address, or null when it reads it as a name. + * + * The shape test is HostValidator::isNumericIpv4Host(); this method adds + * the range checks that predicate omits: every part but the last must fit + * one octet, and the last must fit the octets the earlier parts left. A + * trailing root dot is not swallowed, unlike libcurl 8.21.0 and later, + * because assertRequestHost() rejects that spelling first. + */ + private static function numericIpv4ToBinary(string $host): ?string + { + if (!HostValidator::isNumericIpv4Host($host)) { + return null; + } + + $values = []; + foreach (\explode('.', $host) as $part) { + $values[] = self::numericIpv4PartValue($part); + } + + // Every accepted value is a whole number no larger than 0xFFFFFFFF, + // which a float holds exactly, so the arithmetic below is correct on a + // 32-bit build too, where the widest part overflows an integer. + $address = (float) \array_pop($values); + + $packed = ''; + foreach ($values as $value) { + if ($value > 255.0) { + return null; + } + + $packed .= \chr((int) $value); + } + + $width = 4 - \count($values); + if ($address >= 256.0 ** $width) { + return null; + } + + for ($shift = $width - 1; $shift >= 0; --$shift) { + $packed .= \chr((int) \fmod(\floor($address / 256.0 ** $shift), 256.0)); + } + + return $packed; + } + + /** + * Returns the value of one accepted part as a float, so a part filling + * all four octets such as 2130706433 stays exact on every integer width. + */ + private static function numericIpv4PartValue(string $part): float + { + if ($part[0] === '0' && isset($part[1]) && ($part[1] === 'x' || $part[1] === 'X')) { + return (float) \hexdec((string) \substr($part, 2)); + } + + if ($part[0] === '0') { + return (float) \octdec($part); + } + + return (float) $part; + } + private function getDefaultContext(RequestInterface $request): array { $headers = ''; @@ -582,7 +674,7 @@ private function getDefaultContext(RequestInterface $request): array 'follow_location' => 0, ], 'ssl' => [ - 'peer_name' => $request->getUri()->getHost(), + 'peer_name' => self::canonicalConnectionHost($request->getUri()->getHost()), ], ]; diff --git a/core/vendor/guzzlehttp/promises/CHANGELOG.md b/core/vendor/guzzlehttp/promises/CHANGELOG.md index e9df8b8800..7e4c822a4b 100644 --- a/core/vendor/guzzlehttp/promises/CHANGELOG.md +++ b/core/vendor/guzzlehttp/promises/CHANGELOG.md @@ -1,6 +1,14 @@ # CHANGELOG +## 2.5.2 - 2026-08-05 + +### Fixed + +- Fixed `EachPromise` abandoning its aggregate when the pending window drains unsettled +- Fixed `EachPromise` admitting new work after its aggregate has settled + + ## 2.5.1 - 2026-07-08 ### Fixed diff --git a/core/vendor/guzzlehttp/promises/src/EachPromise.php b/core/vendor/guzzlehttp/promises/src/EachPromise.php index 4f8976224b..5de9feb95a 100644 --- a/core/vendor/guzzlehttp/promises/src/EachPromise.php +++ b/core/vendor/guzzlehttp/promises/src/EachPromise.php @@ -34,6 +34,9 @@ class EachPromise implements PromisorInterface /** @var bool|null */ private $mutex; + /** @var bool */ + private $stepWhileLocked = false; + /** * Configuration hash can include the following key value pairs: * @@ -123,16 +126,23 @@ private function createPromise(): void { $this->mutex = false; $this->aggregate = new Promise(function (): void { - if ($this->checkIfFinished()) { - return; - } - reset($this->pending); - // Consume a potentially fluctuating list of promises while - // ensuring that indexes are maintained (precluding array_shift). - while ($promise = current($this->pending)) { - next($this->pending); - $promise->wait(); - if (Is::settled($this->aggregate)) { + while (true) { + if ($this->checkIfFinished()) { + return; + } + reset($this->pending); + // Consume a potentially fluctuating list of promises while + // ensuring that indexes are maintained (precluding array_shift). + while ($promise = current($this->pending)) { + next($this->pending); + $promise->wait(); + if (Is::settled($this->aggregate)) { + return; + } + } + // Refill and re-sweep; give up only when nothing remains. + $this->refillPending(); + if (Is::settled($this->aggregate) || !$this->pending) { return; } } @@ -162,6 +172,10 @@ private function refillPending(): void $concurrency = is_callable($this->concurrency) ? ($this->concurrency)(count($this->pending)) : $this->concurrency; + // The callable can settle the aggregate; admit nothing more. + if (Is::settled($this->aggregate)) { + return; + } $concurrency = max($concurrency - count($this->pending), 0); // Concurrency may be set to 0 to disallow new promises. if (!$concurrency) { @@ -223,6 +237,8 @@ private function advanceIterator(): bool // Place a lock on the iterator so that we ensure to not recurse, // preventing fatal generator errors. if ($this->mutex) { + $this->stepWhileLocked = true; + return false; } @@ -231,14 +247,22 @@ private function advanceIterator(): bool try { $this->iterable->next(); $this->mutex = false; - - return true; } catch (\Throwable $e) { $this->aggregate->reject($e); $this->mutex = false; return false; } + + // Run the completion check that locked steps skipped. + if ($this->stepWhileLocked) { + $this->stepWhileLocked = false; + if (!Is::settled($this->aggregate)) { + $this->checkIfFinished(); + } + } + + return true; } private function step(int $idx): void @@ -259,6 +283,7 @@ private function step(int $idx): void } } + /** @phpstan-impure */ private function checkIfFinished(): bool { if (!$this->pending && !$this->iterable->valid()) { diff --git a/core/vendor/illuminate/console/Scheduling/CronExpressionTimezoneConverter.php b/core/vendor/illuminate/console/Scheduling/CronExpressionTimezoneConverter.php index 04ee0b8827..d383d2b966 100644 --- a/core/vendor/illuminate/console/Scheduling/CronExpressionTimezoneConverter.php +++ b/core/vendor/illuminate/console/Scheduling/CronExpressionTimezoneConverter.php @@ -4,6 +4,7 @@ use DateTimeZone; use Illuminate\Support\Carbon; +use Throwable; class CronExpressionTimezoneConverter { @@ -20,34 +21,27 @@ public static function forEvent(Event $event, DateTimeZone $timezone) { $eventTimezone = static::resolveEventTimezone($event, $timezone); - [$totalOffsetMinutes, $hourOffset, $minuteOffset] = static::offsetComponents( - $eventTimezone, $timezone + $offsetComponents = static::offsetComponents( + $event, $eventTimezone, $timezone ); - if ($totalOffsetMinutes === 0) { + if (is_null($offsetComponents)) { return [$event->expression]; } - $segments = preg_split("/\s+/", $event->expression); - $minuteGroups = static::shiftAndGroup($segments[0], $minuteOffset, 60); - - $expressions = []; + [$totalOffsetMinutes, $hourOffset, $minuteOffset] = $offsetComponents; - foreach ($minuteGroups as $minuteCarry => $minuteValues) { - $hourGroups = static::shiftAndGroup($segments[1], $hourOffset + $minuteCarry, 24); + if ($totalOffsetMinutes === 0) { + return [$event->expression]; + } - foreach ($hourGroups as $hourCarry => $hourValues) { - $parts = $segments; - $parts[0] = $minuteValues; - $parts[1] = $hourValues; + $segments = preg_split("/\s+/", trim($event->expression)); - foreach (static::expressionsForHourCarry($segments, $parts, $hourCarry) as $expression) { - $expressions[] = $expression; - } - } + if (count($segments) !== 5) { + return [$event->expression]; } - return $expressions; + return static::convert($segments, $hourOffset, $minuteOffset) ?? [$event->expression]; } /** @@ -67,26 +61,87 @@ protected static function resolveEventTimezone(Event $event, DateTimeZone $defau /** * Get offset components between the event and display timezones. * - * @return array{int, int, int} + * @return array{int, int, int}|null */ - protected static function offsetComponents(DateTimeZone $eventTimezone, DateTimeZone $displayTimezone) + protected static function offsetComponents(Event $event, DateTimeZone $eventTimezone, DateTimeZone $displayTimezone) { - $now = Carbon::now(); + try { + $at = $event->nextRunDate(Carbon::now()); + $nextAt = $event->nextRunDate(Carbon::now(), 1); + } catch (Throwable) { + $at = Carbon::now(); + $nextAt = null; + } $totalOffsetMinutes = intdiv( - $displayTimezone->getOffset($now) - $eventTimezone->getOffset($now), + $displayTimezone->getOffset($at) - $eventTimezone->getOffset($at), 60 ); + if ($nextAt && $totalOffsetMinutes !== intdiv( + $displayTimezone->getOffset($nextAt) - $eventTimezone->getOffset($nextAt), + 60 + )) { + return null; + } + return [$totalOffsetMinutes, intdiv($totalOffsetMinutes, 60), $totalOffsetMinutes % 60]; } + /** + * Convert the expression segments by the given offsets. + * + * @param array';
echo Tracy\Helpers::escapeHtml($message) /* pos 10:14 */;
diff --git a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/BlueScreen.php b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/BlueScreen.php
index 1ff43695c8..a670f58f0c 100644
--- a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/BlueScreen.php
+++ b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/BlueScreen.php
@@ -7,7 +7,7 @@
namespace Tracy;
-use function in_array;
+use function count, in_array;
use const ARRAY_FILTER_USE_KEY, ENT_IGNORE, PHP_VERSION_ID;
@@ -468,7 +468,7 @@ public function getAgentDumper(): \Closure
public function formatMessage(\Throwable $exception): string
{
- $msg = Helpers::encodeString(trim((string) $exception->getMessage()), self::MaxMessageLength, showWhitespaces: false);
+ $msg = Helpers::encodeString(trim($exception->getMessage()), self::MaxMessageLength, showWhitespaces: false);
// highlight 'string'
$msg = preg_replace(
@@ -481,9 +481,10 @@ public function formatMessage(\Throwable $exception): string
$msg = preg_replace_callback(
'#(\w+\\\[\w\\\]+\w)(?:::(\w+))?#',
function ($m) {
- if (isset($m[2]) && method_exists($m[1], $m[2])) {
+ $classLike = class_exists($m[1], autoload: false) || interface_exists($m[1], autoload: false) || trait_exists($m[1], autoload: false);
+ if ($classLike && isset($m[2]) && method_exists($m[1], $m[2])) {
$r = new \ReflectionMethod($m[1], $m[2]);
- } elseif (class_exists($m[1], autoload: false) || interface_exists($m[1], autoload: false)) {
+ } elseif ($classLike) {
$r = new \ReflectionClass($m[1]);
}
diff --git a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/CodeHighlighter.php b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/CodeHighlighter.php
index 8d7003a309..0820b4a4ac 100644
--- a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/CodeHighlighter.php
+++ b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/CodeHighlighter.php
@@ -87,7 +87,7 @@ private static function highlightPhpCode(string $code): string
$code = str_replace("\r\n", "\n", $code);
$code = preg_replace('#(__halt_compiler\s*\(\)\s*;).*#is', '$1', $code);
$code = rtrim($code);
- $code = preg_replace('#/\*sensitive\{\*/.*?/\*\}\*/#s', Dumper\Describer::HiddenValue, $code);
+ $code = preg_replace('#/\*sensitive\{\*/.*?/\*}\*/#s', Dumper\Describer::HiddenValue, $code);
$last = $out = '';
foreach (\PhpToken::tokenize($code) as $token) {
diff --git a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/assets/bluescreen.js b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/assets/bluescreen.js
index c8983b2071..9e53d88bc4 100644
--- a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/assets/bluescreen.js
+++ b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/assets/bluescreen.js
@@ -9,7 +9,7 @@ class BlueScreen {
let blueScreen = document.getElementById('tracy-bs');
document.documentElement.classList.add('tracy-bs-visible');
- if (navigator.platform.indexOf('Mac') > -1) {
+ if (navigator.userAgent.includes('Mac')) {
blueScreen.classList.add('tracy-mac');
}
@@ -40,7 +40,7 @@ class BlueScreen {
static globalInit() {
// enables toggling via ESC
document.addEventListener('keyup', (e) => {
- if (e.keyCode === 27 && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) { // ESC
+ if (e.key === 'Escape' && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) {
Tracy.Toggle.toggle(document.getElementById('tracy-bs-toggle'));
}
});
diff --git a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/agent.phtml b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/agent.phtml
index fdaf58fb16..d0ab024516 100644
--- a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/agent.phtml
+++ b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/agent.phtml
@@ -63,7 +63,7 @@ Mapped source: ';
echo "\n";
foreach (Helpers::getExceptionChain($exception) as $i => $ex) /* pos 46:1 */ {
$title = $blueScreen->getExceptionTitle($ex) /* pos 47:2 */;
- $code = $ex->getCode() ? ' #' . $ex->getCode() : '' /* pos 48:2 */;
+ $code = $ex->getCode() ? ' #' . $ex->getCode() : null /* pos 48:2 */;
if ($i === 0) /* pos 49:2 */ {
echo '# ';
echo Tracy\Helpers::escapeMd($title) /* pos 50:5 */;
diff --git a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/page.phtml b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/page.phtml
index 587232dab1..b6e2bb73b0 100644
--- a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/page.phtml
+++ b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/page.phtml
@@ -8,7 +8,7 @@ use Tracy\Helpers;
/** @var string $js */
/** @var string $source */
$title = $blueScreen->getExceptionTitle($exception) /* pos 8:1 */;
-$code = $exception->getCode() ? ' #' . $exception->getCode() : '' /* pos 9:1 */;
+$code = $exception->getCode() ? ' #' . $exception->getCode() : null /* pos 9:1 */;
$chain = Helpers::getExceptionChain($exception) /* pos 10:1 */;
echo '
@@ -39,7 +39,7 @@ if (count($chain) > 1) /* pos 24:2 */ {
echo Tracy\Helpers::escapeHtml(get_debug_type($ex)) /* pos 26:12 */;
echo ': ';
echo Tracy\Helpers::escapeHtml($ex->getMessage()) /* pos 26:35 */;
- echo Tracy\Helpers::escapeHtml($ex->getCode() ? ' #' . $ex->getCode() : '') /* pos 26:54 */;
+ echo Tracy\Helpers::escapeHtml($ex->getCode() ? ' #' . $ex->getCode() : null) /* pos 26:54 */;
echo '
';
diff --git a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/section-header.phtml b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/section-header.phtml
index f6d912bc02..4cdd26a209 100644
--- a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/section-header.phtml
+++ b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/section-header.phtml
@@ -7,7 +7,7 @@ use Tracy\Helpers;
/** @var Tracy\BlueScreen $blueScreen */
echo "\n";
$title = $blueScreen->getExceptionTitle($ex) /* pos 7:1 */;
-$code = $ex->getCode() ? ' #' . $ex->getCode() : '' /* pos 8:1 */;
+$code = $ex->getCode() ? ' #' . $ex->getCode() : null /* pos 8:1 */;
echo '
' . Helpers::escapeHtml($s) . '' + : $s; + try { $logFile = Debugger::log($exception, Debugger::EXCEPTION); } catch (\Throwable $e) { - echo "$exception\nTracy is unable to log error: {$e->getMessage()}\n"; + echo $escape("$exception\nTracy is unable to log error: {$e->getMessage()}\n"); return; } @@ -62,11 +66,17 @@ private function renderExceptionCli(\Throwable $exception): void header("X-Tracy-Error-Log: $logFile", replace: false); } + if (Helpers::isAgent() && !Helpers::isCli() && !Helpers::isHtmlMode()) { + // non-HTML HTTP response for an agent carries the markdown report directly in the body + echo $this->blueScreen->renderAgent($exception) . ($logFile ? "\n(stored in $logFile)\n" : ''); + return; + } + if (Helpers::detectColors() && @is_file($exception->getFile())) { echo "\n\n" . CodeHighlighter::highlightPhpCli((string) file_get_contents($exception->getFile()), $exception->getLine()) . "\n"; } - echo "$exception\n" . ($logFile ? "\n(stored in $logFile)\n" : ''); + echo $escape("$exception\n" . ($logFile ? "\n(stored in $logFile)\n" : '')); if ($logFile && Debugger::$browser) { exec(Debugger::$browser . ' ' . escapeshellarg(strtr($logFile, Debugger::$editorMapping))); } diff --git a/core/vendor/tracy/tracy/src/Tracy/Dumper/Describer.php b/core/vendor/tracy/tracy/src/Tracy/Dumper/Describer.php index d71b67466c..76a938c36d 100644 --- a/core/vendor/tracy/tracy/src/Tracy/Dumper/Describer.php +++ b/core/vendor/tracy/tracy/src/Tracy/Dumper/Describer.php @@ -8,7 +8,7 @@ namespace Tracy\Dumper; use Tracy\Helpers; -use function array_map, array_slice, class_exists, count, explode, file, get_debug_type, get_resource_type, gettype, htmlspecialchars, implode, is_bool, is_file, is_int, is_resource, is_string, is_subclass_of, json_encode, method_exists, preg_match, spl_object_id, str_replace, strlen, strpos, strtolower, trim, uksort; +use function array_map, array_slice, class_exists, count, file, get_debug_type, get_resource_type, htmlspecialchars, implode, interface_exists, is_array, is_bool, is_float, is_int, is_object, is_resource, is_string, is_subclass_of, json_encode, method_exists, preg_match, spl_object_id, str_replace, strlen, strpos, strtolower, trim, uksort; /** @@ -20,7 +20,7 @@ final class Describer public const HiddenValue = '*****'; // Number.MAX_SAFE_INTEGER - private const JsSafeInteger = 1 << 53 - 1; + private const JsSafeInteger = (1 << 53) - 1; public int $maxDepth = 7; public int $maxLength = 150; @@ -53,7 +53,10 @@ final class Describer public function describe(mixed $var): \stdClass { - uksort($this->objectExposers, fn($a, $b): int => $b === '' || (class_exists($a, autoload: false) && is_subclass_of($a, $b)) ? -1 : 1); + // exposers are sorted from the most specific type to the most general; '' acts as the universal supertype + $isSubtypeOf = fn(string $type, string $parent): bool => $parent === '' + || ((class_exists($type, autoload: false) || interface_exists($type, autoload: false)) && is_subclass_of($type, $parent)); + uksort($this->objectExposers, fn($a, $b): int => $isSubtypeOf($b, $a) <=> $isSubtypeOf($a, $b)); try { return (object) [ @@ -72,12 +75,15 @@ public function describe(mixed $var): \stdClass private function describeVar(mixed $var, int $depth = 0, ?int $refId = null): mixed { - if ($var === null || is_bool($var)) { - return $var; - } - - $m = 'describe' . explode(' ', gettype($var))[0]; - return $this->$m($var, $depth, $refId); + return match (true) { + $var === null, is_bool($var) => $var, + is_int($var) => $this->describeInteger($var), + is_float($var) => $this->describeDouble($var), + is_string($var) => $this->describeString($var, $depth), + is_array($var) => $this->describeArray($var, $depth, $refId), + is_object($var) => $this->describeObject($var, $depth), + default => $this->describeResource($var, $depth), // open or closed resource + }; } @@ -193,7 +199,10 @@ private function describeObject(object $obj, int $depth = 0): Value $value->items = []; $props = $this->exposeObject($obj, $value); foreach ($props ?? [] as $k => $v) { - $this->addPropertyTo($value, (string) $k, $v, Value::PropertyVirtual, $this->getReferenceId($props ?? [], $k)); + $described = $this->isSensitive((string) $k, $v, get_debug_type($obj)) // props come from user callbacks (__debugInfo, custom exposers) + ? new Value(Value::TypeText, self::hideValue($v)) + : null; + $this->addPropertyTo($value, (string) $k, $v, Value::PropertyVirtual, $this->getReferenceId($props ?? [], $k), described: $described); } } diff --git a/core/vendor/tracy/tracy/src/Tracy/Dumper/Dumper.php b/core/vendor/tracy/tracy/src/Tracy/Dumper/Dumper.php index 5bee48b404..552967c9a4 100644 --- a/core/vendor/tracy/tracy/src/Tracy/Dumper/Dumper.php +++ b/core/vendor/tracy/tracy/src/Tracy/Dumper/Dumper.php @@ -12,6 +12,7 @@ use Tracy\Dumper\Describer; use Tracy\Dumper\Exposer; use Tracy\Dumper\Renderer; +use Uri; use function array_flip, array_map, file_get_contents, fwrite, str_replace; use const STDOUT; @@ -69,7 +70,6 @@ class Dumper public static array $resources = [ 'stream' => 'stream_get_meta_data', 'stream-context' => 'stream_context_get_options', - 'curl' => 'curl_getinfo', ]; /** @var array
- let snapshot = JSON.parse(pre.getAttribute('data-tracy-snapshot'));
- pre.removeAttribute('data-tracy-snapshot');
- pre.appendChild(build(JSON.parse(pre.getAttribute('data-tracy-dump')), snapshot, pre.classList.contains('tracy-collapsed')));
- pre.removeAttribute('data-tracy-dump');
- pre.classList.remove('tracy-collapsed');
+ try {
+ let snapshot = JSON.parse(pre.getAttribute('data-tracy-snapshot'));
+ pre.removeAttribute('data-tracy-snapshot');
+ pre.appendChild(build(JSON.parse(pre.getAttribute('data-tracy-dump')), snapshot, pre.classList.contains('tracy-collapsed')));
+ pre.removeAttribute('data-tracy-dump');
+ pre.classList.remove('tracy-collapsed');
+ } catch {
+ // a broken dump must not prevent the remaining dumps from initializing
+ }
});
// snapshots
(context || document).querySelectorAll('meta[itemprop=tracy-snapshot]').forEach((meta) => {
let snapshot = JSON.parse(meta.getAttribute('content'));
meta.parentElement.querySelectorAll('[data-tracy-dump]').forEach((pre) => { //
- if (pre.closest('[data-tracy-snapshot]')) { // ignore unrelated
- return;
+ try {
+ if (pre.closest('[data-tracy-snapshot]')) { // ignore unrelated
+ return;
+ }
+ pre.appendChild(build(JSON.parse(pre.getAttribute('data-tracy-dump')), snapshot, pre.classList.contains('tracy-collapsed')));
+ pre.removeAttribute('data-tracy-dump');
+ pre.classList.remove('tracy-collapsed');
+ } catch {
+ // a broken dump must not prevent the remaining dumps from initializing
}
- pre.appendChild(build(JSON.parse(pre.getAttribute('data-tracy-dump')), snapshot, pre.classList.contains('tracy-collapsed')));
- pre.removeAttribute('data-tracy-dump');
- pre.classList.remove('tracy-collapsed');
});
// must be left for debug bar panel content
});
diff --git a/core/vendor/tracy/tracy/src/Tracy/Helpers.php b/core/vendor/tracy/tracy/src/Tracy/Helpers.php
index e439f0787f..96258dd709 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Helpers.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Helpers.php
@@ -218,7 +218,7 @@ public static function getSource(): string
. (isset($_SERVER['argv']) ? ': ' . implode(' ', array_map(self::escapeArg(...), $_SERVER['argv'])) : '');
} elseif (isset($_SERVER['REQUEST_URI'])) {
- return (!empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://')
+ return (self::isHttps() ? 'https://' : 'http://')
. ($_SERVER['HTTP_HOST'] ?? '')
. $_SERVER['REQUEST_URI'];
@@ -228,6 +228,13 @@ public static function getSource(): string
}
+ /** @internal */
+ public static function isHttps(): bool
+ {
+ return !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') !== 0;
+ }
+
+
/** @internal */
public static function improveException(\Throwable $e): void
{
@@ -385,7 +392,8 @@ public static function consoleLog(string $data): void
/** @internal */
public static function isAgent(): bool
{
- return ($_COOKIE['tracy-webdriver'] ?? null) === '1';
+ return ($_COOKIE['tracy-webdriver'] ?? null) === '1' // set by bar.js when navigator.webdriver
+ || isset($_SERVER['HTTP_X_TRACY_AGENT']); // non-browser agents (curl, HTTP clients)
}
diff --git a/core/vendor/tracy/tracy/src/Tracy/Logger/Logger.php b/core/vendor/tracy/tracy/src/Tracy/Logger/Logger.php
index 62415339ca..39b5d458cb 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Logger/Logger.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Logger/Logger.php
@@ -8,7 +8,7 @@
namespace Tracy;
use function in_array, is_string;
-use const DIRECTORY_SEPARATOR, FILE_APPEND, LOCK_EX, PHP_EOL;
+use const DIRECTORY_SEPARATOR, FILE_APPEND, LOCK_EX, LOCK_UN, PHP_EOL;
/**
@@ -28,7 +28,7 @@ class Logger implements ILogger
/** @var string|int interval for sending email is 2 days */
public $emailSnooze = '2 days';
- /** @var callable(mixed $message, string $email): void handler for sending emails */
+ /** @var ?callable(mixed $message, string $email): void handler for sending emails, null disables sending */
public $mailer;
/** @var ?BlueScreen */
@@ -164,17 +164,52 @@ protected function logException(\Throwable $exception, ?string $file = null): st
*/
protected function sendEmail($message): void
{
- $snooze = is_numeric($this->emailSnooze)
- ? $this->emailSnooze
- : strtotime($this->emailSnooze) - time();
-
- if (
- $this->email
- && $this->mailer
- && @filemtime($this->directory . '/email-sent') + $snooze < time() // @ file may not exist
- && @file_put_contents($this->directory . '/email-sent', 'sent') // @ file may not be writable
- ) {
- ($this->mailer)($message, implode(', ', (array) $this->email));
+ if (!$this->email || !$this->mailer) {
+ return;
+ }
+
+ if (is_numeric($this->emailSnooze)) {
+ $snooze = (int) $this->emailSnooze;
+ } elseif (($time = strtotime($this->emailSnooze)) !== false) {
+ $snooze = $time - time();
+ } else {
+ throw new \InvalidArgumentException("Invalid time interval '$this->emailSnooze'.");
+ }
+
+ $this->throttle(
+ $this->directory . '/email-sent',
+ $snooze,
+ fn() => ($this->mailer)($message, implode(', ', (array) $this->email)),
+ );
+ }
+
+
+ /**
+ * Executes the action at most once per given interval. The check is atomic across
+ * concurrent requests and the interval restarts only after the action succeeds.
+ * @param \Closure(): void $action
+ */
+ private function throttle(string $lockFile, int $interval, \Closure $action): void
+ {
+ $handle = @fopen($lockFile, 'c+'); // @ - file may not be writable
+ if (!$handle) {
+ return;
+ }
+
+ try {
+ if (!flock($handle, LOCK_EX)) {
+ return;
+ }
+
+ $stat = fstat($handle);
+ if ($stat['size'] === 0 || $stat['mtime'] + $interval < time()) {
+ $action();
+ ftruncate($handle, 0);
+ fwrite($handle, date('c'));
+ }
+ } finally {
+ flock($handle, LOCK_UN);
+ fclose($handle);
}
}
diff --git a/core/vendor/tracy/tracy/src/Tracy/Session/FileSession.php b/core/vendor/tracy/tracy/src/Tracy/Session/FileSession.php
index f3cc3e218b..2c13182f48 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Session/FileSession.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Session/FileSession.php
@@ -56,12 +56,20 @@ private function open(): void
|| !($file = @fopen($path = $this->dir . '/' . self::FilePrefix . $id, 'r+')) // intentionally @
) {
$id = Helpers::createId();
- setcookie($this->cookieName, $id, time() + self::CookieLifetime, '/', '', secure: false, httponly: true);
+ setcookie($this->cookieName, $id, [
+ 'expires' => time() + self::CookieLifetime,
+ 'path' => '/',
+ 'secure' => Helpers::isHttps(),
+ 'httponly' => true,
+ 'samesite' => 'Lax',
+ ]);
$file = @fopen($path = $this->dir . '/' . self::FilePrefix . $id, 'c+'); // intentionally @
if ($file === false) {
throw new \RuntimeException("Unable to create file '$path'. " . (error_get_last()['message'] ?? ''));
}
+
+ @chmod($path, 0o600); // @ - may not be supported by the filesystem
}
if (!@flock($file, LOCK_EX)) { // intentionally @
@@ -69,7 +77,7 @@ private function open(): void
}
$this->file = $file;
- $this->data = @unserialize(stream_get_contents($this->file)) ?: []; // @ - file may be empty
+ $this->data = @unserialize(stream_get_contents($this->file), ['allowed_classes' => false]) ?: []; // @ - file may be empty; data contain no objects
if (mt_rand() / mt_getrandmax() < $this->gcProbability) {
$this->clean();
@@ -87,8 +95,8 @@ public function clean(): void
{
$old = strtotime('-1 week');
foreach (glob($this->dir . '/' . self::FilePrefix . '*') ?: [] as $file) {
- if (filemtime($file) < $old) {
- unlink($file);
+ if (@filemtime($file) < $old) { // @ - file may be deleted by concurrent GC
+ @unlink($file); // @ - file may be deleted by concurrent GC
}
}
}
diff --git a/core/vendor/tracy/tracy/src/Tracy/assets/toggle.js b/core/vendor/tracy/tracy/src/Tracy/assets/toggle.js
index aabefb5f17..bcc82a9f56 100644
--- a/core/vendor/tracy/tracy/src/Tracy/assets/toggle.js
+++ b/core/vendor/tracy/tracy/src/Tracy/assets/toggle.js
@@ -17,7 +17,7 @@ class Toggle {
if (
!e.shiftKey && !e.ctrlKey && !e.metaKey
&& (el = e.target.closest('.tracy-toggle'))
- && Math.pow(start[0] - e.clientX, 2) + Math.pow(start[1] - e.clientY, 2) < MOVE_THRESHOLD
+ && (!start || Math.pow(start[0] - e.clientX, 2) + Math.pow(start[1] - e.clientY, 2) < MOVE_THRESHOLD) // no start = keyboard activation
) {
Toggle.toggle(el, undefined, e);
e.preventDefault();
@@ -31,7 +31,7 @@ class Toggle {
// changes element visibility
static toggle(el, expand, e) {
let collapsed = el.classList.contains('tracy-collapsed'),
- ref = el.getAttribute('data-tracy-ref') || el.getAttribute('href', 2),
+ ref = el.getAttribute('data-tracy-ref') || el.getAttribute('href'),
dest = el;
if (typeof expand === 'undefined') {
@@ -73,7 +73,12 @@ class Toggle {
}
});
- let toggles = JSON.parse(sessionStorage.getItem('tracy-toggles-' + baseEl.id));
+ let toggles;
+ try {
+ toggles = JSON.parse(sessionStorage.getItem('tracy-toggles-' + baseEl.id));
+ } catch {
+ // ignore corrupt data
+ }
if (toggles && restore !== false) {
toggles.forEach((item) => {
let el = baseEl;
diff --git a/install/stubs/migrations/2026_08_20_000000_add_cachepwd_expiry_to_users.php b/install/stubs/migrations/2026_08_20_000000_add_cachepwd_expiry_to_users.php
new file mode 100644
index 0000000000..e09191842e
--- /dev/null
+++ b/install/stubs/migrations/2026_08_20_000000_add_cachepwd_expiry_to_users.php
@@ -0,0 +1,40 @@
+dateTime('cachepwd_valid_to')->nullable()->after('cachepwd');
+ });
+
+ // From here on an empty deadline means "never expires" (pwd_repair_minutes = 0).
+ // Tokens that already exist have no deadline recorded and would silently become
+ // eternal, so they are cleared: their owners simply request a new link.
+ DB::table('users')->where('cachepwd', '<>', '')->update([
+ 'cachepwd' => '',
+ 'cachepwd_valid_to' => null,
+ ]);
+ }
+
+ public function down() {
+ if (Schema::hasTable('users') && Schema::hasColumn('users', 'cachepwd_valid_to')) {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn('cachepwd_valid_to');
+ });
+ }
+ }
+}
diff --git a/manager/actions/mutate_web_user.dynamic.php b/manager/actions/mutate_web_user.dynamic.php
index 3e06534b45..d26b88672d 100755
--- a/manager/actions/mutate_web_user.dynamic.php
+++ b/manager/actions/mutate_web_user.dynamic.php
@@ -355,7 +355,7 @@ function evoRenderTvImageCheck(a) {