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 $segments + * @param int $hourOffset + * @param int $minuteOffset + * @return array|null + */ + protected static function convert(array $segments, int $hourOffset, int $minuteOffset) + { + $daysAreWildcard = $segments[2] === '*' && $segments[3] === '*' && $segments[4] === '*'; + + $minuteGroups = static::shiftedGroups( + $segments[0], $minuteOffset, 0, 59, + mergeCarries: $daysAreWildcard && static::expand($segments[1], 0, 23) === range(0, 23), + ); + + if (is_null($minuteGroups)) { + return null; + } + + $expressions = []; + + foreach ($minuteGroups as $minuteCarry => $minuteValues) { + $hourGroups = static::shiftedGroups( + $segments[1], $hourOffset + $minuteCarry, 0, 23, mergeCarries: $daysAreWildcard, + ); + + if (is_null($hourGroups)) { + return null; + } + + foreach ($hourGroups as $hourCarry => $hourValues) { + $parts = $segments; + $parts[0] = $minuteValues; + $parts[1] = $hourValues; + + if (is_null($carried = static::expressionsForHourCarry($segments, $parts, $hourCarry))) { + return null; + } + + $expressions = array_merge($expressions, $carried); + } + } + + return $expressions; + } + /** * Build expressions for the given hour carry direction. * * @param array $segments * @param array $parts - * @return array + * @return array|null */ protected static function expressionsForHourCarry(array $segments, array $parts, int $hourCarry) { @@ -94,83 +149,315 @@ protected static function expressionsForHourCarry(array $segments, array $parts, return [implode(' ', $parts)]; } - $parts[4] = static::shiftField($segments[4], $hourCarry, 7); + if ($segments[4] !== '*') { + // Cron treats restricted day-of-month and day-of-week fields as an "or", so they cannot shift together... + if ($segments[2] !== '*' || $segments[3] !== '*') { + return null; + } - $dayGroups = static::shiftAndGroup($segments[2], $hourCarry, 31, min: 1); + if (is_null($days = static::expand($segments[4], 0, 7))) { + return null; + } - $expressions = []; + $parts[4] = static::collapse(static::shiftWrapped($days, $hourCarry, 7), 0, 6); + + return [implode(' ', $parts)]; + } - foreach ($dayGroups as $dayCarry => $dayValues) { - $dayParts = $parts; - $dayParts[2] = $dayValues; + if ($segments[2] === '*' && $segments[3] === '*') { + return [implode(' ', $parts)]; + } + + if (is_null($dayGroups = static::shiftDaysOfMonth($segments[2], $segments[3], $hourCarry))) { + return null; + } + + return array_map(function ($group) use ($parts) { + [$parts[2], $parts[3]] = $group; - if ($dayCarry !== 0) { - $dayParts[3] = static::shiftField($segments[3], $dayCarry, 12, min: 1); + return implode(' ', $parts); + }, $dayGroups); + } + + /** + * Shift the day-of-month field by the given carry, respecting month lengths. + * + * @param string $domField + * @param string $monthField + * @param int $carry + * @return array|null + */ + protected static function shiftDaysOfMonth(string $domField, string $monthField, int $carry) + { + $months = $monthField === '*' ? range(1, 12) : static::expand($monthField, 1, 12); + $days = $domField === '*' ? range(1, 31) : static::expand($domField, 1, 31); + + if (is_null($months) || is_null($days)) { + return null; + } + + $shifted = []; + + foreach ($months as $month) { + foreach ($days as $day) { + if ($month === 2 && $day === 29) { + return null; + } + + if ($day > static::daysInMonth($month)) { + continue; + } + + [$targetMonth, $targetDay] = [$month, $day + $carry]; + + while ($targetDay < 1) { + $targetMonth = $targetMonth === 1 ? 12 : $targetMonth - 1; + + if ($targetMonth === 2) { + return null; + } + + $targetDay += static::daysInMonth($targetMonth); + } + + while ($targetDay > static::daysInMonth($targetMonth)) { + if ($targetMonth === 2) { + return null; + } + + $targetDay -= static::daysInMonth($targetMonth); + $targetMonth = $targetMonth === 12 ? 1 : $targetMonth + 1; + } + + $shifted[$targetMonth][$targetDay] = true; } + } - $expressions[] = implode(' ', $dayParts); + if ($shifted === []) { + return null; } - return $expressions; + $groups = []; + + foreach ($shifted as $month => $days) { + $days = array_keys($days); + + sort($days); + + $groups[implode(',', $days)][] = $month; + } + + return collect($groups)->map(function ($months, $days) { + sort($months); + + return [ + static::collapse(array_map(intval(...), explode(',', $days)), 1, 31), + static::collapse($months, 1, 12), + ]; + })->values()->all(); } /** - * Shift values in a cron field and group them by carry direction. + * Get the number of days in the given month of a non-leap year. + * + * @param int $month + * @return int + */ + protected static function daysInMonth(int $month) + { + return Carbon::create(2023, $month)->daysInMonth; + } + + /** + * Shift a cron field by the given offset and group it by carry direction. * * @param string $field * @param int $offset - * @param int $mod - * @return array + * @param int $min + * @param int $max + * @param bool $mergeCarries + * @return array|null */ - protected static function shiftAndGroup($field, $offset, $mod, $min = 0) + protected static function shiftedGroups(string $field, int $offset, int $min, int $max, bool $mergeCarries) { - if ($offset === 0 || ! preg_match('/^[\d,]+$/', $field)) { + if ($offset === 0) { return [0 => $field]; } - $groups = []; + if (is_null($values = static::expand($field, $min, $max))) { + return null; + } + + $groups = static::shiftAndGroupValues($values, $offset, $max - $min + 1, $min); - foreach (explode(',', $field) as $value) { - $new = (int) $value + $offset; - $carry = 0; + if ($mergeCarries && count($groups) > 1) { + $groups = [0 => static::mergeGroups($groups)]; + } - if ($new >= $mod + $min) { - $carry = 1; - $new -= $mod; - } elseif ($new < $min) { - $carry = -1; - $new += $mod; + return array_map(fn ($group) => static::collapse($group, $min, $max), $groups); + } + + /** + * Expand a cron field into the sorted list of values it matches. + * + * @param string $field + * @param int $min + * @param int $max + * @return array|null + */ + protected static function expand(string $field, int $min, int $max) + { + if ($field === '*') { + return range($min, $max); + } + + $values = []; + + foreach (explode(',', $field) as $part) { + if (! preg_match('/^(\*|\d+(?:-\d+)?)(?:\/(\d+))?$/', $part, $matches)) { + return null; + } + + $step = ($matches[2] ?? '') !== '' ? (int) $matches[2] : null; + + if ($step === 0) { + return null; + } + + if ($matches[1] === '*') { + [$start, $end] = [$min, $max]; + } elseif (str_contains($matches[1], '-')) { + [$start, $end] = array_map(intval(...), explode('-', $matches[1])); + } else { + [$start, $end] = [(int) $matches[1], is_null($step) ? (int) $matches[1] : $max]; } - $groups[$carry][] = $new; + if ($start < $min || $end > $max || $start > $end) { + return null; + } + + for ($value = $start; $value <= $end; $value += $step ?? 1) { + $values[] = $value; + } } - return collect($groups)->map(function ($values) { - sort($values); + $values = array_values(array_unique($values)); - return implode(',', $values); - })->all(); + sort($values); + + return $values; } /** - * Shift a cron field by the given offset. + * Shift values in a cron field and group them by carry direction. * - * @param string $field + * @param array $values * @param int $offset * @param int $mod * @param int $min + * @return array> + */ + protected static function shiftAndGroupValues(array $values, int $offset, int $mod, int $min = 0) + { + $groups = []; + + foreach ($values as $value) { + $carry = (int) floor(($value + $offset - $min) / $mod); + + $groups[$carry][] = $value + $offset - $carry * $mod; + } + + return array_map(function ($group) { + sort($group); + + return $group; + }, $groups); + } + + /** + * Shift values within a cron field, wrapping around its domain. + * + * @param array $values + * @param int $offset + * @param int $mod + * @param int $min + * @return array + */ + protected static function shiftWrapped(array $values, int $offset, int $mod, int $min = 0) + { + $shifted = array_values(array_unique(array_map( + fn ($value) => (($value + $offset - $min) % $mod + $mod) % $mod + $min, $values + ))); + + sort($shifted); + + return $shifted; + } + + /** + * Merge grouped values into a single sorted group. + * + * @param array> $groups + * @return array + */ + protected static function mergeGroups(array $groups) + { + $merged = array_merge(...array_values($groups)); + + sort($merged); + + return $merged; + } + + /** + * Collapse a sorted list of values into the most compact cron field syntax. + * + * @param array $values + * @param int $min + * @param int $max * @return string */ - protected static function shiftField($field, $offset, $mod, $min = 0) + protected static function collapse(array $values, int $min, int $max) { - if ($offset === 0 || ! preg_match('/^[\d,]+$/', $field)) { - return $field; + if ($values === range($min, $max)) { + return '*'; + } + + if (count($values) === 1) { + return (string) $values[0]; } - $shifted = collect(explode(',', $field)) - ->map(fn ($v) => (((int) $v + $offset - $min) % $mod + $mod) % $mod + $min) - ->sort(); + $steps = collect($values) + ->sliding(2) + ->map(fn ($pair) => $pair->last() - $pair->first()) + ->unique(); + + if (count($values) < 3 || $steps->count() > 1) { + return static::collapseRuns($values); + } - return $shifted->implode(','); + [$first, $last, $step] = [$values[0], end($values), $steps->first()]; + + if ($step === 1) { + return "{$first}-{$last}"; + } + + return $first === $min && $last + $step > $max + ? "*/{$step}" + : "{$first}-{$last}/{$step}"; + } + + /** + * Collapse consecutive runs of three or more values into ranges. + * + * @param array $values + * @return string + */ + protected static function collapseRuns(array $values) + { + return collect($values) + ->chunkWhile(fn ($value, $key, $chunk) => $value === $chunk->last() + 1) + ->map(fn ($run) => $run->count() >= 3 ? $run->first().'-'.$run->last() : $run->implode(',')) + ->implode(','); } } diff --git a/core/vendor/illuminate/database/Eloquent/Factories/Factory.php b/core/vendor/illuminate/database/Eloquent/Factories/Factory.php index 1a75907135..82fd5ce1d3 100644 --- a/core/vendor/illuminate/database/Eloquent/Factories/Factory.php +++ b/core/vendor/illuminate/database/Eloquent/Factories/Factory.php @@ -485,6 +485,10 @@ public function insert(array $attributes = [], ?Model $parent = null): void ? $made : $this->newModel()->newCollection([$made]); + if ($madeCollection->isEmpty()) { + return; + } + $model = $madeCollection->first(); if (isset($this->connection)) { diff --git a/core/vendor/illuminate/database/Migrations/DatabaseMigrationRepository.php b/core/vendor/illuminate/database/Migrations/DatabaseMigrationRepository.php index 45c4389d27..17ea95f5aa 100644 --- a/core/vendor/illuminate/database/Migrations/DatabaseMigrationRepository.php +++ b/core/vendor/illuminate/database/Migrations/DatabaseMigrationRepository.php @@ -99,7 +99,7 @@ public function getLast() /** * Get the completed migrations with their batch numbers. * - * @return array[] + * @return array */ public function getMigrationBatches() { diff --git a/core/vendor/illuminate/database/Migrations/MigrationRepositoryInterface.php b/core/vendor/illuminate/database/Migrations/MigrationRepositoryInterface.php index 49cc08e21a..491261884f 100644 --- a/core/vendor/illuminate/database/Migrations/MigrationRepositoryInterface.php +++ b/core/vendor/illuminate/database/Migrations/MigrationRepositoryInterface.php @@ -37,7 +37,7 @@ public function getLast(); /** * Get the completed migrations with their batch numbers. * - * @return array[] + * @return array */ public function getMigrationBatches(); diff --git a/core/vendor/illuminate/database/Query/Grammars/PostgresGrammar.php b/core/vendor/illuminate/database/Query/Grammars/PostgresGrammar.php index 67f6458b91..8efa57e8d8 100644 --- a/core/vendor/illuminate/database/Query/Grammars/PostgresGrammar.php +++ b/core/vendor/illuminate/database/Query/Grammars/PostgresGrammar.php @@ -761,9 +761,17 @@ protected function wrapJsonPathAttributes($path) ->map(fn ($attribute) => $this->parseJsonPathArrayKeys($attribute)) ->collapse() ->map(function ($attribute) use ($quote) { - return filter_var($attribute, FILTER_VALIDATE_INT) !== false - ? $attribute - : $quote.$attribute.$quote; + if (filter_var($attribute, FILTER_VALIDATE_INT) !== false) { + return $attribute; + } + + $attribute = str_replace("'", "''", $attribute); + + if ($quote !== "'") { + $attribute = str_replace($quote, $quote.$quote, $attribute); + } + + return $quote.$attribute.$quote; }) ->all(); } diff --git a/core/vendor/illuminate/database/Schema/ColumnDefinition.php b/core/vendor/illuminate/database/Schema/ColumnDefinition.php index 05618db57c..73caac3761 100644 --- a/core/vendor/illuminate/database/Schema/ColumnDefinition.php +++ b/core/vendor/illuminate/database/Schema/ColumnDefinition.php @@ -30,7 +30,7 @@ * @method $this storedAs(string|\Illuminate\Contracts\Database\Query\Expression $expression) Create a stored generated column (MySQL/PostgreSQL/SQLite) * @method $this type(string $type) Specify a type for the column * @method $this unique(bool|string $indexName = null) Add a unique index - * @method $this unsigned() Set the INTEGER column as UNSIGNED (MySQL) + * @method $this unsigned(bool $value = true) Set the INTEGER column as UNSIGNED if value is true (MySQL) * @method $this useCurrent() Set the TIMESTAMP column to use CURRENT_TIMESTAMP as default value * @method $this useCurrentOnUpdate() Set the TIMESTAMP column to use CURRENT_TIMESTAMP when updating (MySQL) * @method $this virtualAs(string|\Illuminate\Contracts\Database\Query\Expression $expression) Create a virtual generated column (MySQL/PostgreSQL/SQLite) diff --git a/core/vendor/illuminate/filesystem/ReceiveFile.php b/core/vendor/illuminate/filesystem/ReceiveFile.php index bc488d1eb0..2119cb1e66 100644 --- a/core/vendor/illuminate/filesystem/ReceiveFile.php +++ b/core/vendor/illuminate/filesystem/ReceiveFile.php @@ -44,6 +44,6 @@ public function __invoke(Request $request, string $path): Response */ protected function hasValidSignature(Request $request): bool { - return $request->boolean('upload') && $request->hasValidRelativeSignature(); + return filter_var($request->query('upload', false), FILTER_VALIDATE_BOOLEAN) && $request->hasValidRelativeSignature(); } } diff --git a/core/vendor/illuminate/filesystem/ServeFile.php b/core/vendor/illuminate/filesystem/ServeFile.php index e8732cb147..3041da8249 100644 --- a/core/vendor/illuminate/filesystem/ServeFile.php +++ b/core/vendor/illuminate/filesystem/ServeFile.php @@ -54,7 +54,7 @@ function ($response) use ($headers) { */ protected function hasValidSignature(Request $request): bool { - return ! $request->boolean('upload') && ( + return ! filter_var($request->query('upload', false), FILTER_VALIDATE_BOOLEAN) && ( ($this->config['visibility'] ?? 'private') === 'public' || $request->hasValidRelativeSignature() ); diff --git a/core/vendor/illuminate/http/Client/Factory.php b/core/vendor/illuminate/http/Client/Factory.php index c99bace1ce..3f4b8ef17a 100644 --- a/core/vendor/illuminate/http/Client/Factory.php +++ b/core/vendor/illuminate/http/Client/Factory.php @@ -151,6 +151,27 @@ public function globalOptions($options) return $this; } + /** + * Execute a callback while requests are created without global middleware or global options. + * + * @template TReturn + * + * @param (\Closure(): TReturn) $callback + * @return TReturn + */ + public function withoutGlobalConfiguration(Closure $callback) + { + [$middleware, $options] = [$this->globalMiddleware, $this->globalOptions]; + + [$this->globalMiddleware, $this->globalOptions] = [[], []]; + + try { + return $callback(); + } finally { + [$this->globalMiddleware, $this->globalOptions] = [$middleware, $options]; + } + } + /** * Create a new response instance for use during stubbing. * diff --git a/core/vendor/illuminate/routing/UrlGenerator.php b/core/vendor/illuminate/routing/UrlGenerator.php index 5e23a5d1ac..1bb9839d3a 100644 --- a/core/vendor/illuminate/routing/UrlGenerator.php +++ b/core/vendor/illuminate/routing/UrlGenerator.php @@ -451,7 +451,9 @@ public function hasValidRelativeSignature(Request $request, Closure|array $ignor */ public function hasCorrectSignature(Request $request, $absolute = true, Closure|array $ignoreQuery = []) { - $url = $absolute ? $request->url() : '/'.$request->path(); + $url = $absolute + ? rtrim($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo(), '/') + : '/'.$request->path(); $queryString = (new Collection(explode('&', (string) $request->server->get('QUERY_STRING')))) ->reject(function ($parameter) use ($ignoreQuery) { diff --git a/core/vendor/illuminate/support/Facades/Http.php b/core/vendor/illuminate/support/Facades/Http.php index 50bd17818f..1f188d96b4 100644 --- a/core/vendor/illuminate/support/Facades/Http.php +++ b/core/vendor/illuminate/support/Facades/Http.php @@ -9,6 +9,7 @@ * @method static \Illuminate\Http\Client\Factory globalRequestMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\Factory globalResponseMiddleware(callable $middleware) * @method static \Illuminate\Http\Client\Factory globalOptions(\Closure|array $options) + * @method static mixed withoutGlobalConfiguration(\Closure $callback) * @method static \GuzzleHttp\Promise\PromiseInterface response(array|string|null $body = null, int $status = 200, array $headers = []) * @method static \GuzzleHttp\Psr7\Response psr7Response(array|string|null $body = null, int $status = 200, array $headers = []) * @method static \Illuminate\Http\Client\RequestException failedRequest(array|string|null $body = null, int $status = 200, array $headers = []) diff --git a/core/vendor/illuminate/validation/Concerns/ValidatesAttributes.php b/core/vendor/illuminate/validation/Concerns/ValidatesAttributes.php index 36ba2b2e07..40aa476845 100644 --- a/core/vendor/illuminate/validation/Concerns/ValidatesAttributes.php +++ b/core/vendor/illuminate/validation/Concerns/ValidatesAttributes.php @@ -1522,7 +1522,7 @@ public function validateIn($attribute, $value, $parameters) return count(array_diff($value, $parameters)) === 0; } - return ! is_array($value) && in_array((string) $value, $parameters); + return ! is_array($value) && in_array((string) $value, $parameters, true); } /** diff --git a/core/vendor/illuminate/validation/ValidationRuleParser.php b/core/vendor/illuminate/validation/ValidationRuleParser.php index c3dab58864..fdf1838c2c 100644 --- a/core/vendor/illuminate/validation/ValidationRuleParser.php +++ b/core/vendor/illuminate/validation/ValidationRuleParser.php @@ -176,11 +176,13 @@ protected function explodeWildcardRules($results, $attribute, $rules) [$attribute => [$key]] ); - $results = $this->mergeRules($results, $compiled->rules); + foreach ($compiled->rules as $compiledAttribute => $compiledRules) { + $this->mergeRulesForAttributeInto($results, $compiledAttribute, $compiledRules); + } } else { $this->implicitAttributes[$attribute][] = $key; - $results = $this->mergeRules($results, $key, $rule); + $this->mergeRulesForAttributeInto($results, $key, $rule); } } } @@ -221,14 +223,27 @@ public function mergeRules($results, $attribute, $rules = []) * @return array */ protected function mergeRulesForAttribute($results, $attribute, $rules) + { + $this->mergeRulesForAttributeInto($results, $attribute, $rules); + + return $results; + } + + /** + * Merge additional rules into a given attribute by reference. + * + * @param array $results + * @param string $attribute + * @param string|array $rules + * @return void + */ + private function mergeRulesForAttributeInto(&$results, $attribute, $rules) { $merge = head($this->explodeRules([$rules])); $results[$attribute] = array_merge( isset($results[$attribute]) ? $this->explodeExplicitRule($results[$attribute], $attribute) : [], $merge ); - - return $results; } /** diff --git a/core/vendor/laravel/prompts/src/Spinner.php b/core/vendor/laravel/prompts/src/Spinner.php index 4029e859d1..8aa7173529 100644 --- a/core/vendor/laravel/prompts/src/Spinner.php +++ b/core/vendor/laravel/prompts/src/Spinner.php @@ -52,6 +52,7 @@ public function spin(Closure $callback): mixed } $originalAsync = pcntl_async_signals(true); + $originalSignalHandler = pcntl_signal_get_handler(SIGINT); pcntl_signal(SIGINT, fn () => exit()); @@ -72,12 +73,12 @@ public function spin(Closure $callback): mixed } else { $result = $callback(); - $this->resetTerminal($originalAsync); + $this->resetTerminal($originalAsync, $originalSignalHandler); return $result; } } catch (\Throwable $e) { - $this->resetTerminal($originalAsync); + $this->resetTerminal($originalAsync, $originalSignalHandler); throw $e; } @@ -86,10 +87,10 @@ public function spin(Closure $callback): mixed /** * Reset the terminal. */ - protected function resetTerminal(bool $originalAsync): void + protected function resetTerminal(bool $originalAsync, callable|int $originalSignalHandler = SIG_DFL): void { pcntl_async_signals($originalAsync); - pcntl_signal(SIGINT, SIG_DFL); + pcntl_signal(SIGINT, $originalSignalHandler); $this->eraseRenderedLines(); } diff --git a/core/vendor/laravel/prompts/src/Task.php b/core/vendor/laravel/prompts/src/Task.php index 9f7fb4e4fd..deaa1eb09a 100644 --- a/core/vendor/laravel/prompts/src/Task.php +++ b/core/vendor/laravel/prompts/src/Task.php @@ -114,6 +114,7 @@ public function run(Closure $callback): mixed } $originalAsync = pcntl_async_signals(true); + $originalSignalHandler = pcntl_signal_get_handler(SIGINT); pcntl_signal(SIGINT, fn () => exit()); @@ -124,6 +125,9 @@ public function run(Closure $callback): mixed $sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); if ($sockets === false) { + pcntl_async_signals($originalAsync); + pcntl_signal(SIGINT, $originalSignalHandler); + return $this->renderStatically($callback); } @@ -157,10 +161,13 @@ public function run(Closure $callback): mixed usleep($this->interval * 2000); } + pcntl_async_signals($originalAsync); + pcntl_signal(SIGINT, $originalSignalHandler); + return $result; } } catch (\Throwable $e) { - $this->resetTerminal($originalAsync, success: false); + $this->resetTerminal($originalAsync, success: false, originalSignalHandler: $originalSignalHandler); throw $e; } @@ -302,12 +309,12 @@ protected function recalculateMaxStableMessages(): void /** * Reset the terminal. */ - protected function resetTerminal(bool $originalAsync, bool $success = true): void + protected function resetTerminal(bool $originalAsync, bool $success = true, callable|int $originalSignalHandler = SIG_DFL): void { $this->finished = true; pcntl_async_signals($originalAsync); - pcntl_signal(SIGINT, SIG_DFL); + pcntl_signal(SIGINT, $originalSignalHandler); if ($this->socket !== null) { fclose($this->socket); diff --git a/core/vendor/laravel/prompts/src/Themes/Default/GridRenderer.php b/core/vendor/laravel/prompts/src/Themes/Default/GridRenderer.php index 8c731f83fa..09f890b71c 100644 --- a/core/vendor/laravel/prompts/src/Themes/Default/GridRenderer.php +++ b/core/vendor/laravel/prompts/src/Themes/Default/GridRenderer.php @@ -23,18 +23,23 @@ public function __invoke(Grid $grid): string return $this; } + $items = array_map( + fn ($item) => $this->truncate($item, max(1, $grid->maxWidth - 5)), + $grid->items + ); + $maxWidth = $grid->maxWidth - 2; - $cellWidth = max(array_map(fn ($item) => mb_strwidth($this->stripEscapeSequences($item)), $grid->items)) + 4; + $cellWidth = max(array_map(fn ($item) => mb_strwidth($this->stripEscapeSequences($item)), $items)) + 4; $maxColumns = max(1, (int) floor(($maxWidth - 1) / ($cellWidth + 1))); - $columnCount = max(1, $this->balancedColumnCount(count($grid->items), $maxColumns)); + $columnCount = max(1, $this->balancedColumnCount(count($items), $maxColumns)); - $rows = $this->buildRowsWithSeparators($grid->items, $columnCount); + $rows = $this->buildRowsWithSeparators($items, $columnCount); $tableStyle = (new TableStyle) ->setHorizontalBorderChars('─') ->setVerticalBorderChars('│', '│') ->setCellRowFormat('%s') - ->setCrossingChars('┼', '', '', '', '┤', '┘', '┴', '└', '├', '┌', '┬', '┐'); + ->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├', '┌', '┬', '┐'); $buffered = new BufferedConsoleOutput; diff --git a/core/vendor/laravel/prompts/src/Themes/Default/TableRenderer.php b/core/vendor/laravel/prompts/src/Themes/Default/TableRenderer.php index c8765a1ec3..59e8d260ee 100644 --- a/core/vendor/laravel/prompts/src/Themes/Default/TableRenderer.php +++ b/core/vendor/laravel/prompts/src/Themes/Default/TableRenderer.php @@ -21,7 +21,7 @@ public function __invoke(Table $table): string ->setCellRowFormat('%s'); if (empty($table->headers)) { - $tableStyle->setCrossingChars('┼', '', '', '', '┤', '┘', '┴', '└', '├', '┌', '┬', '┐'); + $tableStyle->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├', '┌', '┬', '┐'); } else { $tableStyle->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├'); } diff --git a/core/vendor/laravel/serializable-closure/src/Serializers/Native.php b/core/vendor/laravel/serializable-closure/src/Serializers/Native.php index 670500ccf9..8c608c7e04 100644 --- a/core/vendor/laravel/serializable-closure/src/Serializers/Native.php +++ b/core/vendor/laravel/serializable-closure/src/Serializers/Native.php @@ -328,6 +328,10 @@ public function getReflector() */ protected function mapPointers(&$data) { + if ($data instanceof SerializableClosure || $data instanceof UnsignedSerializableClosure) { + return; + } + $scope = $this->scope; if ($data instanceof static) { @@ -363,6 +367,8 @@ protected function mapPointers(&$data) foreach ($data as $key => &$value) { if ($value instanceof SelfReference && $value->hash === $this->code['self']) { $data->{$key} = &$this->closure; + } elseif ($value instanceof static) { + $data->{$key} = &$value->closure; } elseif (is_array($value) || is_object($value)) { $this->mapPointers($value); } @@ -399,6 +405,8 @@ protected function mapPointers(&$data) 'property' => $property, 'object' => $item instanceof SelfReference ? $this : $item, ]; + } elseif ($item instanceof static) { + static::setPropertyValue($property, $data, $item->closure); } elseif (is_array($item) || is_object($item)) { $this->mapPointers($item); static::setPropertyValue($property, $data, $item); diff --git a/core/vendor/nesbot/carbon/readme.md b/core/vendor/nesbot/carbon/readme.md index 36e1c0e45c..eda7a87d47 100644 --- a/core/vendor/nesbot/carbon/readme.md +++ b/core/vendor/nesbot/carbon/readme.md @@ -130,11 +130,11 @@ Support this project by becoming a sponsor. Your logo will show up here with a l - + @@ -156,70 +156,70 @@ Support this project by becoming a sponsor. Your logo will show up here with a l - - + - - - - + + - - + + - - - - + + + - - + - - + + - - - + + - + + + + - + + - - - - - - - - - - + + + + + + + + + + + +
Онлайн казино 777Ставки на спорт Non GamStop Bookies UK Route4Me Route Planner Releaf – Medizinischer Cannabis Shop PayID PokiesСтавки на спорт Best non Gamstop sites in the UK CasinoHex Canada
OnlineCasinosSpelen PayID Pokies Sites Online Pokies in AustraliaPlinko Game Paysafecard Casino Australian Online Pokies Top 10 Online Casino Australia Eu ComBuitenlandse Online CasinoPlinko Game Online Kasyno Polis Guidebook.BetWinnerCasinomithandyrechnung
lolajack Top Rating casino parhaatuudetkasinot.com
New Casino Bonuses Fun88 East Villa FCFun88Fun88Fun88Fun88 Casino ohne oasis Najlepsi Bukmacherzy Paysafecard Casino WNB Casino Bewertung
Casinos ohne OASISCasino mit Schneller Auszahlung casino en ligne avec GirandièresOnline Casino Zonder RegistratieBuitenlandse Online Casino
Real Money PokiesОнлайн казино casino.ua TideliftiDealeCasinosOnline Casino Zonder Registratie Sportsbook Reviews OnlineTop 10 Casino AustraliaInterac-casino.com - CanadalolajackCasinomithandyrechnung Casino-portugal.pt
WildflowerCasino mit Schneller AuszahlungОнлайн казино casino.ua Ставки на спорт Favbet Probukmacherbest non Gamstop casinosInterac-casino.com - Canada
iDealeCasinos inkedinInstant withdrawal casinobest non Gamstop casinosTop 10 Casino Australia freispace bestecasinozondercruks betking онлайн казино Run Weekly New Casino Sites UK (2025) Help Direct UnAIMyText
TopKasynoOnline PL seguidores en instagram gratisWettanbieter ohne OASISCasino ohne LizenzCasino ohne LimitCasino ohne VerifizierungCasino ohne AnmeldungCrypto CasinoPaypal CasinoFair Go Casino
ssddanbrown
diff --git a/core/vendor/nesbot/carbon/src/Carbon/Lang/es.php b/core/vendor/nesbot/carbon/src/Carbon/Lang/es.php index dc24945cf7..991a42baf2 100644 --- a/core/vendor/nesbot/carbon/src/Carbon/Lang/es.php +++ b/core/vendor/nesbot/carbon/src/Carbon/Lang/es.php @@ -103,8 +103,7 @@ 'sameElse' => 'L', ], 'months' => ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'], - 'months_short' => ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'], - 'mmm_suffix' => '.', + 'months_short' => ['ene.', 'feb.', 'mar.', 'abr.', 'may.', 'jun.', 'jul.', 'ago.', 'sep.', 'oct.', 'nov.', 'dic.'], 'ordinal' => ':numberº', 'weekdays' => ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'], 'weekdays_short' => ['dom.', 'lun.', 'mar.', 'mié.', 'jue.', 'vie.', 'sáb.'], diff --git a/core/vendor/nesbot/carbon/src/Carbon/Lang/fy.php b/core/vendor/nesbot/carbon/src/Carbon/Lang/fy.php index 42ecaed1e4..00009a5ea6 100644 --- a/core/vendor/nesbot/carbon/src/Carbon/Lang/fy.php +++ b/core/vendor/nesbot/carbon/src/Carbon/Lang/fy.php @@ -65,8 +65,7 @@ return $number.(($number === 1 || $number === 8 || $number >= 20) ? 'ste' : 'de'); }, 'months' => ['jannewaris', 'febrewaris', 'maart', 'april', 'maaie', 'juny', 'july', 'augustus', 'septimber', 'oktober', 'novimber', 'desimber'], - 'months_short' => ['jan', 'feb', 'mrt', 'apr', 'mai', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'des'], - 'mmm_suffix' => '.', + 'months_short' => ['jan.', 'feb.', 'mrt.', 'apr.', 'mai.', 'jun.', 'jul.', 'aug.', 'sep.', 'okt.', 'nov.', 'des.'], 'weekdays' => ['snein', 'moandei', 'tiisdei', 'woansdei', 'tongersdei', 'freed', 'sneon'], 'weekdays_short' => ['si.', 'mo.', 'ti.', 'wo.', 'to.', 'fr.', 'so.'], 'weekdays_min' => ['Si', 'Mo', 'Ti', 'Wo', 'To', 'Fr', 'So'], diff --git a/core/vendor/nesbot/carbon/src/Carbon/Lang/nl.php b/core/vendor/nesbot/carbon/src/Carbon/Lang/nl.php index ccf1925776..8536ed472d 100644 --- a/core/vendor/nesbot/carbon/src/Carbon/Lang/nl.php +++ b/core/vendor/nesbot/carbon/src/Carbon/Lang/nl.php @@ -101,8 +101,7 @@ return $number.(($number === 1 || $number === 8 || $number >= 20) ? 'ste' : 'de'); }, 'months' => ['januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'], - 'months_short' => ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'], - 'mmm_suffix' => '.', + 'months_short' => ['jan.', 'feb.', 'mrt.', 'apr.', 'mei', 'jun.', 'jul.', 'aug.', 'sep.', 'okt.', 'nov.', 'dec.'], 'weekdays' => ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'], 'weekdays_short' => ['zo.', 'ma.', 'di.', 'wo.', 'do.', 'vr.', 'za.'], 'weekdays_min' => ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'], diff --git a/core/vendor/nesbot/carbon/src/Carbon/Traits/Creator.php b/core/vendor/nesbot/carbon/src/Carbon/Traits/Creator.php index 33fa9f5bfa..09852eba3b 100644 --- a/core/vendor/nesbot/carbon/src/Carbon/Traits/Creator.php +++ b/core/vendor/nesbot/carbon/src/Carbon/Traits/Creator.php @@ -236,7 +236,9 @@ public static function parseFromLocale( ?string $locale = null, DateTimeZone|string|int|null $timezone = null, ): static { - return static::rawParse(static::translateTimeString($time, $locale, static::DEFAULT_LOCALE), $timezone); + $text = static::translateTimeString($time, $locale, static::DEFAULT_LOCALE); + + return static::rawParse(str_replace("'", '', $text), $timezone); } /** diff --git a/core/vendor/nesbot/carbon/src/Carbon/Traits/Date.php b/core/vendor/nesbot/carbon/src/Carbon/Traits/Date.php index 13d2925fdb..782145b83a 100644 --- a/core/vendor/nesbot/carbon/src/Carbon/Traits/Date.php +++ b/core/vendor/nesbot/carbon/src/Carbon/Traits/Date.php @@ -2127,15 +2127,9 @@ public static function getIsoUnits(): array 'SSSSSSSSS' => static fn (CarbonInterface $date) => self::floorZeroPad($date->micro * 1000, 9), 'M' => 'month', 'MM' => ['rawFormat', ['m']], - 'MMM' => static function (CarbonInterface $date, $originalFormat = null) { - $month = $date->getTranslatedShortMonthName($originalFormat); - $suffix = $date->getTranslationMessage('mmm_suffix'); - if ($suffix && $month !== $date->monthName) { - $month .= $suffix; - } - - return $month; - }, + 'MMM' => static fn (CarbonInterface $date, $originalFormat = null) => $date->getTranslatedShortMonthName( + $originalFormat, + ), 'MMMM' => static fn (CarbonInterface $date, $originalFormat = null) => $date->getTranslatedMonthName( $originalFormat, ), diff --git a/core/vendor/nesbot/carbon/src/Carbon/Traits/Localization.php b/core/vendor/nesbot/carbon/src/Carbon/Traits/Localization.php index 344dfb9ddf..b4ee67e8c3 100644 --- a/core/vendor/nesbot/carbon/src/Carbon/Traits/Localization.php +++ b/core/vendor/nesbot/carbon/src/Carbon/Traits/Localization.php @@ -291,13 +291,17 @@ public static function translateTimeString( } } - $$translationKey = array_merge( + $monthTranslations = array_merge( $mode & CarbonInterface::TRANSLATE_MONTHS ? $processList(self::getTranslationArray($months, static::MONTHS_PER_YEAR, $timeString)) : [], $mode & CarbonInterface::TRANSLATE_MONTHS ? $processList(self::getTranslationArray($messages['months_short'] ?? [], static::MONTHS_PER_YEAR, $timeString)) : [], + ); + + $$translationKey = array_merge( + $monthTranslations, $mode & CarbonInterface::TRANSLATE_DAYS ? $processList(self::getTranslationArray($weekdays, static::DAYS_PER_WEEK, $timeString)) : [], @@ -338,24 +342,50 @@ public static function translateTimeString( ]), $fromTranslations, ); + $monthNameCount = \count($monthTranslations); + $firstNumberOffset = preg_match('/^\D+\d/', $timeString, $match) + ? \strlen($match[0]) // -1 to remove the first digit, +1 to add the extra wrapping spaced added just below + : INF; - return substr(preg_replace_callback('/(?<=[\d\s+.\/,_-])('.implode('|', $fromTranslations).')(?=[\d\s+.\/,_-])/iu', function ($match) use ($fromTranslations, $toTranslations) { - [$chunk] = $match; + return substr(preg_replace_callback( + '/(?<=[\d\s+.\/,_-])('.implode('|', $fromTranslations).')(?=[\d\s+.\/,_-])/iu', + function ($match) use ($fromTranslations, $toTranslations, $monthNameCount, $firstNumberOffset) { + [[$chunk, $offset]] = $match; - $index = array_search($chunk, $fromTranslations); + $indexes = self::getMatchingWordIndexes($fromTranslations, $chunk); - if ($index !== false) { - return $toTranslations[$index] ?? ''; - } + if ($indexes !== []) { + // Before the first number in the string, prefer day names over month names + $bestIndexes = \count($indexes) > 1 && $offset < $firstNumberOffset + ? array_values(array_filter($indexes, static fn ($index) => $index > $monthNameCount)) + : []; + $index = $bestIndexes[0] ?? $indexes[0]; - foreach ($fromTranslations as $index => $word) { - if (preg_match("/^$word\$/iu", $chunk)) { return $toTranslations[$index] ?? ''; } + + return $chunk; // @codeCoverageIgnore + }, + " $timeString ", + flags: PREG_OFFSET_CAPTURE, + ), 1, -1); + } + + private static function getMatchingWordIndexes(array $fromTranslations, string $search): array + { + $indexes = array_keys($fromTranslations, $search, true); + + if ($indexes !== []) { + return $indexes; + } + + foreach ($fromTranslations as $index => $word) { + if (preg_match("/^$word\$/iu", $search)) { + $indexes[] = $index; } + } - return $chunk; // @codeCoverageIgnore - }, " $timeString "), 1, -1); + return $indexes; } /** diff --git a/core/vendor/predis/predis/README.md b/core/vendor/predis/predis/README.md index a91a4ed670..2c72168b2f 100644 --- a/core/vendor/predis/predis/README.md +++ b/core/vendor/predis/predis/README.md @@ -22,6 +22,7 @@ More details about this project can be found on the [frequently asked questions] - Command pipelining on both single nodes and clusters (client-side sharding only). - Abstraction for Redis transactions (Redis >= 2.0) and CAS operations (Redis >= 2.2). - Abstraction for Lua scripting (Redis >= 2.6) and automatic switching between `EVALSHA` or `EVAL`. +- Abstraction for Hinted Hash Templates (`HIMPORT`, Redis >= 8.10) with automatic per-connection fieldset replay. - Abstraction for `SCAN`, `SSCAN`, `ZSCAN` and `HSCAN` (Redis >= 2.8) based on PHP iterators. - Connections are established lazily by the client upon the first command and can be persisted. - Connections can be established via TCP/IP (also TLS/SSL-encrypted) or UNIX domain sockets. @@ -434,6 +435,83 @@ $response = $redis->transaction(function (MultiExec $tx) { ``` +### Hinted Hash Templates (HIMPORT) ### + +> **Experimental:** this feature is experimental and its API (the `himport` container and option) may +> change in a future release. + +`HIMPORT` (Redis >= 8.10) speeds up loading many hashes that share the same field names. The field +names are sent once with `HIMPORT PREPARE`, registering them under a fieldset name, and hashes are +then created with `HIMPORT SET` by sending only the values. The server stores such hashes in a +memory-efficient encoding where the field names are kept only once; the resulting keys are ordinary +hashes that work with every regular hash command. + +Predis exposes the command family through the `himport` container: + +```php +$client->himport->prepare('users', ['name', 'email', 'age']); + +$client->himport->set('user:1', 'users', ['alice', 'alice@example.com', '25']); +$client->himport->set('user:2', 'users', ['bob', 'bob@example.com', '30']); + +$client->himport->discard('users'); // 1 +$client->himport->discardAll(); // number of fieldsets removed +``` + +Values are paired by position with the fields supplied to `prepare()`, in the caller's order — Predis +never reorders them. Hash enumeration order (e.g. `HGETALL`) is not guaranteed to match the `PREPARE` +order, only the value-to-field pairing is. + +A fieldset is **server-side session state that lives on a single physical connection** and is lost when +that connection is dropped (reconnect, `RESET`, cluster failover). To keep this transparent, Predis +tracks the fieldsets prepared through the container and: + +- replays each `PREPARE` automatically when a connection is re-established (at most once per physical + connection); this always happens and does not depend on retries; +- if a `HIMPORT SET` still reports `no such fieldset` (for example on a connection created after a + cluster redirection), re-prepares the fieldset on the executing connection and retries the write. + +The re-prepare-and-retry step **reuses the client's configured retry policy** — it is not a separate +mechanism. It therefore only happens when retries are enabled (via the `retry` connection parameter), +and never more than the configured number of attempts; with retries disabled the `no such fieldset` +error propagates unchanged. It can additionally be turned off, even when retries are enabled, with the +`himport` option: + +```php +$client = new Predis\Client( + $parameters + ['retry' => new Predis\Retry\Retry(new Predis\Retry\Strategy\ExponentialBackoff(), 3)], + ['himport' => ['auto_prepare' => false]] // opt out of HIMPORT re-prepare specifically +); +``` + +Fieldsets can also be declared up front through the `himport` option. Fieldsets declared this way are +prepared on demand the first time a `HIMPORT SET` references them on a connection, so the application +never has to call `prepare()` for them (this uses the re-prepare-and-retry path above, so it requires +retries to be enabled): + +```php +$client = new Predis\Client($parameters + ['retry' => new Predis\Retry\Retry(new Predis\Retry\Strategy\ExponentialBackoff(), 3)], [ + 'himport' => [ + 'fieldsets' => [ + 'users' => ['name', 'email', 'age'], + ], + ], +]); + +// No prepare() call needed — "users" is known from configuration: +$client->himport->set('user:1', 'users', ['alice', 'alice@example.com', '25']); +``` + +On a cluster, `prepare()`, `discard()` and `discardAll()` fan out to every master shard, while `set()` +is routed by the hash slot of its key like any other write. This ensures a `HIMPORT SET` succeeds on +whichever shard owns its key. + +The raw command form (`$client->himport('PREPARE', 'users', 'name', 'email')`) is also available and is +the one to use inside pipelines and transactions; it performs no client-side tracking or recovery, so +`PREPARE` and the dependent `SET` commands must run on the same connection (which pipelines and +transactions guarantee). + + ### Adding new commands ### While we try to update Predis to stay up to date with all the commands available in Redis, you might diff --git a/core/vendor/predis/predis/src/Client.php b/core/vendor/predis/predis/src/Client.php index 35d4856157..c7c41275df 100644 --- a/core/vendor/predis/predis/src/Client.php +++ b/core/vendor/predis/predis/src/Client.php @@ -56,7 +56,7 @@ */ class Client implements ClientInterface, IteratorAggregate { - public const VERSION = '3.5.1'; + public const VERSION = '3.6.0'; /** @var OptionsInterface */ private $options; @@ -272,8 +272,8 @@ public function getConnection() /** * Applies the configured serializer and compression to given value. * - * @param mixed $value - * @return string + * @param mixed $value + * @return mixed */ public function pack($value) { @@ -285,8 +285,8 @@ public function pack($value) /** * Deserializes and decompresses to given value. * - * @param mixed $value - * @return string + * @param mixed $value + * @return mixed */ public function unpack($value) { @@ -338,6 +338,40 @@ public function __call($commandID, $arguments) ); } + /** + * Reads entries from one or multiple streams. + * + * @deprecated Use xread_v4() instead. Public API will be changed in the next major version. + * + * @param int|null $count Maximum number of entries per stream + * @param int|null $block Milliseconds to block waiting for new entries + * @param array|null $streams Stream keys to read from + * @param string ...$id Last-seen ID per stream + * @return array|null + */ + public function xread($count = null, $block = null, ?array $streams = null, ...$id) + { + return $this->__call('xread', func_get_args()); + } + + /** + * Reads entries from one or multiple streams as part of a consumer group. + * + * @deprecated Use xreadgroup_claim() instead. Public API will be changed in the next major version. + * + * @param string $group Consumer-group name + * @param string $consumer Consumer name + * @param int|null $count Maximum number of entries per stream + * @param int|null $blockMs Milliseconds to block waiting for new entries + * @param bool $noAck Do not add entries to the pending entries list + * @param string ...$keyOrId Stream keys followed by one ID per stream + * @return array + */ + public function xreadgroup($group, $consumer, $count = null, $blockMs = null, $noAck = false, ...$keyOrId) + { + return $this->__call('xreadgroup', func_get_args()); + } + /** * {@inheritdoc} */ diff --git a/core/vendor/predis/predis/src/ClientContextInterface.php b/core/vendor/predis/predis/src/ClientContextInterface.php index 2e02140d40..704beb15b8 100644 --- a/core/vendor/predis/predis/src/ClientContextInterface.php +++ b/core/vendor/predis/predis/src/ClientContextInterface.php @@ -37,11 +37,14 @@ use Predis\Command\Argument\TimeSeries\InfoArguments; use Predis\Command\Argument\TimeSeries\MGetArguments; use Predis\Command\Argument\TimeSeries\MRangeArguments; +use Predis\Command\Argument\TimeSeries\NRangeArguments; use Predis\Command\Argument\TimeSeries\RangeArguments; +use Predis\Command\Argument\TimeSeries\ReadArguments; use Predis\Command\CommandInterface; use Predis\Command\Container\ACL; use Predis\Command\Container\CLIENT; use Predis\Command\Container\FUNCTIONS; +use Predis\Command\Container\HIMPORT; use Predis\Command\Container\HOTKEYS; use Predis\Command\Container\Json\JSONDEBUG; use Predis\Command\Container\Search\FTCONFIG; @@ -127,7 +130,7 @@ * @method $this cfinsertnx(string $key, int $capacity = -1, bool $noCreate = false, string ...$item) * @method $this cfreserve(string $key, int $capacity, int $bucketSize = -1, int $maxIterations = -1, int $expansion = -1) * @method $this cfscandump(string $key, int $iterator) - * @method $this cmsincrby(string $key, string|int...$itemIncrementDictionary) + * @method $this cmsincrby(string $key, string|int ...$itemIncrementDictionary) * @method $this cmsinfo(string $key) * @method $this cmsinitbydim(string $key, int $width, int $depth) * @method $this cmsinitbyprob(string $key, float $errorRate, float $probability) @@ -142,6 +145,7 @@ * @method $this ftaggregate(string $index, string $query, ?AggregateArguments $arguments = null) * @method $this ftaliasadd(string $alias, string $index) * @method $this ftaliasdel(string $alias) + * @method $this ftaliaslist(string $index) * @method $this ftaliasupdate(string $alias, string $index) * @method $this ftalter(string $index, FieldInterface[] $schema, ?AlterArguments $arguments = null) * @method $this ftcreate(string $index, FieldInterface[] $schema, ?CreateArguments $arguments = null) @@ -234,6 +238,7 @@ * @method $this jsontoggle(string $key, string $path) * @method $this jsontype(string $key, string $path = '$') * @method $this blmove(string $source, string $destination, string $where, string $to, int $timeout) + * @method $this blmovem(string $source, string $destination, string $from, string $to, int|float $timeout, ?string $quantifier = null, ?int $count = null, ?string $ordering = null) * @method $this blpop(array|string $keys, $timeout) * @method $this brpop(array|string $keys, $timeout) * @method $this brpoplpush($source, $destination, $timeout) @@ -242,6 +247,7 @@ * @method $this linsert($key, $whence, $pivot, $value) * @method $this llen($key) * @method $this lmove(string $source, string $destination, string $where, string $to) + * @method $this lmovem(string $source, string $destination, string $from, string $to, ?string $quantifier = null, ?int $count = null, ?string $ordering = null) * @method $this lmpop(array $keys, string $modifier = 'left', int $count = 1) * @method $this lpop($key) * @method $this lpush($key, array $values) @@ -257,6 +263,7 @@ * @method $this sadd($key, array $members) * @method $this scard($key) * @method $this sdiff(array|string $keys) + * @method $this sdiffcard(array $keys, int $limit = 0) * @method $this sdiffstore($destination, array|string $keys) * @method $this sinter(array|string $keys) * @method $this sintercard(array $keys, int $limit = 0) @@ -273,6 +280,7 @@ * @method $this subscribe(string ...$channels) * @method $this sunsubscribe(?string ...$shardChannels = null) * @method $this sunion(array|string $keys) + * @method $this sunioncard(array $keys, bool $approx = false, int $limit = 0) * @method $this sunionstore($destination, array|string $keys) * @method $this tdigestadd(string $key, float ...$value) * @method $this tdigestbyrank(string $key, int ...$rank) @@ -308,8 +316,12 @@ * @method $this tsmget(MGetArguments $arguments, string ...$filterExpression) * @method $this tsmrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments) * @method $this tsmrevrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments) + * @method $this tsnrange(array $keys, $fromTimestamp, $toTimestamp, ?NRangeArguments $arguments = null) + * @method $this tsnrevrange(array $keys, $fromTimestamp, $toTimestamp, ?NRangeArguments $arguments = null) * @method $this tsqueryindex(string ...$filterExpression) + * @method $this tsquerylabels(?string $label = null, string ...$filterExpression) * @method $this tsrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null) + * @method $this tsread(string $key, $timestamp, ?ReadArguments $arguments = null) * @method $this tsrevrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null) * @method $this xack(string $key, string $group, string ...$id) * @method $this xackdel(string $key, string $group, string $mode, array $ids) @@ -325,8 +337,9 @@ * @method $this xrevrange(string $key, string $end, string $start, ?int $count = null) * @method $this xrange(string $key, string $start, string $end, ?int $count = null) * @method $this xread(int $count = null, int $block = null, array $streams = null, string ...$id) + * @method $this xread_v4(array $keyIdDict, ?int $count = null, ?int $blockMs = null, ?int $maxCount = null, ?int $maxSize = null) * @method $this xreadgroup(string $group, string $consumer, ?int $count = null, ?int $blockMs = null, bool $noAck = false, string ...$keyOrId) - * @method $this xreadgroup_claim(string $group, string $consumer, array $keyIdDict, ?int $count = null, ?int $blockMs = null, bool $noAck = false, ?int $claim = null) + * @method $this xreadgroup_claim(string $group, string $consumer, array $keyIdDict, ?int $count = null, ?int $blockMs = null, bool $noAck = false, ?int $claim = null, ?int $maxCount = null, ?int $maxSize = null) * @method $this xsetid(string $key, string $lastId, ?int $entriesAdded = null, ?string $maxDeleteId = null) * @method $this xtrim(string $key, array|string $strategy, string $threshold, array $options = null) * @method $this zadd($key, array $membersAndScoresDictionary) @@ -419,6 +432,7 @@ * * Container commands * @property CLIENT $client + * @property HIMPORT $himport * @property HOTKEYS $hotkeys * @property FUNCTIONS $function * @property FTCONFIG $ftconfig diff --git a/core/vendor/predis/predis/src/ClientInterface.php b/core/vendor/predis/predis/src/ClientInterface.php index 71d2ed1f9a..fc61dcc0a5 100644 --- a/core/vendor/predis/predis/src/ClientInterface.php +++ b/core/vendor/predis/predis/src/ClientInterface.php @@ -37,11 +37,14 @@ use Predis\Command\Argument\TimeSeries\InfoArguments; use Predis\Command\Argument\TimeSeries\MGetArguments; use Predis\Command\Argument\TimeSeries\MRangeArguments; +use Predis\Command\Argument\TimeSeries\NRangeArguments; use Predis\Command\Argument\TimeSeries\RangeArguments; +use Predis\Command\Argument\TimeSeries\ReadArguments; use Predis\Command\CommandInterface; use Predis\Command\Container\ACL; use Predis\Command\Container\CLIENT; use Predis\Command\Container\FUNCTIONS; +use Predis\Command\Container\HIMPORT; use Predis\Command\Container\HOTKEYS; use Predis\Command\Container\Json\JSONDEBUG; use Predis\Command\Container\Search\FTCONFIG; @@ -153,6 +156,7 @@ * @method array ftaggregate(string $index, string $query, ?AggregateArguments $arguments = null) * @method Status ftaliasadd(string $alias, string $index) * @method Status ftaliasdel(string $alias) + * @method array ftaliaslist(string $index) * @method Status ftaliasupdate(string $alias, string $index) * @method Status ftalter(string $index, FieldInterface[] $schema, ?AlterArguments $arguments = null) * @method Status ftcreate(string $index, FieldInterface[] $schema, ?CreateArguments $arguments = null) @@ -245,6 +249,7 @@ * @method array jsontoggle(string $key, string $path) * @method array jsontype(string $key, string $path = '$') * @method string blmove(string $source, string $destination, string $where, string $to, int $timeout) + * @method array|null blmovem(string $source, string $destination, string $from, string $to, int|float $timeout, ?string $quantifier = null, ?int $count = null, ?string $ordering = null) * @method array|null blpop(array|string $keys, int|float $timeout) * @method array|null brpop(array|string $keys, int|float $timeout) * @method string|null brpoplpush(string $source, string $destination, int|float $timeout) @@ -253,6 +258,7 @@ * @method int linsert(string $key, $whence, $pivot, $value) * @method int llen(string $key) * @method string lmove(string $source, string $destination, string $where, string $to) + * @method array|null lmovem(string $source, string $destination, string $from, string $to, ?string $quantifier = null, ?int $count = null, ?string $ordering = null) * @method array|null lmpop(array $keys, string $modifier = 'left', int $count = 1) * @method string|null lpop(string $key) * @method int lpush(string $key, array $values) @@ -268,6 +274,7 @@ * @method int sadd(string $key, array $members) * @method int scard(string $key) * @method string[] sdiff(array|string $keys) + * @method int sdiffcard(array $keys, int $limit = 0) * @method int sdiffstore(string $destination, array|string $keys) * @method string[] sinter(array|string $keys) * @method int sintercard(array $keys, int $limit = 0) @@ -283,6 +290,7 @@ * @method array ssubscribe(string ...$shardChannels) * @method array subscribe(string ...$channels) * @method string[] sunion(array|string $keys) + * @method int sunioncard(array $keys, bool $approx = false, int $limit = 0) * @method int sunionstore(string $destination, array|string $keys) * @method array sunsubscribe(?string ...$shardChannels = null) * @method int touch(string[]|string $keyOrKeys, string ...$keys = null) @@ -320,8 +328,12 @@ * @method array tsmget(MGetArguments $arguments, string ...$filterExpression) * @method array tsmrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments) * @method array tsmrevrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments) + * @method array tsnrange(array $keys, $fromTimestamp, $toTimestamp, ?NRangeArguments $arguments = null) + * @method array tsnrevrange(array $keys, $fromTimestamp, $toTimestamp, ?NRangeArguments $arguments = null) * @method array tsqueryindex(string ...$filterExpression) + * @method array tsquerylabels(?string $label = null, string ...$filterExpression) * @method array tsrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null) + * @method array tsread(string $key, $timestamp, ?ReadArguments $arguments = null) * @method array tsrevrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null) * @method int xack(string $key, string $group, string ...$id) * @method array xackdel(string $key, string $group, string $mode, array $ids) @@ -337,8 +349,9 @@ * @method array xrevrange(string $key, string $end, string $start, ?int $count = null) * @method array xrange(string $key, string $start, string $end, ?int $count = null) * @method array|null xread(int $count = null, int $block = null, array $streams = null, string ...$id) + * @method array|null xread_v4(array $keyIdDict, ?int $count = null, ?int $blockMs = null, ?int $maxCount = null, ?int $maxSize = null) * @method array xreadgroup(string $group, string $consumer, ?int $count = null, ?int $blockMs = null, bool $noAck = false, string ...$keyOrId) - * @method array xreadgroup_claim(string $group, string $consumer, array $keyIdDict, ?int $count = null, ?int $blockMs = null, bool $noAck = false, ?int $claim = null) + * @method array xreadgroup_claim(string $group, string $consumer, array $keyIdDict, ?int $count = null, ?int $blockMs = null, bool $noAck = false, ?int $claim = null, ?int $maxCount = null, ?int $maxSize = null) * @method Status xsetid(string $key, string $lastId, ?int $entriesAdded = null, ?string $maxDeleteId = null) * @method string xtrim(string $key, array|string $strategy, string $threshold, array $options = null) * @method int zadd(string $key, array $membersAndScoresDictionary) @@ -434,6 +447,7 @@ * * Container commands * @property CLIENT $client + * @property HIMPORT $himport * @property HOTKEYS $hotkeys * @property FUNCTIONS $function * @property FTCONFIG $ftconfig diff --git a/core/vendor/predis/predis/src/Cluster/ClusterStrategy.php b/core/vendor/predis/predis/src/Cluster/ClusterStrategy.php index 16064b06c3..547deae517 100644 --- a/core/vendor/predis/predis/src/Cluster/ClusterStrategy.php +++ b/core/vendor/predis/predis/src/Cluster/ClusterStrategy.php @@ -54,6 +54,7 @@ protected function getDefaultCommands() 'SORT' => [$this, 'getKeyFromSortCommand'], 'DUMP' => $getKeyFromFirstArgument, 'RESTORE' => $getKeyFromFirstArgument, + 'OBJECT' => [$this, 'getKeyFromObjectCommand'], 'FLUSHDB' => [$this, 'getFakeKey'], /* commands operating on string values */ @@ -86,9 +87,11 @@ protected function getDefaultCommands() 'LINSERT' => $getKeyFromFirstArgument, 'LINDEX' => $getKeyFromFirstArgument, 'LLEN' => $getKeyFromFirstArgument, + 'LMOVEM' => [$this, 'getKeyFromFirstTwoKeys'], 'LPOP' => $getKeyFromFirstArgument, 'RPOP' => $getKeyFromFirstArgument, 'RPOPLPUSH' => $getKeyFromAllArguments, + 'BLMOVEM' => [$this, 'getKeyFromFirstTwoKeys'], 'BLPOP' => [$this, 'getKeyFromBlockingListCommands'], 'BRPOP' => [$this, 'getKeyFromBlockingListCommands'], 'BRPOPLPUSH' => [$this, 'getKeyFromBlockingListCommands'], @@ -153,6 +156,7 @@ protected function getDefaultCommands() 'ZREVRANGEBYSCORE' => $getKeyFromFirstArgument, 'ZREVRANK' => $getKeyFromFirstArgument, 'ZSCORE' => $getKeyFromFirstArgument, + 'ZMSCORE' => $getKeyFromFirstArgument, 'ZUNIONSTORE' => [$this, 'getKeyFromZsetAggregationCommands'], 'ZSCAN' => $getKeyFromFirstArgument, 'ZLEXCOUNT' => $getKeyFromFirstArgument, @@ -176,11 +180,56 @@ protected function getDefaultCommands() 'HVALS' => $getKeyFromFirstArgument, 'HSCAN' => $getKeyFromFirstArgument, 'HSTRLEN' => $getKeyFromFirstArgument, + 'HIMPORT' => [$this, 'getKeyFromHimportCommands'], + 'HEXPIRE' => $getKeyFromFirstArgument, + 'HEXPIREAT' => $getKeyFromFirstArgument, + 'HPERSIST' => $getKeyFromFirstArgument, + 'HPEXPIRE' => $getKeyFromFirstArgument, + 'HPEXPIREAT' => $getKeyFromFirstArgument, + 'HTTL' => $getKeyFromFirstArgument, + 'HPTTL' => $getKeyFromFirstArgument, + 'HEXPIRETIME' => $getKeyFromFirstArgument, + 'HPEXPIRETIME' => $getKeyFromFirstArgument, + 'HGETEX' => $getKeyFromFirstArgument, + 'HGETDEL' => $getKeyFromFirstArgument, /* commands operating on streams */ + 'XACK' => $getKeyFromFirstArgument, + 'XACKDEL' => $getKeyFromFirstArgument, 'XADD' => $getKeyFromFirstArgument, + 'XAUTOCLAIM' => $getKeyFromFirstArgument, + 'XCFGSET' => $getKeyFromFirstArgument, + 'XCLAIM' => $getKeyFromFirstArgument, 'XDEL' => $getKeyFromFirstArgument, + 'XDELEX' => $getKeyFromFirstArgument, + 'XGROUP' => [$this, 'getKeyFromStreamGroupCommands'], + 'XINFO' => [$this, 'getKeyFromStreamGroupCommands'], + 'XLEN' => $getKeyFromFirstArgument, + 'XNACK' => $getKeyFromFirstArgument, + 'XPENDING' => $getKeyFromFirstArgument, 'XRANGE' => $getKeyFromFirstArgument, + 'XREAD' => [$this, 'getKeyFromStreamReadCommands'], + 'XREADGROUP' => [$this, 'getKeyFromStreamReadCommands'], + 'XREVRANGE' => $getKeyFromFirstArgument, + 'XSETID' => $getKeyFromFirstArgument, + 'XTRIM' => $getKeyFromFirstArgument, + + /* commands operating on vector sets */ + 'VADD' => $getKeyFromFirstArgument, + 'VCARD' => $getKeyFromFirstArgument, + 'VDIM' => $getKeyFromFirstArgument, + 'VEMB' => $getKeyFromFirstArgument, + 'VGETATTR' => $getKeyFromFirstArgument, + 'VINFO' => $getKeyFromFirstArgument, + 'VLINKS' => $getKeyFromFirstArgument, + 'VRANDMEMBER' => $getKeyFromFirstArgument, + 'VRANGE' => $getKeyFromFirstArgument, + 'VREM' => $getKeyFromFirstArgument, + 'VSETATTR' => $getKeyFromFirstArgument, + 'VSIM' => $getKeyFromFirstArgument, + + /* commands operating on time series */ + 'TS.READ' => $getKeyFromFirstArgument, /* commands operating on HyperLogLog */ 'PFADD' => $getKeyFromFirstArgument, @@ -356,6 +405,47 @@ protected function getKeyFromSortCommand(CommandInterface $command) return $firstKey; } + /** + * Extracts the key from the OBJECT command, where it follows the subcommand. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromObjectCommand(CommandInterface $command) + { + $arguments = $command->getArguments(); + + if (!isset($arguments[1])) { + return null; + } + + return $arguments[1]; + } + + /** + * Extracts the key from commands where the first two arguments are keys, + * followed by non-key arguments (e.g. LMOVEM). + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromFirstTwoKeys(CommandInterface $command) + { + $arguments = $command->getArguments(); + + if (!isset($arguments[1])) { + return $arguments[0] ?? null; + } + + if (!$this->checkSameSlotForKeys(array_slice($arguments, 0, 2))) { + return null; + } + + return $arguments[0]; + } + /** * Extracts the key from BLPOP and BRPOP commands. * @@ -392,6 +482,31 @@ protected function getKeyFromBitOp(CommandInterface $command) return $arguments[1]; } + /** + * Extracts the key from HIMPORT commands. + * + * Only HIMPORT SET operates on a key (at position 1, after the subcommand) + * and is routed by its hash slot. HIMPORT PREPARE/DISCARD/DISCARDALL are + * connection-session commands that the server advertises as all-shards + * commands; they have no key, so this returns null and the cluster + * connection rejects them. The dedicated container command is the sanctioned + * path for fanning those out across the master shards. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromHimportCommands(CommandInterface $command) + { + $arguments = $command->getArguments(); + + if (isset($arguments[0], $arguments[1]) && strtoupper($arguments[0]) === 'SET') { + return $arguments[1]; + } + + return null; + } + /** * Extracts the key from GEORADIUS and GEORADIUSBYMEMBER commands. * @@ -460,6 +575,67 @@ protected function getKeyFromSUnsubscribeCommand(CommandInterface $command): ?st return $this->getKeyFromAllArguments($command); } + /** + * Extracts the key from XGROUP and XINFO commands, where it follows the subcommand. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromStreamGroupCommands(CommandInterface $command) + { + $arguments = $command->getArguments(); + + // Subcommands such as XINFO HELP and XGROUP HELP take no key at all. + if (!isset($arguments[1])) { + return null; + } + + return $arguments[1]; + } + + /** + * Extracts the key from XREAD and XREADGROUP commands, where the STREAMS token is followed by + * the same number of keys and IDs. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromStreamReadCommands(CommandInterface $command) + { + $arguments = $command->getArguments(); + + $offset = $command->getId() === 'XREADGROUP' ? 3 : 0; + $position = null; + + for ($index = $offset, $argc = count($arguments); $index < $argc; ++$index) { + if (is_string($arguments[$index]) && strtoupper($arguments[$index]) === 'STREAMS') { + $position = $index; + break; + } + } + + if ($position === null) { + return null; + } + + $keysAndIds = array_slice($arguments, $position + 1); + $count = count($keysAndIds); + + if ($count === 0 || $count % 2 !== 0) { + return null; + } + + $keys = array_slice($keysAndIds, 0, intdiv($count, 2)); + + if (!$this->checkSameSlotForKeys($keys)) { + return null; + } + + return $keys[0]; + } + /** * Extracts the key from EVAL and EVALSHA commands. * diff --git a/core/vendor/predis/predis/src/Command/Argument/Search/AggregateArguments.php b/core/vendor/predis/predis/src/Command/Argument/Search/AggregateArguments.php index bbf0dab393..6b595fbf7b 100644 --- a/core/vendor/predis/predis/src/Command/Argument/Search/AggregateArguments.php +++ b/core/vendor/predis/predis/src/Command/Argument/Search/AggregateArguments.php @@ -96,6 +96,30 @@ public function reduce(string $function, ...$argument): self return $this; } + /** + * Adds a COLLECT reducer to the current GROUPBY. + * + * Emits "REDUCE COLLECT [AS ]" where is the + * number of tokens produced by the given CollectArguments (the FIELDS, + * DISTINCT, SORTBY and LIMIT clauses) and excludes the trailing AS . + * + * @param CollectArguments $arguments COLLECT clause builder + * @param string|null $alias Optional reducer output column name + * @return $this + */ + public function reduceCollect(CollectArguments $arguments, ?string $alias = null): self + { + $tokens = $arguments->toArray(); + + array_push($this->arguments, 'REDUCE', 'COLLECT', count($tokens), ...$tokens); + + if ($alias !== null) { + array_push($this->arguments, 'AS', $alias); + } + + return $this; + } + /** * Sorts the pipeline up until the point of SORTBY, using a list of properties. * diff --git a/core/vendor/predis/predis/src/Command/Argument/Search/CollectArguments.php b/core/vendor/predis/predis/src/Command/Argument/Search/CollectArguments.php new file mode 100644 index 0000000000..4f377bb063 --- /dev/null +++ b/core/vendor/predis/predis/src/Command/Argument/Search/CollectArguments.php @@ -0,0 +1,145 @@ + ... [AS ]" framing is + * added by AggregateArguments::reduceCollect(), which derives from the + * length of this token list. + * + * Field and sort-key names are emitted with a leading "@" on the wire; bare + * names are normalized automatically. Output map keys are the names with the + * "@" stripped by the server. + */ +class CollectArguments implements ArrayableArgument +{ + public const SORT_ASC = 'ASC'; + public const SORT_DESC = 'DESC'; + + /** + * @var array + */ + protected $arguments = []; + + /** + * Projects all fields present in the pipeline at the COLLECT stage (FIELDS *). + * + * This is stage-local: it projects whatever the pipeline has already + * materialized, not a whole-document fetch. Pair with an upstream LOAD * to + * collect the complete document. + * + * @return $this + */ + public function allFields(): self + { + array_push($this->arguments, 'FIELDS', '*'); + + return $this; + } + + /** + * Projects the explicitly listed fields (FIELDS @field ...). + * + * Names may be supplied with or without a leading "@"; each is normalized to + * a single "@" on the wire. + * + * @param string ...$fields + * @return $this + */ + public function fields(string ...$fields): self + { + $names = array_map([$this, 'ensureAtPrefix'], $fields); + + array_push($this->arguments, 'FIELDS', count($names), ...$names); + + return $this; + } + + /** + * Deduplicates entries with identical projected fields. + * + * NOTE: DISTINCT is specified by the product but not yet implemented by the + * server. Sending it currently results in a server error. The option is kept + * for forward-compatibility; do not rely on it until the server ships it. + * + * @return $this + */ + public function distinct(): self + { + $this->arguments[] = 'DISTINCT'; + + return $this; + } + + /** + * Sorts entries within each group by one or more keys (SORTBY @field [ASC|DESC] ...). + * + * Accepts an ordered map of field name to sort direction, e.g. + * ['@rating' => CollectArguments::SORT_DESC, '@name' => CollectArguments::SORT_ASC]. + * Field names are normalized to carry a leading "@". + * + * @param array $sortByFields Ordered map of field => direction (self::SORT_ASC|self::SORT_DESC) + * @return $this + */ + public function sortBy(array $sortByFields): self + { + $tokens = []; + + foreach ($sortByFields as $field => $direction) { + $tokens[] = $this->ensureAtPrefix((string) $field); + $tokens[] = $direction; + } + + array_push($this->arguments, 'SORTBY', count($tokens), ...$tokens); + + return $this; + } + + /** + * Returns at most entries per group after skipping (LIMIT ). + * + * @param int $offset + * @param int $count + * @return $this + */ + public function limit(int $offset, int $count): self + { + array_push($this->arguments, 'LIMIT', $offset, $count); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->arguments; + } + + /** + * Ensures a field or sort-key name carries a single leading "@" prefix. + * + * @param string $name + * @return string + */ + private function ensureAtPrefix(string $name): string + { + return (strpos($name, '@') === 0) ? $name : '@' . $name; + } +} diff --git a/core/vendor/predis/predis/src/Command/Argument/TimeSeries/MRangeArguments.php b/core/vendor/predis/predis/src/Command/Argument/TimeSeries/MRangeArguments.php index 6e28d098a3..b4d65cf18f 100644 --- a/core/vendor/predis/predis/src/Command/Argument/TimeSeries/MRangeArguments.php +++ b/core/vendor/predis/predis/src/Command/Argument/TimeSeries/MRangeArguments.php @@ -51,7 +51,8 @@ public function aggregation($aggregator, int $bucketDuration, int $align = 0, in * Splits time series into groups, each group contains time series that share the same * value for the provided label name, then aggregates results in each group. * - * GROUPBY cannot be combined with multiple aggregators set via aggregation(). + * GROUPBY cannot be combined with multiple aggregators set via aggregation() + * or with excludeEmpty(). * * @param string $label * @param string $reducer @@ -69,8 +70,33 @@ public function groupBy(string $label, string $reducer): self throw new UnexpectedValueException('GROUPBY cannot be combined with multiple aggregators.'); } + if (in_array('EXCLUDEEMPTY', $this->arguments, true)) { + throw new UnexpectedValueException('GROUPBY cannot be combined with EXCLUDEEMPTY.'); + } + array_push($this->arguments, 'GROUPBY', $label, 'REDUCE', $reducer); return $this; } + + /** + * Omits matching time series whose reported samples array is empty + * from the command reply. + * + * EXCLUDEEMPTY cannot be combined with groupBy(). + * + * @return $this + */ + public function excludeEmpty(): self + { + if (in_array('GROUPBY', $this->arguments, true)) { + throw new UnexpectedValueException('EXCLUDEEMPTY cannot be combined with GROUPBY.'); + } + + if (!in_array('EXCLUDEEMPTY', $this->arguments, true)) { + $this->arguments[] = 'EXCLUDEEMPTY'; + } + + return $this; + } } diff --git a/core/vendor/predis/predis/src/Command/Argument/TimeSeries/NRangeArguments.php b/core/vendor/predis/predis/src/Command/Argument/TimeSeries/NRangeArguments.php new file mode 100644 index 0000000000..2dd94e148a --- /dev/null +++ b/core/vendor/predis/predis/src/Command/Argument/TimeSeries/NRangeArguments.php @@ -0,0 +1,51 @@ + 0) { + array_push($this->arguments, 'ALIGN', $align); + } + + array_push($this->arguments, 'AGGREGATION', ...$aggregators, ...[$bucketDuration]); + + if ($bucketTimestamp > 0) { + array_push($this->arguments, 'BUCKETTIMESTAMP', $bucketTimestamp); + } + + if (true === $empty) { + $this->arguments[] = 'EMPTY'; + } + + return $this; + } +} diff --git a/core/vendor/predis/predis/src/Command/Argument/TimeSeries/ReadArguments.php b/core/vendor/predis/predis/src/Command/Argument/TimeSeries/ReadArguments.php new file mode 100644 index 0000000000..d38432302d --- /dev/null +++ b/core/vendor/predis/predis/src/Command/Argument/TimeSeries/ReadArguments.php @@ -0,0 +1,60 @@ +arguments, 'BLOCK', $milliseconds, $minCount); + + return $this; + } + + /** + * Caps the reply at the given number of samples. When more samples qualify, + * the oldest ones are returned so callers can page forward. + * + * @param int $maxCount Reply cap, positive. + * @return $this + */ + public function maxCount(int $maxCount): self + { + array_push($this->arguments, 'MAX_COUNT', $maxCount); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->arguments; + } +} diff --git a/core/vendor/predis/predis/src/Command/Container/HIMPORT.php b/core/vendor/predis/predis/src/Command/Container/HIMPORT.php new file mode 100644 index 0000000000..7f93bf85b8 --- /dev/null +++ b/core/vendor/predis/predis/src/Command/Container/HIMPORT.php @@ -0,0 +1,484 @@ + false]`. + * + * The recovery layer only ever acts on fieldsets declared through this container + * (or the `himport` option). The raw command form ($client->himport('SET', ...)), + * pipelines and transactions stay fully explicit: they never recover and + * propagate server errors as-is. + * + * @experimental This API is experimental and may change in a future release. + * + * @method Status prepare(string $fieldset, array $fields) + * @method Status set(string $key, string $fieldset, array $values) + * @method int discard(string $fieldset) + * @method int discardAll() + */ +class HIMPORT extends AbstractContainer +{ + private const SUBCOMMAND_PREPARE = 'PREPARE'; + private const SUBCOMMAND_SET = 'SET'; + private const SUBCOMMAND_DISCARD = 'DISCARD'; + private const SUBCOMMAND_DISCARDALL = 'DISCARDALL'; + + private const NO_SUCH_FIELDSET = 'no such fieldset'; + private const SESSION_KEY_PREFIX = 'himport:'; + + public function getContainerCommandId(): string + { + return 'HIMPORT'; + } + + /** + * Registers an ordered field list under a fieldset name for use by later + * SET calls. On a cluster this is fanned out to every master shard. + * + * @param string $fieldset + * @param array $fields Ordered, non-empty field names (sent verbatim). + * @return Status|ErrorInterface + * @throws ServerException + */ + public function prepare(string $fieldset, array $fields) + { + if (empty($fields)) { + throw new InvalidArgumentException('HIMPORT PREPARE requires a non-empty list of fields.'); + } + + $command = $this->client->createCommand('HIMPORT', [self::SUBCOMMAND_PREPARE, $fieldset, $fields]); + $connection = $this->client->getConnection(); + + if ($connection instanceof ClusterInterface) { + $results = $this->fanOut($connection, $command); + + if (null !== $error = $this->firstError($results)) { + // Do not record a fieldset the server rejected: a poison entry + // would make every later SET retry a PREPARE that cannot succeed. + return $this->surfaceError($error); + } + + // Record and pin only after every master confirmed the PREPARE. + $this->getRegistry()->set($fieldset, $fields); + + foreach ($results as $result) { + $this->pinSessionCommand($result[0], $this->sessionKey($fieldset), $command); + } + + return Status::get('OK'); + } + + $response = $this->client->executeCommand($command); + + if ($response instanceof ErrorInterface) { + return $response; + } + + $this->getRegistry()->set($fieldset, $fields); + $this->pinSessionCommand($this->resolveNode($command), $this->sessionKey($fieldset), $command); + + return $response; + } + + /** + * Creates or overwrites a hash at $key using the field list previously + * prepared under $fieldset on the executing connection. + * + * If the fieldset is missing on the executing connection but known to the + * client, the write is re-prepared and retried through the connection's Retry + * policy: this happens only when retries are configured, and never more than + * the configured number of attempts. With retries disabled the "no such + * fieldset" error propagates unchanged. + * + * @param string $key + * @param string $fieldset + * @param array $values Ordered, non-empty values paired positionally with the prepared fields. + * @return Status|ErrorInterface + * @throws ServerException + */ + public function set(string $key, string $fieldset, array $values) + { + if (empty($values)) { + throw new InvalidArgumentException('HIMPORT SET requires a non-empty list of values.'); + } + + $command = $this->client->createCommand('HIMPORT', [self::SUBCOMMAND_SET, $key, $fieldset, $values]); + $retry = $this->getConfiguredRetry(); + + try { + if (null === $retry) { + // No retry policy available: run once, no recovery. + return $this->executeSet($command, $fieldset); + } + + // Reuse the client-configured Retry (its enabled/disabled state, count + // and backoff). Register our retryable condition on it, the same way + // the cluster and sentinel connections register theirs. + $retry->updateCatchableExceptions([FieldsetNotPreparedException::class]); + + return $retry->callWithRetry( + function () use ($command, $fieldset) { + return $this->executeSet($command, $fieldset); + }, + function (Throwable $exception) use ($command, $fieldset) { + // Only HIMPORT's own "no such fieldset" is recovered and + // retried here; anything else the shared policy might catch + // (connection errors, other server errors) is left to its + // owner and propagated immediately. + if (!$exception instanceof FieldsetNotPreparedException) { + throw $exception; + } + + $this->reprepare($command, $fieldset); + } + ); + } catch (FieldsetNotPreparedException $exception) { + // Retries were disabled or exhausted. Preserve the client's error + // semantics: return the error response when exceptions are off. + if (!$this->client->getOptions()->exceptions) { + return new Error($exception->getMessage()); + } + + throw $exception; + } + } + + /** + * Removes a fieldset from the connection session. On a cluster this is + * fanned out to every master shard. + * + * @param string $fieldset + * @return int|ErrorInterface Number of removals (1 if present, 0 otherwise). + * @throws ServerException + */ + public function discard(string $fieldset) + { + $command = $this->client->createCommand('HIMPORT', [self::SUBCOMMAND_DISCARD, $fieldset]); + $connection = $this->client->getConnection(); + + if ($connection instanceof ClusterInterface) { + $results = $this->fanOut($connection, $command); + + if (null !== $error = $this->firstError($results)) { + // Leave the registry and pins untouched so the fieldset stays + // consistently known and the discard can be retried. + return $this->surfaceError($error); + } + + // Drop registry entry and pins together, only after every master + // confirmed the discard, so the two never disagree. + $this->getRegistry()->remove($fieldset); + $removed = 0; + + foreach ($results as $result) { + $this->unpinSessionCommand($result[0], $this->sessionKey($fieldset)); + $removed = max($removed, (int) $result[1]); + } + + return $removed; + } + + $response = $this->client->executeCommand($command); + + if ($response instanceof ErrorInterface) { + return $response; + } + + $this->getRegistry()->remove($fieldset); + $this->unpinSessionCommand($this->resolveNode($command), $this->sessionKey($fieldset)); + + return $response; + } + + /** + * Removes all fieldsets from the connection session. On a cluster this is + * fanned out to every master shard. + * + * @return int|ErrorInterface Number of fieldsets removed. + * @throws ServerException + */ + public function discardAll() + { + $command = $this->client->createCommand('HIMPORT', [self::SUBCOMMAND_DISCARDALL]); + $connection = $this->client->getConnection(); + + if ($connection instanceof ClusterInterface) { + $results = $this->fanOut($connection, $command); + + if (null !== $error = $this->firstError($results)) { + // Leave client-side state intact so it stays consistent and the + // discard can be retried. + return $this->surfaceError($error); + } + + $this->getRegistry()->clear(); + $removed = 0; + + foreach ($results as $result) { + $this->unpinSessionCommandsByPrefix($result[0], self::SESSION_KEY_PREFIX); + $removed = max($removed, (int) $result[1]); + } + + return $removed; + } + + $response = $this->client->executeCommand($command); + + if ($response instanceof ErrorInterface) { + return $response; + } + + $this->getRegistry()->clear(); + $this->unpinSessionCommandsByPrefix($this->resolveNode($command), self::SESSION_KEY_PREFIX); + + return $response; + } + + /** + * Executes the SET, translating a recoverable "no such fieldset" into a + * retryable exception so the Retry policy can drive re-prepare + retry. + * Non-recoverable errors are returned/propagated unchanged. + * + * @param CommandInterface $command + * @param string $fieldset + * @return Status|ErrorInterface + * @throws FieldsetNotPreparedException + * @throws ServerException + */ + private function executeSet(CommandInterface $command, string $fieldset) + { + try { + $response = $this->client->executeCommand($command); + } catch (ServerException $exception) { + if ($this->canRecover($exception->getMessage(), $fieldset)) { + throw new FieldsetNotPreparedException($exception->getMessage()); + } + + throw $exception; + } + + if ($response instanceof ErrorInterface && $this->canRecover($response->getMessage(), $fieldset)) { + throw new FieldsetNotPreparedException($response->getMessage()); + } + + return $response; + } + + /** + * Re-prepares $fieldset on the connection the (next) SET attempt targets. + * A failed re-prepare aborts recovery and is surfaced as the root cause, + * through the same "no such fieldset" path so it honours the client's + * exceptions option (thrown when on, returned as an Error when off). + * + * @param CommandInterface $setCommand + * @param string $fieldset + * @throws FieldsetNotPreparedException + */ + private function reprepare(CommandInterface $setCommand, string $fieldset): void + { + $fields = $this->getRegistry()->get($fieldset); + $prepare = $this->client->createCommand('HIMPORT', [self::SUBCOMMAND_PREPARE, $fieldset, $fields]); + + // Resolve after the SET failed so that, following a MOVED/ASK redirection, + // we re-prepare on the node the retry will actually target. + $node = $this->resolveNode($setCommand); + $response = $node->executeCommand($prepare); + + if ($response instanceof ErrorInterface) { + // Surface the PREPARE error as the root cause via set()'s catch, which + // applies the exceptions option (throw vs. return an Error response). + throw new FieldsetNotPreparedException($response->getMessage()); + } + + $this->pinSessionCommand($node, $this->sessionKey($fieldset), $prepare); + } + + /** + * Returns the Retry policy configured on the executing connection, or null + * when none is available. HIMPORT does not define its own retry behaviour: + * it reuses whatever the client is configured with, so retries happen only + * when (and as many times as) the user enabled them. + * + * @return Retry|null + */ + private function getConfiguredRetry(): ?Retry + { + $parameters = $this->client->getConnection()->getParameters(); + + if (null !== $parameters && $parameters->retry instanceof Retry) { + return $parameters->retry; + } + + return null; + } + + /** + * @param string $message Server error message. + * @param string $fieldset Fieldset referenced by the failed SET. + */ + private function canRecover(string $message, string $fieldset): bool + { + return false !== stripos($message, self::NO_SUCH_FIELDSET) + && $this->getOptions()->isAutoPrepareEnabled() + && $this->getRegistry()->has($fieldset); + } + + /** + * Executes a command on every master shard of a cluster connection. + * + * Fails fast rather than silently no-op'ing: a cluster that cannot be + * enumerated, or that yields no master shards, must never look like a + * successful fan-out to the callers that mutate the registry and pins. + * + * @param ClusterInterface $cluster + * @param CommandInterface $command + * @return array + * @throws NotSupportedException + */ + private function fanOut(ClusterInterface $cluster, CommandInterface $command): array + { + if (!$cluster instanceof IteratorAggregate) { + throw new NotSupportedException(sprintf( + "HIMPORT requires an iterable cluster connection to fan out to master shards; '%s' is not iterable.", + get_class($cluster) + )); + } + + $results = []; + + foreach ($cluster->getIterator() as $node) { + $results[] = [$node, $node->executeCommand($command)]; + } + + if (empty($results)) { + throw new NotSupportedException( + 'HIMPORT could not fan out: the cluster connection reported no master shards.' + ); + } + + return $results; + } + + /** + * @param array $results + * @return ErrorInterface|null + */ + private function firstError(array $results): ?ErrorInterface + { + foreach ($results as $result) { + if ($result[1] instanceof ErrorInterface) { + return $result[1]; + } + } + + return null; + } + + /** + * @param ErrorInterface $error + * @return ErrorInterface + * @throws ServerException + */ + private function surfaceError(ErrorInterface $error) + { + if ($this->client->getOptions()->exceptions) { + throw new ServerException($error->getMessage()); + } + + return $error; + } + + /** + * Resolves the physical connection that executes (or executed) $command. + * + * @param CommandInterface $command + * @return NodeConnectionInterface + */ + private function resolveNode(CommandInterface $command): NodeConnectionInterface + { + $connection = $this->client->getConnection(); + + if ($connection instanceof AggregateConnectionInterface) { + return $connection->getConnectionByCommand($command); + } + + return $connection; + } + + private function pinSessionCommand(NodeConnectionInterface $node, string $key, CommandInterface $command): void + { + if (method_exists($node, 'addSessionCommand')) { + $node->addSessionCommand($key, $command); + } + } + + private function unpinSessionCommand(NodeConnectionInterface $node, string $key): void + { + if (method_exists($node, 'removeSessionCommand')) { + $node->removeSessionCommand($key); + } + } + + private function unpinSessionCommandsByPrefix(NodeConnectionInterface $node, string $prefix): void + { + if (method_exists($node, 'removeSessionCommandsByPrefix')) { + $node->removeSessionCommandsByPrefix($prefix); + } + } + + private function sessionKey(string $fieldset): string + { + return self::SESSION_KEY_PREFIX . $fieldset; + } + + private function getOptions(): HimportOptions + { + return $this->client->getOptions()->himport; + } + + private function getRegistry() + { + return $this->getOptions()->getRegistry(); + } +} diff --git a/core/vendor/predis/predis/src/Command/Redis/BLMOVEM.php b/core/vendor/predis/predis/src/Command/Redis/BLMOVEM.php new file mode 100644 index 0000000000..8c30b0a4cc --- /dev/null +++ b/core/vendor/predis/predis/src/Command/Redis/BLMOVEM.php @@ -0,0 +1,38 @@ +getArguments(); + + // Only HIMPORT SET carries a key (at index 1, after the subcommand). + // PREPARE/DISCARD/DISCARDALL operate on fieldset names, never keys. + if (isset($arguments[0], $arguments[1]) && strtoupper($arguments[0]) === 'SET') { + $arguments[1] = $prefix . $arguments[1]; + $this->setRawArguments($arguments); + } + } +} diff --git a/core/vendor/predis/predis/src/Command/Redis/LMOVEM.php b/core/vendor/predis/predis/src/Command/Redis/LMOVEM.php new file mode 100644 index 0000000000..8c882e417b --- /dev/null +++ b/core/vendor/predis/predis/src/Command/Redis/LMOVEM.php @@ -0,0 +1,100 @@ +getQuantifierOffset(); + + if (count($arguments) <= $offset) { + parent::setArguments($arguments); + + return; + } + + $processed = array_slice($arguments, 0, $offset); + + $quantifier = strtoupper($arguments[$offset]); + + if (!in_array($quantifier, [self::COUNT, self::EXACTLY], true)) { + throw new UnexpectedValueException('Quantifier argument accepts only: COUNT, EXACTLY values'); + } + + if (!isset($arguments[$offset + 1])) { + throw new UnexpectedValueException("{$quantifier} quantifier requires a count argument"); + } + + if (!isset($arguments[$offset + 2])) { + throw new UnexpectedValueException("{$quantifier} quantifier requires an ordering argument"); + } + + $ordering = strtoupper($arguments[$offset + 2]); + + if (!in_array($ordering, [self::OBO, self::BULK], true)) { + throw new UnexpectedValueException('Ordering argument accepts only: OBO, BULK values'); + } + + array_push($processed, $quantifier, $arguments[$offset + 1], $ordering); + + parent::setArguments($processed); + } + + public function prefixKeys($prefix) + { + if ($arguments = $this->getArguments()) { + $arguments[0] = $prefix . $arguments[0]; + + if (isset($arguments[1])) { + $arguments[1] = $prefix . $arguments[1]; + } + + $this->setRawArguments($arguments); + } + } +} diff --git a/core/vendor/predis/predis/src/Command/Redis/SDIFFCARD.php b/core/vendor/predis/predis/src/Command/Redis/SDIFFCARD.php new file mode 100644 index 0000000000..80d97f4e4a --- /dev/null +++ b/core/vendor/predis/predis/src/Command/Redis/SDIFFCARD.php @@ -0,0 +1,43 @@ +setLimit($arguments); + $arguments = $this->getArguments(); + + $this->setKeys($arguments); + } +} diff --git a/core/vendor/predis/predis/src/Command/Redis/SUNIONCARD.php b/core/vendor/predis/predis/src/Command/Redis/SUNIONCARD.php new file mode 100644 index 0000000000..421ffa1784 --- /dev/null +++ b/core/vendor/predis/predis/src/Command/Redis/SUNIONCARD.php @@ -0,0 +1,52 @@ +setApprox($arguments); + $arguments = $this->getArguments(); + + $this->setLimit($arguments); + $arguments = $this->getArguments(); + + $this->setKeys($arguments); + $this->filterArguments(); + } +} diff --git a/core/vendor/predis/predis/src/Command/Redis/Search/FTALIASLIST.php b/core/vendor/predis/predis/src/Command/Redis/Search/FTALIASLIST.php new file mode 100644 index 0000000000..c041d3cea7 --- /dev/null +++ b/core/vendor/predis/predis/src/Command/Redis/Search/FTALIASLIST.php @@ -0,0 +1,33 @@ +applyPrefixForFirstArgument($prefix); + } +} diff --git a/core/vendor/predis/predis/src/Command/Redis/TimeSeries/TSNRANGE.php b/core/vendor/predis/predis/src/Command/Redis/TimeSeries/TSNRANGE.php new file mode 100644 index 0000000000..99711ed39d --- /dev/null +++ b/core/vendor/predis/predis/src/Command/Redis/TimeSeries/TSNRANGE.php @@ -0,0 +1,61 @@ +toArray() : []; + + parent::setArguments(array_merge( + [count($keys)], + $keys, + [$fromTimestamp, $toTimestamp], + $commandArguments + )); + } + + public function prefixKeys($prefix) + { + $arguments = $this->getArguments(); + + $keysCount = $arguments[0]; + $keys = array_slice($arguments, 1, $keysCount); + $prefixedKeys = array_map(static function ($key) use ($prefix) { + return $prefix . $key; + }, $keys); + + $this->setRawArguments(array_merge( + [$arguments[0]], + $prefixedKeys, + array_slice($arguments, $keysCount + 1) + )); + } +} diff --git a/core/vendor/predis/predis/src/Command/Redis/TimeSeries/TSNREVRANGE.php b/core/vendor/predis/predis/src/Command/Redis/TimeSeries/TSNREVRANGE.php new file mode 100644 index 0000000000..8a74336772 --- /dev/null +++ b/core/vendor/predis/predis/src/Command/Redis/TimeSeries/TSNREVRANGE.php @@ -0,0 +1,29 @@ +toArray() + : []; + + parent::setArguments(array_merge( + array_slice($arguments, 0, 2), + $commandArguments + )); + } + + public function prefixKeys($prefix) + { + $this->applyPrefixForFirstArgument($prefix); + } +} diff --git a/core/vendor/predis/predis/src/Command/Redis/XREAD.php b/core/vendor/predis/predis/src/Command/Redis/XREAD.php index 31c301f1c1..99867786f2 100644 --- a/core/vendor/predis/predis/src/Command/Redis/XREAD.php +++ b/core/vendor/predis/predis/src/Command/Redis/XREAD.php @@ -14,6 +14,10 @@ use Predis\Command\Command as RedisCommand; +/** + * @deprecated Public API will be changed in the next major version. + * XREAD_V4 API will be used instead. + */ class XREAD extends RedisCommand { public function getId() diff --git a/core/vendor/predis/predis/src/Command/Redis/XREADGROUP_CLAIM.php b/core/vendor/predis/predis/src/Command/Redis/XREADGROUP_CLAIM.php index e08a7b966a..621247fdaf 100644 --- a/core/vendor/predis/predis/src/Command/Redis/XREADGROUP_CLAIM.php +++ b/core/vendor/predis/predis/src/Command/Redis/XREADGROUP_CLAIM.php @@ -32,6 +32,14 @@ public function setArguments(array $arguments) array_push($processedArguments, 'COUNT', $arguments[3]); } + if (count($arguments) >= 8 && null !== $arguments[7]) { + array_push($processedArguments, 'MAXCOUNT', $arguments[7]); + } + + if (count($arguments) >= 9 && null !== $arguments[8]) { + array_push($processedArguments, 'MAXSIZE', $arguments[8]); + } + if (count($arguments) >= 5 && null !== $arguments[4]) { array_push($processedArguments, 'BLOCK', $arguments[4]); } @@ -40,7 +48,7 @@ public function setArguments(array $arguments) $processedArguments[] = 'NOACK'; } - if (count($arguments) >= 7 && false !== $arguments[6]) { + if (count($arguments) >= 7 && false !== $arguments[6] && null !== $arguments[6]) { array_push($processedArguments, 'CLAIM', $arguments[6]); } diff --git a/core/vendor/predis/predis/src/Command/Redis/XREAD_V4.php b/core/vendor/predis/predis/src/Command/Redis/XREAD_V4.php new file mode 100644 index 0000000000..2357cf506f --- /dev/null +++ b/core/vendor/predis/predis/src/Command/Redis/XREAD_V4.php @@ -0,0 +1,84 @@ += 2 && null !== $arguments[1]) { + array_push($processedArguments, 'COUNT', $arguments[1]); + } + + if (count($arguments) >= 4 && null !== $arguments[3]) { + array_push($processedArguments, 'MAXCOUNT', $arguments[3]); + } + + if (count($arguments) >= 5 && null !== $arguments[4]) { + array_push($processedArguments, 'MAXSIZE', $arguments[4]); + } + + if (count($arguments) >= 3 && null !== $arguments[2]) { + array_push($processedArguments, 'BLOCK', $arguments[2]); + } + + array_push($processedArguments, 'STREAMS', ...array_keys($arguments[0]), ...array_values($arguments[0])); + + parent::setArguments($processedArguments); + } + + public function parseResponse($data) + { + if (!$data) { + return []; + } + + if ($data !== array_values($data)) { + return $data; // Relay + } + + $processedData = []; + + foreach ($data as $stream) { + $processedData[$stream[0]] = $stream[1]; + } + + return $processedData; + } + + public function prefixKeys($prefix) + { + $arguments = $this->getArguments(); + $keyIdsStartingIndex = array_search('STREAMS', $arguments) + 1; + $keysAndIdsCount = count($arguments) - $keyIdsStartingIndex; + $keysCount = $keysAndIdsCount / 2; + + for ($i = $keyIdsStartingIndex; $i < $keyIdsStartingIndex + $keysCount; $i++) { + $arguments[$i] = $prefix . $arguments[$i]; + } + + parent::setRawArguments($arguments); + } +} diff --git a/core/vendor/predis/predis/src/Command/Redis/ZMSCORE.php b/core/vendor/predis/predis/src/Command/Redis/ZMSCORE.php index 79715c534a..92d2edd606 100644 --- a/core/vendor/predis/predis/src/Command/Redis/ZMSCORE.php +++ b/core/vendor/predis/predis/src/Command/Redis/ZMSCORE.php @@ -12,7 +12,7 @@ namespace Predis\Command\Redis; -use Predis\Command\Command as RedisCommand; +use Predis\Command\PrefixableCommand as RedisCommand; /** * @see https://redis.io/commands/zmscore/ @@ -31,4 +31,9 @@ public function getId() { return 'ZMSCORE'; } + + public function prefixKeys($prefix) + { + $this->applyPrefixForFirstArgument($prefix); + } } diff --git a/core/vendor/predis/predis/src/Command/RedisFactory.php b/core/vendor/predis/predis/src/Command/RedisFactory.php index 2de80a6272..282f607cad 100644 --- a/core/vendor/predis/predis/src/Command/RedisFactory.php +++ b/core/vendor/predis/predis/src/Command/RedisFactory.php @@ -14,6 +14,7 @@ use Predis\ClientConfiguration; use Predis\Command\Redis\FUNCTIONS; +use Predis\Command\Redis\TimeSeries\TSREAD; /** * Command factory for mainline Redis servers. @@ -36,6 +37,8 @@ public function __construct() 'OBJECT' => 'Predis\Command\Redis\OBJECT_', // Class name corresponds to PHP reserved word "function", added mapping to bypass restrictions 'FUNCTION' => FUNCTIONS::class, + // Wire ID mapping so cluster-routable module commands resolve by their command ID + 'TS.READ' => TSREAD::class, ]; } diff --git a/core/vendor/predis/predis/src/Command/Traits/Approx.php b/core/vendor/predis/predis/src/Command/Traits/Approx.php new file mode 100644 index 0000000000..80db4372a8 --- /dev/null +++ b/core/vendor/predis/predis/src/Command/Traits/Approx.php @@ -0,0 +1,44 @@ + ordered, non-empty field + * list. Pre-declared fieldsets are prepared on demand the + * first time a `HIMPORT SET` references them on a connection, + * so the application does not need to call `prepare()` for + * them (this uses the same auto-prepare mechanism below). + * - `auto_prepare`: optional bool (default true) toggling whether the himport + * container prepares/re-prepares fieldsets on demand and + * recovers from "no such fieldset". + * + * @experimental This option is experimental and its shape may change in a future release. + */ +class Himport implements OptionInterface +{ + /** + * {@inheritdoc} + */ + public function filter(OptionsInterface $options, $value) + { + if ($value instanceof HimportOptions) { + return $value; + } + + if (!is_array($value)) { + throw new InvalidArgumentException( + 'Invalid value for the himport option: expected an array or a Predis\Himport\HimportOptions instance.' + ); + } + + $autoPrepare = array_key_exists('auto_prepare', $value) + ? (bool) $value['auto_prepare'] + : true; + + $registry = null; + + if (array_key_exists('fieldsets', $value)) { + $registry = $this->buildRegistry($value['fieldsets']); + } + + return new HimportOptions($registry, $autoPrepare); + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + return new HimportOptions(null, true); + } + + /** + * Builds a fieldset registry pre-seeded from the `fieldsets` option. + * + * @param mixed $fieldsets + * @return FieldsetRegistry + */ + private function buildRegistry($fieldsets): FieldsetRegistry + { + if (!is_array($fieldsets)) { + throw new InvalidArgumentException( + 'The "fieldsets" himport option must be a map of fieldset name => list of field names.' + ); + } + + $registry = new FieldsetRegistry(); + + foreach ($fieldsets as $name => $fields) { + if (!is_array($fields) || empty($fields)) { + throw new InvalidArgumentException( + sprintf('Fieldset "%s" must be a non-empty list of field names.', $name) + ); + } + + $registry->set((string) $name, array_values($fields)); + } + + return $registry; + } +} diff --git a/core/vendor/predis/predis/src/Configuration/Options.php b/core/vendor/predis/predis/src/Configuration/Options.php index 3a051d0664..6e98c86a6d 100644 --- a/core/vendor/predis/predis/src/Configuration/Options.php +++ b/core/vendor/predis/predis/src/Configuration/Options.php @@ -33,6 +33,7 @@ class Options implements OptionsInterface 'prefix' => Option\Prefix::class, 'crc16' => Option\CRC16::class, 'upstream_driver' => Option\UpstreamDriver::class, + 'himport' => Option\Himport::class, ]; /** @var array */ diff --git a/core/vendor/predis/predis/src/Configuration/OptionsInterface.php b/core/vendor/predis/predis/src/Configuration/OptionsInterface.php index 20b9a065f7..a604073838 100644 --- a/core/vendor/predis/predis/src/Configuration/OptionsInterface.php +++ b/core/vendor/predis/predis/src/Configuration/OptionsInterface.php @@ -15,6 +15,7 @@ use Predis\Command\Processor\ProcessorInterface; use Predis\Connection\FactoryInterface; use Predis\Connection\ParametersInterface; +use Predis\Himport\HimportOptions; /** * @property callable $aggregate Custom aggregate connection initializer @@ -27,6 +28,7 @@ * @property callable $replication Aggregate connection initializer for replication * @property int $readTimeout Timeout in milliseconds between read operations on reading from multiple connections. * @property string|string[] $upstream_driver Upstream driver info for CLIENT SETINFO. + * @property HimportOptions $himport HIMPORT fieldset registry and auto-recovery configuration. */ interface OptionsInterface { diff --git a/core/vendor/predis/predis/src/Connection/AbstractConnection.php b/core/vendor/predis/predis/src/Connection/AbstractConnection.php index b61c425d6a..03629a9875 100644 --- a/core/vendor/predis/predis/src/Connection/AbstractConnection.php +++ b/core/vendor/predis/predis/src/Connection/AbstractConnection.php @@ -47,6 +47,17 @@ abstract class AbstractConnection implements NodeConnectionInterface */ protected $initCommands = []; + /** + * Commands that restore server-side session state (e.g. HIMPORT fieldsets) + * on the physical connection. Unlike init commands, these are keyed so they + * can be replaced and removed, are replayed best-effort after init commands + * on every (re)connect, and are intentionally excluded from serialization: a + * deserialized or freshly created connection is a new server session. + * + * @var array + */ + protected $sessionCommands = []; + /** * @param ParametersInterface $parameters Initialization parameters for the connection. */ @@ -127,6 +138,50 @@ public function getInitCommands(): array return $this->initCommands; } + /** + * Registers (or replaces) a session command under the given key so it is + * replayed on the next (re)connect. Keying makes replacement idempotent. + * + * @param string $key Identifier for the session command. + * @param CommandInterface $command Command replayed to restore session state. + */ + public function addSessionCommand(string $key, CommandInterface $command): void + { + $this->sessionCommands[$key] = $command; + } + + /** + * Removes a single session command previously registered under the key. + * + * @param string $key Identifier for the session command. + */ + public function removeSessionCommand(string $key): void + { + unset($this->sessionCommands[$key]); + } + + /** + * Removes every session command whose key starts with the given prefix. + * + * @param string $prefix Key prefix (e.g. "himport:") to match. + */ + public function removeSessionCommandsByPrefix(string $prefix): void + { + foreach (array_keys($this->sessionCommands) as $key) { + if (strpos($key, $prefix) === 0) { + unset($this->sessionCommands[$key]); + } + } + } + + /** + * @return array + */ + public function getSessionCommands(): array + { + return $this->sessionCommands; + } + /** * {@inheritdoc} */ diff --git a/core/vendor/predis/predis/src/Connection/RelayConnection.php b/core/vendor/predis/predis/src/Connection/RelayConnection.php index b48e054d33..8e287b3ce9 100644 --- a/core/vendor/predis/predis/src/Connection/RelayConnection.php +++ b/core/vendor/predis/predis/src/Connection/RelayConnection.php @@ -369,15 +369,25 @@ protected function initializeUnixConnection(ParametersInterface $parameters) */ public function connect() { - if (parent::connect() && $this->initCommands) { - foreach ($this->initCommands as $command) { - $response = $this->executeCommand($command); - - if ($response instanceof ErrorResponseInterface && ($command->getId() === 'CLIENT')) { - // Do nothing on CLIENT SETINFO command failure - } elseif ($response instanceof ErrorResponseInterface) { - $this->onConnectionError("`{$command->getId()}` failed: {$response->getMessage()}", 0); - } + if (!parent::connect()) { + return; + } + + foreach ($this->initCommands as $command) { + $response = $this->executeCommand($command); + + if ($response instanceof ErrorResponseInterface && ($command->getId() === 'CLIENT')) { + // Do nothing on CLIENT SETINFO command failure + } elseif ($response instanceof ErrorResponseInterface) { + $this->onConnectionError("`{$command->getId()}` failed: {$response->getMessage()}", 0); + } + } + + foreach ($this->sessionCommands as $key => $command) { + $response = $this->executeCommand($command); + + if ($response instanceof ErrorResponseInterface) { + unset($this->sessionCommands[$key]); } } } diff --git a/core/vendor/predis/predis/src/Connection/Replication/SentinelReplication.php b/core/vendor/predis/predis/src/Connection/Replication/SentinelReplication.php index 4a5099f80f..af4376f6b8 100644 --- a/core/vendor/predis/predis/src/Connection/Replication/SentinelReplication.php +++ b/core/vendor/predis/predis/src/Connection/Replication/SentinelReplication.php @@ -33,6 +33,7 @@ use Predis\Response\ServerException; use Predis\Retry\Retry; use Predis\Retry\Strategy\ExponentialBackoff; +use Throwable; /** * @author Daniele Alessandri @@ -753,9 +754,12 @@ private function retryCommandOnFailure(CommandInterface $command, $method) return $response; }; - $failCallback = function (CommunicationException $exception) { + $failCallback = function (Throwable $exception) { $this->wipeServerList(); - $exception->getConnection()->disconnect(); + + if ($exception instanceof CommunicationException) { + $exception->getConnection()->disconnect(); + } }; return $retry->callWithRetry($doCallback, $failCallback); diff --git a/core/vendor/predis/predis/src/Connection/StreamConnection.php b/core/vendor/predis/predis/src/Connection/StreamConnection.php index 6ed0ebc432..1dd5d830a5 100644 --- a/core/vendor/predis/predis/src/Connection/StreamConnection.php +++ b/core/vendor/predis/predis/src/Connection/StreamConnection.php @@ -89,7 +89,11 @@ protected function createResource(): StreamInterface */ public function connect() { - if (parent::connect() && $this->initCommands) { + if (!parent::connect()) { + return; + } + + if ($this->initCommands) { $responses = $this->sendPipeline($this->initCommands); if ($responses[0][0] instanceof ErrorResponseInterface) { @@ -103,6 +107,35 @@ public function connect() $this->handleOnConnectResponse($response[0], $response[1]); } } + + $this->replaySessionCommands(); + } + + /** + * Replays registered session commands after a (re)connect. + * + * Replay is best-effort: a server error for a single command drops only that + * entry and never tears down the connection (contrast with init commands, + * whose failures raise a ConnectionException). Whatever state could not be + * restored is surfaced later as the authoritative server error on the first + * command that depends on it. + * + * @throws CommunicationException + */ + protected function replaySessionCommands(): void + { + if (!$this->sessionCommands) { + return; + } + + $keys = array_keys($this->sessionCommands); + $responses = $this->sendPipeline(array_values($this->sessionCommands)); + + foreach ($responses as $index => $response) { + if ($response[0] instanceof ErrorResponseInterface) { + unset($this->sessionCommands[$keys[$index]]); + } + } } /** diff --git a/core/vendor/predis/predis/src/Himport/FieldsetNotPreparedException.php b/core/vendor/predis/predis/src/Himport/FieldsetNotPreparedException.php new file mode 100644 index 0000000000..f8a8e1dd4e --- /dev/null +++ b/core/vendor/predis/predis/src/Himport/FieldsetNotPreparedException.php @@ -0,0 +1,31 @@ + + */ + private $fieldsets = []; + + /** + * Registers a fieldset, replacing any previous definition (last PREPARE + * wins, matching the server's silent-replace semantics). Field order is + * preserved verbatim. + * + * @param string $name Fieldset name. + * @param array $fields Ordered field names. + */ + public function set(string $name, array $fields): void + { + $this->fieldsets[$name] = array_values($fields); + } + + /** + * @param string $name Fieldset name. + * @return array|null The ordered field list, or null when not registered. + */ + public function get(string $name): ?array + { + return $this->fieldsets[$name] ?? null; + } + + /** + * @param string $name Fieldset name (empty string is a valid name). + * @return bool + */ + public function has(string $name): bool + { + return array_key_exists($name, $this->fieldsets); + } + + /** + * @param string $name Fieldset name to forget. + */ + public function remove(string $name): void + { + unset($this->fieldsets[$name]); + } + + /** + * Forgets every registered fieldset. + */ + public function clear(): void + { + $this->fieldsets = []; + } + + /** + * @return array + */ + public function all(): array + { + return $this->fieldsets; + } +} diff --git a/core/vendor/predis/predis/src/Himport/HimportOptions.php b/core/vendor/predis/predis/src/Himport/HimportOptions.php new file mode 100644 index 0000000000..5f72b2d4f8 --- /dev/null +++ b/core/vendor/predis/predis/src/Himport/HimportOptions.php @@ -0,0 +1,58 @@ +registry = $registry ?: new FieldsetRegistry(); + $this->autoPrepare = $autoPrepare; + } + + /** + * @return FieldsetRegistry + */ + public function getRegistry(): FieldsetRegistry + { + return $this->registry; + } + + /** + * @return bool + */ + public function isAutoPrepareEnabled(): bool + { + return $this->autoPrepare; + } +} diff --git a/core/vendor/predis/predis/src/Pipeline/Pipeline.php b/core/vendor/predis/predis/src/Pipeline/Pipeline.php index 615d66bfe3..ab3b946b65 100644 --- a/core/vendor/predis/predis/src/Pipeline/Pipeline.php +++ b/core/vendor/predis/predis/src/Pipeline/Pipeline.php @@ -70,6 +70,40 @@ public function __call($method, $arguments) return $this; } + /** + * Reads entries from one or multiple streams. + * + * @deprecated Use xread_v4() instead. Public API will be changed in the next major version. + * + * @param int|null $count Maximum number of entries per stream + * @param int|null $block Milliseconds to block waiting for new entries + * @param array|null $streams Stream keys to read from + * @param string ...$id Last-seen ID per stream + * @return $this + */ + public function xread($count = null, $block = null, ?array $streams = null, ...$id) + { + return $this->__call('xread', func_get_args()); + } + + /** + * Reads entries from one or multiple streams as part of a consumer group. + * + * @deprecated Use xreadgroup_claim() instead. Public API will be changed in the next major version. + * + * @param string $group Consumer-group name + * @param string $consumer Consumer name + * @param int|null $count Maximum number of entries per stream + * @param int|null $blockMs Milliseconds to block waiting for new entries + * @param bool $noAck Do not add entries to the pending entries list + * @param string ...$keyOrId Stream keys followed by one ID per stream + * @return $this + */ + public function xreadgroup($group, $consumer, $count = null, $blockMs = null, $noAck = false, ...$keyOrId) + { + return $this->__call('xreadgroup', func_get_args()); + } + /** * Queues a command instance into the pipeline buffer. * diff --git a/core/vendor/predis/predis/src/Retry/Retry.php b/core/vendor/predis/predis/src/Retry/Retry.php index d2a19e166c..d93c2a458c 100644 --- a/core/vendor/predis/predis/src/Retry/Retry.php +++ b/core/vendor/predis/predis/src/Retry/Retry.php @@ -72,12 +72,20 @@ public function updateRetriesCount(int $retries): void /** * Extend catchable exceptions list. * + * Already-registered classes are ignored, so callers may safely re-register + * on every command without the list (and the per-failure lookup) growing + * without bound. + * * @param array $catchableExceptions * @return void */ public function updateCatchableExceptions(array $catchableExceptions): void { - $this->catchableExceptions = array_merge($this->catchableExceptions, $catchableExceptions); + foreach ($catchableExceptions as $catchableException) { + if (!in_array($catchableException, $this->catchableExceptions, true)) { + $this->catchableExceptions[] = $catchableException; + } + } } /** diff --git a/core/vendor/predis/predis/src/Transaction/MultiExec.php b/core/vendor/predis/predis/src/Transaction/MultiExec.php index e36bd49b08..393e3c8b2a 100644 --- a/core/vendor/predis/predis/src/Transaction/MultiExec.php +++ b/core/vendor/predis/predis/src/Transaction/MultiExec.php @@ -181,6 +181,40 @@ public function __call($method, $arguments) ); } + /** + * Reads entries from one or multiple streams. + * + * @deprecated Use xread_v4() instead. Public API will be changed in the next major version. + * + * @param int|null $count Maximum number of entries per stream + * @param int|null $block Milliseconds to block waiting for new entries + * @param array|null $streams Stream keys to read from + * @param string ...$id Last-seen ID per stream + * @return $this|mixed + */ + public function xread($count = null, $block = null, ?array $streams = null, ...$id) + { + return $this->__call('xread', func_get_args()); + } + + /** + * Reads entries from one or multiple streams as part of a consumer group. + * + * @deprecated Use xreadgroup_claim() instead. Public API will be changed in the next major version. + * + * @param string $group Consumer-group name + * @param string $consumer Consumer name + * @param int|null $count Maximum number of entries per stream + * @param int|null $blockMs Milliseconds to block waiting for new entries + * @param bool $noAck Do not add entries to the pending entries list + * @param string ...$keyOrId Stream keys followed by one ID per stream + * @return $this|mixed + */ + public function xreadgroup($group, $consumer, $count = null, $blockMs = null, $noAck = false, ...$keyOrId) + { + return $this->__call('xreadgroup', func_get_args()); + } + /** * Executes the specified Redis command. * diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/app-window-bottom-left.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/app-window-bottom-left.svg new file mode 100644 index 0000000000..0447776ed5 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/app-window-bottom-left.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/app-window-bottom-right.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/app-window-bottom-right.svg new file mode 100644 index 0000000000..05621392e5 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/app-window-bottom-right.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/app-window-bottom.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/app-window-bottom.svg new file mode 100644 index 0000000000..56ba7f389c --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/app-window-bottom.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/app-window-center.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/app-window-center.svg new file mode 100644 index 0000000000..89e0f15ddf --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/app-window-center.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/arrow-fork-triple.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/arrow-fork-triple.svg new file mode 100644 index 0000000000..4f3980a196 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/arrow-fork-triple.svg @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-adobe-after-effects.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-adobe-after-effects.svg new file mode 100644 index 0000000000..5f294fef41 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-adobe-after-effects.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-kakao-talk.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-kakao-talk.svg new file mode 100644 index 0000000000..135aa4140a --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-kakao-talk.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-signal-f.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-signal-f.svg new file mode 100644 index 0000000000..3c5a2b3707 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-signal-f.svg @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-signal-filled.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-signal-filled.svg new file mode 100644 index 0000000000..3c5a2b3707 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-signal-filled.svg @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-signal.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-signal.svg new file mode 100644 index 0000000000..188eb3351c --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/brand-signal.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/credits.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/credits.svg index e88be276f1..11cdc95dc8 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/credits.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/credits.svg @@ -11,9 +11,9 @@ class="icon icon-tabler icons-tabler-outline icon-tabler-credits" > - + - + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/currency-ruble.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/currency-ruble.svg new file mode 100644 index 0000000000..bfc9071d76 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/currency-ruble.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/currency-tether.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/currency-tether.svg index d56ea26a5d..858e3953dc 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/currency-tether.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/currency-tether.svg @@ -11,7 +11,7 @@ class="icon icon-tabler icons-tabler-outline icon-tabler-currency-tether" > - + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/device-3d-camera.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/device-3d-camera.svg index 3921b0d002..79bd3f74e2 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/device-3d-camera.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/device-3d-camera.svg @@ -12,6 +12,6 @@ > - + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/device-screen.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/device-screen.svg index 828d987280..1fd6bed1e7 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/device-screen.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/device-screen.svg @@ -11,7 +11,7 @@ class="icon icon-tabler icons-tabler-outline icon-tabler-device-screen" > - + diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/device-vision-pro-wifi.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/device-vision-pro-wifi.svg new file mode 100644 index 0000000000..4c1a8a9ee6 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/device-vision-pro-wifi.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/device-workstation.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/device-workstation.svg new file mode 100644 index 0000000000..8778907f3c --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/device-workstation.svg @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/dragon.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/dragon.svg new file mode 100644 index 0000000000..cc2ce2a60a --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/dragon.svg @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/flip-horizontal.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/flip-horizontal.svg index 533026ca18..16eef11318 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/flip-horizontal.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/flip-horizontal.svg @@ -11,7 +11,7 @@ class="icon icon-tabler icons-tabler-outline icon-tabler-flip-horizontal" > - - - + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/flip-vertical.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/flip-vertical.svg index 286dd8e2a3..af4364ab63 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/flip-vertical.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/flip-vertical.svg @@ -11,7 +11,7 @@ class="icon icon-tabler icons-tabler-outline icon-tabler-flip-vertical" > - - - + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/footsteps.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/footsteps.svg new file mode 100644 index 0000000000..a661ef0f64 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/footsteps.svg @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/gender-travesti.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/gender-travesti.svg new file mode 100644 index 0000000000..edfec34248 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/gender-travesti.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/home-lock.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/home-lock.svg index 3278f13462..f8f3c2e7a3 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/home-lock.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/home-lock.svg @@ -14,6 +14,6 @@ - + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/icosahedron.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/icosahedron.svg new file mode 100644 index 0000000000..1b54e2b81f --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/icosahedron.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/italic-off.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/italic-off.svg new file mode 100644 index 0000000000..a0fae221b3 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/italic-off.svg @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/map-lock.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/map-lock.svg index 71483f9b55..e7c40ffde7 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/map-lock.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/map-lock.svg @@ -13,6 +13,6 @@ - + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/mood-confused-f.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/mood-confused-f.svg new file mode 100644 index 0000000000..09c9c67614 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/mood-confused-f.svg @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/mood-confused-filled.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/mood-confused-filled.svg new file mode 100644 index 0000000000..09c9c67614 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/mood-confused-filled.svg @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/mood-confused.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/mood-confused.svg new file mode 100644 index 0000000000..2efb00927b --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/mood-confused.svg @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/notdef.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/notdef.svg index 3da1a5f1cc..652bd38075 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/notdef.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/notdef.svg @@ -13,5 +13,5 @@ - + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/physiotherapist.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/physiotherapist.svg new file mode 100644 index 0000000000..413f8fa991 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/physiotherapist.svg @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/play-bug.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/play-bug.svg new file mode 100644 index 0000000000..d98045974a --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/play-bug.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/play-bugs.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/play-bugs.svg new file mode 100644 index 0000000000..06aab5efa2 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/play-bugs.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/queue-pop-in.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/queue-pop-in.svg index 2d0b5b11b2..3f101f6143 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/queue-pop-in.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/queue-pop-in.svg @@ -12,7 +12,7 @@ > - + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/remote-control.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/remote-control.svg new file mode 100644 index 0000000000..1baa61be77 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/remote-control.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/rocking-chair.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/rocking-chair.svg new file mode 100644 index 0000000000..8aa80f2d08 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/rocking-chair.svg @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/run-sprint.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/run-sprint.svg new file mode 100644 index 0000000000..e989af96fa --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/run-sprint.svg @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/server-bolt.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/server-bolt.svg index e3c66479a6..e1eb0f5d6e 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/server-bolt.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/server-bolt.svg @@ -11,7 +11,7 @@ class="icon icon-tabler icons-tabler-outline icon-tabler-server-bolt" > - + diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/server.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/server.svg index dc870301d8..7240a620a5 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/server.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/server.svg @@ -11,7 +11,7 @@ class="icon icon-tabler icons-tabler-outline icon-tabler-server" > - + diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/sparkles-2-off.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/sparkles-2-off.svg new file mode 100644 index 0000000000..93c12bba31 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/sparkles-2-off.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/sparkles-2.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/sparkles-2.svg index 6ea89c4d46..e01996a778 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/sparkles-2.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/sparkles-2.svg @@ -11,6 +11,6 @@ class="icon icon-tabler icons-tabler-outline icon-tabler-sparkles-2" > - + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/sport-billiard.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/sport-billiard.svg new file mode 100644 index 0000000000..772cb28625 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/sport-billiard.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tab-close.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tab-close.svg new file mode 100644 index 0000000000..6e740eb777 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tab-close.svg @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tabs.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tabs.svg new file mode 100644 index 0000000000..8bb9d323de --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tabs.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tags-chevron-down.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tags-chevron-down.svg new file mode 100644 index 0000000000..f163feaf7b --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tags-chevron-down.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tags-chevron-left.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tags-chevron-left.svg new file mode 100644 index 0000000000..0072ac37d4 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tags-chevron-left.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tags-chevron-right.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tags-chevron-right.svg new file mode 100644 index 0000000000..eda7f0a378 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tags-chevron-right.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tags-chevron-up.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tags-chevron-up.svg new file mode 100644 index 0000000000..a0cf1f47a2 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/tags-chevron-up.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-outline.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-outline.svg new file mode 100644 index 0000000000..54eb66cb70 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-outline.svg @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-asterisk.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-asterisk.svg new file mode 100644 index 0000000000..d29369e16f --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-asterisk.svg @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-end.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-end.svg new file mode 100644 index 0000000000..c27b319008 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-end.svg @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-plus.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-plus.svg new file mode 100644 index 0000000000..dca3f7fe57 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-plus.svg @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-question.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-question.svg new file mode 100644 index 0000000000..81d38a5b8e --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-question.svg @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-start.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-start.svg new file mode 100644 index 0000000000..9e2e8018a1 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/text-regex-start.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/thinking-high.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/thinking-high.svg new file mode 100644 index 0000000000..d1854e9421 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/thinking-high.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/thinking-low.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/thinking-low.svg new file mode 100644 index 0000000000..ad345f3921 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/thinking-low.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/thinking-medium.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/thinking-medium.svg new file mode 100644 index 0000000000..9a65729fbc --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/thinking-medium.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-bottom-left.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-bottom-left.svg index 6c87eea224..faf285a307 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-bottom-left.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-bottom-left.svg @@ -12,7 +12,7 @@ > - + diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-bottom-right.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-bottom-right.svg index c2f8be2e2b..9429225dbf 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-bottom-right.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-bottom-right.svg @@ -14,7 +14,7 @@ - + diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-top-left.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-top-left.svg index fd3fc68889..c39607de0f 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-top-left.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-top-left.svg @@ -11,7 +11,7 @@ class="icon icon-tabler icons-tabler-outline icon-tabler-transform-point-top-left" > - + diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-top-right.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-top-right.svg index d59e49a0a0..4a91dce14c 100644 --- a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-top-right.svg +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/transform-point-top-right.svg @@ -13,7 +13,7 @@ - + diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/treasure-chest.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/treasure-chest.svg new file mode 100644 index 0000000000..9429aa677c --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/treasure-chest.svg @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/twig.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/twig.svg new file mode 100644 index 0000000000..37f764f28e --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/twig.svg @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/underline-off.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/underline-off.svg new file mode 100644 index 0000000000..5a55f24208 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/underline-off.svg @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/vault.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/vault.svg new file mode 100644 index 0000000000..6b54635dbb --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/vault.svg @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/virtual-space.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/virtual-space.svg new file mode 100644 index 0000000000..58bf358283 --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/virtual-space.svg @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/yarn.svg b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/yarn.svg new file mode 100644 index 0000000000..e89e9f123c --- /dev/null +++ b/core/vendor/secondnetwork/blade-tabler-icons/resources/svg/yarn.svg @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/core/vendor/seld/phar-utils/SECURITY.md b/core/vendor/seld/phar-utils/SECURITY.md new file mode 100644 index 0000000000..16cd0f09e3 --- /dev/null +++ b/core/vendor/seld/phar-utils/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Supported Versions + +The latest minor version released is always the only one supported. + +## Reporting a Vulnerability + +Reach out to j.boggiano@seld.be privately if you find any vulnerability. diff --git a/core/vendor/seld/phar-utils/composer.lock b/core/vendor/seld/phar-utils/composer.lock index 21e33c7a21..27ae393676 100644 --- a/core/vendor/seld/phar-utils/composer.lock +++ b/core/vendor/seld/phar-utils/composer.lock @@ -1,10 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "hash": "e5afe72073d9266712c8e1ddc1648513", + "content-hash": "5f1a1708a2d4fde2d3eba93bfba32886", "packages": [], "packages-dev": [], "aliases": [], @@ -15,5 +15,6 @@ "platform": { "php": ">=5.3" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "2.6.0" } diff --git a/core/vendor/seld/phar-utils/src/Timestamps.php b/core/vendor/seld/phar-utils/src/Timestamps.php index 8077d5b851..80be9c0939 100644 --- a/core/vendor/seld/phar-utils/src/Timestamps.php +++ b/core/vendor/seld/phar-utils/src/Timestamps.php @@ -73,7 +73,7 @@ public function updateTimestamps($timestamp = null) $pos += 4; // update timestamp to a fixed value - $timeStampBytes = pack('L', $timestamp); + $timeStampBytes = pack('V', $timestamp); $this->contents[$pos + 0] = $timeStampBytes[0]; $this->contents[$pos + 1] = $timeStampBytes[1]; $this->contents[$pos + 2] = $timeStampBytes[2]; @@ -120,7 +120,7 @@ public function save($path, $signatureAlgo) // signature $signature = hash($algo, substr($this->contents, 0, $pos), true) // sig type - . pack('L', $signatureAlgo) + . pack('V', $signatureAlgo) // ohai Greg & Marcus . 'GBMB'; diff --git a/core/vendor/symfony/console/Descriptor/TextDescriptor.php b/core/vendor/symfony/console/Descriptor/TextDescriptor.php index 226d87aee0..b2e9df5643 100644 --- a/core/vendor/symfony/console/Descriptor/TextDescriptor.php +++ b/core/vendor/symfony/console/Descriptor/TextDescriptor.php @@ -37,7 +37,7 @@ protected function describeInputArgument(InputArgument $argument, array $options } $totalWidth = $options['total_width'] ?? Helper::width($argument->getName()); - $spacingWidth = $totalWidth - \strlen($argument->getName()); + $spacingWidth = $totalWidth - Helper::width($argument->getName()); $this->writeText(\sprintf(' %s %s%s%s', $argument->getName(), diff --git a/core/vendor/symfony/console/Helper/ProgressBar.php b/core/vendor/symfony/console/Helper/ProgressBar.php index c742e2b3b6..0ce4986439 100644 --- a/core/vendor/symfony/console/Helper/ProgressBar.php +++ b/core/vendor/symfony/console/Helper/ProgressBar.php @@ -571,7 +571,7 @@ private static function initPlaceholderFormatters(): array $display = str_repeat($bar->getBarCharacter(), $completeBars); if ($completeBars < $bar->getBarWidth()) { $emptyBars = $bar->getBarWidth() - $completeBars - Helper::length(Helper::removeDecoration($output->getFormatter(), $bar->getProgressCharacter())); - $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars); + $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), max(0, $emptyBars)); } return $display; diff --git a/core/vendor/symfony/console/Helper/Table.php b/core/vendor/symfony/console/Helper/Table.php index 8c3d0a521e..80d7ed6ee0 100644 --- a/core/vendor/symfony/console/Helper/Table.php +++ b/core/vendor/symfony/console/Helper/Table.php @@ -48,6 +48,7 @@ class Table private TableStyle $style; private array $columnStyles = []; private array $columnWidths = []; + private array $configuredColumnWidths = []; private array $columnMaxWidths = []; private bool $rendered = false; private string $displayOrientation = self::DISPLAY_ORIENTATION_DEFAULT; @@ -134,6 +135,7 @@ public function getColumnStyle(int $columnIndex): TableStyle public function setColumnWidth(int $columnIndex, int $width): static { $this->columnWidths[$columnIndex] = $width; + $this->configuredColumnWidths[$columnIndex] = $width; return $this; } @@ -146,6 +148,7 @@ public function setColumnWidth(int $columnIndex, int $width): static public function setColumnWidths(array $widths): static { $this->columnWidths = []; + $this->configuredColumnWidths = []; foreach ($widths as $index => $width) { $this->setColumnWidth($index, $width); } @@ -395,6 +398,8 @@ public function render(): void $isHeader = !$horizontal; $isFirstRow = $horizontal; $hasTitle = (bool) $this->headerTitle; + $previousRow = null; + $pendingSeparators = 0; foreach ($rowGroups as $rowGroup) { $isHeaderSeparatorRendered = false; @@ -408,7 +413,8 @@ public function render(): void } if ($row instanceof TableSeparator) { - $this->renderRowSeparator(); + // defer rendering until the row below is known, to align crossings with it + ++$pendingSeparators; continue; } @@ -417,11 +423,17 @@ public function render(): void continue; } + for (; $pendingSeparators > 0; --$pendingSeparators) { + $this->renderRowSeparator(self::SEPARATOR_MID, null, null, $previousRow, $row); + } + if ($isHeader && !$isHeaderSeparatorRendered && $this->style->displayOutsideBorder()) { $this->renderRowSeparator( self::SEPARATOR_TOP, $hasTitle ? $this->headerTitle : null, - $hasTitle ? $this->style->getHeaderTitleFormat() : null + $hasTitle ? $this->style->getHeaderTitleFormat() : null, + null, + $row ); $hasTitle = false; $isHeaderSeparatorRendered = true; @@ -429,9 +441,11 @@ public function render(): void if ($isFirstRow) { $this->renderRowSeparator( - $horizontal ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM, + $horizontal || !array_filter($this->headers) ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM, $hasTitle ? $this->headerTitle : null, - $hasTitle ? $this->style->getHeaderTitleFormat() : null + $hasTitle ? $this->style->getHeaderTitleFormat() : null, + $previousRow, + $row ); $isFirstRow = false; $hasTitle = false; @@ -447,11 +461,17 @@ public function render(): void } else { $this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat()); } + + $previousRow = $row; } } + for (; $pendingSeparators > 0; --$pendingSeparators) { + $this->renderRowSeparator(self::SEPARATOR_MID, null, null, $previousRow); + } + if ($this->getStyle()->displayOutsideBorder()) { - $this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat()); + $this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat(), $previousRow); } $this->cleanup(); @@ -465,7 +485,7 @@ public function render(): void * * +-----+-----------+-------+ */ - private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null): void + private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null, ?array $rowAbove = null, ?array $rowBelow = null): void { if (!$count = $this->numberOfColumns) { return; @@ -487,10 +507,33 @@ private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $ti [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[7], $crossings[6], $crossings[5]]; } + // only draw a crossing where a vertical border meets the separator; a cell + // spanning several columns has no border on the columns it covers + $hasLineAbove = self::SEPARATOR_TOP !== $type; + $hasLineBelow = self::SEPARATOR_BOTTOM !== $type; + $columnsAbove = null !== $rowAbove ? $this->getRowColumns($rowAbove) : null; + $columnsBelow = null !== $rowBelow ? $this->getRowColumns($rowBelow) : null; + $markup = $leftChar; for ($column = 0; $column < $count; ++$column) { $markup .= str_repeat($horizontal, $this->effectiveColumnWidths[$column]); - $markup .= $column === $count - 1 ? $rightChar : $midChar; + if ($column === $count - 1) { + $markup .= $rightChar; + + continue; + } + + $borderAbove = $hasLineAbove && (null === $columnsAbove || \in_array($column + 1, $columnsAbove, true)); + $borderBelow = $hasLineBelow && (null === $columnsBelow || \in_array($column + 1, $columnsBelow, true)); + // the one-sided junctions reuse the top/bottom crossing chars; for the "box-double" + // style these are double-horizontal (╤/╧) while an inner separator is single (─), as + // that style has no single-line ┬/┴ to fall back to + $markup .= match (true) { + $borderAbove && $borderBelow => $midChar, + $borderBelow => $crossings[2], + $borderAbove => $crossings[6], + default => $horizontal, + }; } if (null !== $title) { @@ -534,7 +577,7 @@ private function renderRow(array $row, string $cellFormat, ?string $firstCellFor { $rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE); $columns = $this->getRowColumns($row); - $last = \count($columns) - 1; + $last = array_key_last($columns); foreach ($columns as $i => $column) { if ($firstCellFormat && 0 === $i) { $rowContent .= $this->renderCell($row, $column, $firstCellFormat); @@ -660,13 +703,18 @@ private function buildTableRows(array $rows): TableRows $this->columnWidths[$item['column']] = $minWidthColumn; $columnsMinWidthProcessed[$item['column']] = true; $cellWidth -= $minWidthColumn + $lengthColumnBorder; + } elseif ('min' === $item['type'] && ($this->configuredColumnWidths[$item['column']] ?? 0) > 0) { + // this column already covers part of the cell, so share only the rest + $columnsMinWidthProcessed[$item['column']] = true; + $cellWidth -= $this->configuredColumnWidths[$item['column']] + $lengthColumnBorder; } } for ($i = $column; $i < ($column + $colspan); ++$i) { if (isset($columnsMinWidthProcessed[$i])) { continue; } - $this->columnWidths[$i] = $cellWidth + $lengthColumnBorder; + // a width set through setColumnWidth() is a minimum, unlike one computed for an earlier row + $this->columnWidths[$i] = max($this->configuredColumnWidths[$i] ?? 0, $cellWidth + $lengthColumnBorder); } } if (!str_contains($cell ?? '', "\n")) { diff --git a/core/vendor/symfony/console/Helper/TerminalInputHelper.php b/core/vendor/symfony/console/Helper/TerminalInputHelper.php index d6f07db8b3..d0bc120ae9 100644 --- a/core/vendor/symfony/console/Helper/TerminalInputHelper.php +++ b/core/vendor/symfony/console/Helper/TerminalInputHelper.php @@ -59,7 +59,7 @@ public function __construct($inputStream, bool $withStty = true) throw new \RuntimeException('Unable to read the terminal settings.'); } - $this->initialState = $state; + $this->initialState = trim($state); $this->createSignalHandlers(); } @@ -96,7 +96,13 @@ public function finish(): void // Safeguard in case an unhandled kill signal exists $this->checkForKillSignal(); - shell_exec('stty '.$this->initialState); + + // The captured "stty -g" state is not guaranteed to be accepted back by "stty" on every + // platform/terminal (e.g. some nested pty implementations reject it with "invalid argument"), + // and shell_exec() gives no way to detect that failure. Try the exact restore first so + // any custom terminal settings survive, but always fall back to "stty sane" so the + // terminal is left in a usable state even when the exact restore silently fails. + shell_exec('stty '.$this->initialState.' 2>/dev/null || stty sane'); $this->signalToKill = 0; foreach ($this->signalHandlers as $signal => $originalHandler) { @@ -119,9 +125,12 @@ private function createSignalHandlers(): void $this->signalHandlers[$signal] = pcntl_signal_get_handler($signal); pcntl_signal($signal, function ($signal) { - // Save current state, then restore to initial state + // Save current state, then restore to initial state. The original signal + // handler is about to run, so fall back to "stty sane" if the exact restore + // is rejected by "stty" (see the same fallback in finish()), to make sure it + // runs with a usable terminal. $currentState = shell_exec('stty -g'); - shell_exec('stty '.$this->initialState); + shell_exec('stty '.$this->initialState.' 2>/dev/null || stty sane'); $originalHandler = $this->signalHandlers[$signal]; if (\is_callable($originalHandler)) { diff --git a/core/vendor/symfony/console/Output/ConsoleOutput.php b/core/vendor/symfony/console/Output/ConsoleOutput.php index 0aab633c02..675181b29b 100644 --- a/core/vendor/symfony/console/Output/ConsoleOutput.php +++ b/core/vendor/symfony/console/Output/ConsoleOutput.php @@ -41,19 +41,9 @@ public function __construct(int $verbosity = self::VERBOSITY_NORMAL, ?bool $deco { parent::__construct($this->openOutputStream(), $verbosity, $decorated, $formatter); - if (null === $formatter) { - // for BC reasons, stdErr has it own Formatter only when user don't inject a specific formatter. - $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated); - - return; - } - - $actualDecorated = $this->isDecorated(); - $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $this->getFormatter()); - - if (null === $decorated) { - $this->setDecorated($actualDecorated && $this->stderr->isDecorated()); - } + // stderr gets its own formatter: decoration is held by the formatter, so a shared one + // would force both streams to the same state instead of detecting each independently + $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $formatter ? clone $formatter : null); } /** diff --git a/core/vendor/symfony/console/Style/SymfonyStyle.php b/core/vendor/symfony/console/Style/SymfonyStyle.php index 519d71a5c7..a6b67242de 100644 --- a/core/vendor/symfony/console/Style/SymfonyStyle.php +++ b/core/vendor/symfony/console/Style/SymfonyStyle.php @@ -310,10 +310,15 @@ public function askQuestion(Question $question): mixed if ($this->output instanceof ConsoleSectionOutput) { // add the new line of the `return` to submit the input to ConsoleSectionOutput, because ConsoleSectionOutput is holding all it's lines. // this is relevant when a `ConsoleSectionOutput::clear` is called. + // the section already renders the prompt as a whole line, so an extra + // newLine() here would leave a doubled blank line below the answer; only + // keep the buffer in sync so autoPrependBlock() spaces the next block. $this->output->addNewLineOfInputSubmit(); + $this->bufferedOutput->write("\n\n"); + } else { + $this->newLine(); + $this->bufferedOutput->write("\n"); } - $this->newLine(); - $this->bufferedOutput->write("\n"); } return $answer; diff --git a/core/vendor/symfony/error-handler/Error/FatalError.php b/core/vendor/symfony/error-handler/Error/FatalError.php index b80e3fa363..85bd0a95ab 100644 --- a/core/vendor/symfony/error-handler/Error/FatalError.php +++ b/core/vendor/symfony/error-handler/Error/FatalError.php @@ -28,8 +28,9 @@ public function __construct( if (null !== $trace) { if (!$traceArgs) { - foreach ($trace as &$frame) { - unset($frame['args'], $frame['this'], $frame); + foreach ($trace as $index => $frame) { + unset($frame['args'], $frame['this']); + $trace[$index] = $frame; } } } elseif (null !== $traceOffset) { diff --git a/core/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php b/core/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php index 0d78228b44..bc851c7803 100644 --- a/core/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php +++ b/core/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php @@ -152,13 +152,20 @@ public function getCalledListeners(?Request $request = null): array $hash = $request ? spl_object_hash($request) : null; $called = []; - foreach ($this->calledListenerInfos as $requestHash => $infos) { - if (null === $hash || $hash === $requestHash) { - $called[] = $infos; + foreach ($this->calledListenerInfos as $requestHash => $eventInfos) { + if (null !== $hash && $hash !== $requestHash) { + continue; + } + foreach ($eventInfos as $infos) { + foreach ($infos as ['info' => $info, 'count' => $count]) { + while ($count--) { + $called[] = $info; + } + } } } - return $called ? array_merge(...$called) : []; + return $called; } public function getNotCalledListeners(?Request $request = null): array @@ -217,6 +224,7 @@ public function getOrphanedEvents(?Request $request = null): array public function reset(): void { $this->callStack = null; + $this->wrappedListeners = []; $this->orphanedEvents = []; $this->currentRequestHash = ''; $this->dispatchDepth = []; @@ -295,11 +303,7 @@ private function postProcess(string $eventName): void if ($listener->wasCalled()) { $this->logger?->debug('Notified event "{event}" to listener "{listener}".', $context); - $original = $listener->getWrappedListener(); - if (!\in_array($original, $this->calledOriginalListeners[$this->currentRequestHash][$eventName] ?? [], true)) { - $this->calledOriginalListeners[$this->currentRequestHash][$eventName][] = $original; - $this->calledListenerInfos[$this->currentRequestHash][] = $listener->getInfo($eventName); - } + $this->collectCalledListener($listener, $eventName); } unset($this->callStack[$listener]); @@ -328,16 +332,28 @@ private function postProcess(string $eventName): void } foreach ($stale as $listener) { if ($listener->wasCalled()) { - $original = $listener->getWrappedListener(); - if (!\in_array($original, $this->calledOriginalListeners[$this->currentRequestHash][$eventName] ?? [], true)) { - $this->calledOriginalListeners[$this->currentRequestHash][$eventName][] = $original; - $this->calledListenerInfos[$this->currentRequestHash][] = $listener->getInfo($eventName); - } + $this->collectCalledListener($listener, $eventName); } unset($this->callStack[$listener]); } } + /** + * Counts an invocation without storing per-invocation state, so that the + * aggregate cannot grow when the same listeners are called repeatedly. + */ + private function collectCalledListener(WrappedListener $listener, string $eventName): void + { + $original = $listener->getWrappedListener(); + + if (false === $i = array_search($original, $this->calledOriginalListeners[$this->currentRequestHash][$eventName] ?? [], true)) { + $this->calledOriginalListeners[$this->currentRequestHash][$eventName][] = $original; + $this->calledListenerInfos[$this->currentRequestHash][$eventName][] = ['info' => $listener->getInfo($eventName), 'count' => 1]; + } else { + ++$this->calledListenerInfos[$this->currentRequestHash][$eventName][$i]['count']; + } + } + private function sortNotCalledListeners(array $a, array $b): int { if (0 !== $cmp = strcmp($a['event'], $b['event'])) { diff --git a/core/vendor/symfony/filesystem/Filesystem.php b/core/vendor/symfony/filesystem/Filesystem.php index 1600fad184..4a822691f2 100644 --- a/core/vendor/symfony/filesystem/Filesystem.php +++ b/core/vendor/symfony/filesystem/Filesystem.php @@ -605,8 +605,10 @@ public function tempnam(string $dir, string $prefix, string $suffix = ''): strin // If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem if ((null === $scheme || 'file' === $scheme || 'gs' === $scheme) && '' === $suffix) { + // PHP's tempnam() truncates the prefix to 63 characters; trim trailing whitespace + // from the truncated value, as a trailing space makes the file creation fail on Windows // If tempnam failed or no scheme return the filename otherwise prepend the scheme - if ($tmpFile = self::box('tempnam', $hierarchy, $prefix)) { + if ($tmpFile = self::box('tempnam', $hierarchy, rtrim(substr($prefix, 0, 63)))) { if (null !== $scheme && 'gs' !== $scheme) { return $scheme.'://'.$tmpFile; } diff --git a/core/vendor/symfony/http-foundation/Cookie.php b/core/vendor/symfony/http-foundation/Cookie.php index 199287057e..10127176b3 100644 --- a/core/vendor/symfony/http-foundation/Cookie.php +++ b/core/vendor/symfony/http-foundation/Cookie.php @@ -32,6 +32,9 @@ class Cookie private const RESERVED_CHARS_FROM = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"]; private const RESERVED_CHARS_TO = ['%3D', '%2C', '%3B', '%20', '%09', '%0D', '%0A', '%0B', '%0C']; + // same list as above minus "=", which PHP allows in the path and domain attributes + private const RESERVED_ATTR_CHARS_LIST = ",; \t\r\n\v\f"; + /** * Creates cookie from raw header string. */ @@ -108,6 +111,9 @@ public function __construct( throw new \InvalidArgumentException('The cookie name cannot be empty.'); } + self::validateAttribute('path', $path); + self::validateAttribute('domain', $domain); + $this->expire = self::expiresTimestamp($expire); $this->path = $path ?: '/'; $this->sameSite = $this->withSameSite($sameSite)->sameSite; @@ -129,6 +135,8 @@ public function withValue(?string $value): static */ public function withDomain(?string $domain): static { + self::validateAttribute('domain', $domain); + $cookie = clone $this; $cookie->domain = $domain; @@ -165,11 +173,23 @@ private static function expiresTimestamp(int|string|\DateTimeInterface $expire = return 0 < $expire ? (int) $expire : 0; } + /** + * Rejects the characters that PHP's setcookie() also rejects in the path and domain attributes. + */ + private static function validateAttribute(string $attribute, ?string $value): void + { + if (null !== $value && false !== strpbrk($value, self::RESERVED_ATTR_CHARS_LIST)) { + throw new \InvalidArgumentException(\sprintf('The cookie %s "%s" contains invalid characters.', $attribute, $value)); + } + } + /** * Creates a cookie copy with a new path on the server in which the cookie will be available on. */ public function withPath(string $path): static { + self::validateAttribute('path', $path); + $cookie = clone $this; $cookie->path = '' === $path ? '/' : $path; diff --git a/core/vendor/symfony/http-foundation/EventStreamResponse.php b/core/vendor/symfony/http-foundation/EventStreamResponse.php index e739c309c8..f9ee305ac3 100644 --- a/core/vendor/symfony/http-foundation/EventStreamResponse.php +++ b/core/vendor/symfony/http-foundation/EventStreamResponse.php @@ -36,6 +36,8 @@ */ class EventStreamResponse extends StreamedResponse { + private ?int $lastRetry = null; + /** * @param int|null $retry The number of milliseconds the client should wait * before reconnecting in case of network failure @@ -82,8 +84,10 @@ public function setCallback(callable $callback): static */ public function sendEvent(ServerEvent $event): static { - if ($this->retry > 0 && !$event->getRetry()) { - $event->setRetry($this->retry); + if (($retry = $event->getRetry()) > 0) { + $this->lastRetry = $retry; + } elseif ($this->retry > 0 && $this->retry !== $this->lastRetry) { + $event->setRetry($this->lastRetry = $this->retry); } foreach ($event as $part) { diff --git a/core/vendor/symfony/http-foundation/IpUtils.php b/core/vendor/symfony/http-foundation/IpUtils.php index 35cd3bcaf7..533f789149 100644 --- a/core/vendor/symfony/http-foundation/IpUtils.php +++ b/core/vendor/symfony/http-foundation/IpUtils.php @@ -22,8 +22,12 @@ class IpUtils '127.0.0.0/8', // RFC1700 (Loopback) '10.0.0.0/8', // RFC1918 '192.168.0.0/16', // RFC1918 + '192.0.2.0/24', // Documentation Ranges TEST-NET-1 (RFC 5737) + '198.51.100.0/24',// Documentation Ranges TEST-NET-2 (RFC 5737) + '203.0.113.0/24', // Documentation Ranges TEST-NET-3 (RFC 5737) '172.16.0.0/12', // RFC1918 '169.254.0.0/16', // RFC3927 + '198.18.0.0/15', // IPv4 Benchmarking (RFC 2544) '0.0.0.0/8', // RFC5735 '240.0.0.0/4', // RFC1112 '100.64.0.0/10', // RFC6598 @@ -35,6 +39,8 @@ class IpUtils '::/96', // IPv4-compatible IPv6 addresses (RFC 4291 section 2.5.5.1) '2002::/16', // 6to4 (RFC 3056) '2001::/32', // Teredo tunneling (RFC 4380) + '2001:db8::/32', // Documentation Ranges (RFC 3849) + '2001:0002::/48', // IPv6 Benchmarking (RFC 5180 and corrections) '64:ff9b::/96', // NAT64 well-known prefix (RFC 6052) '64:ff9b:1::/48', // NAT64 local-use prefix (RFC 8215) ]; @@ -247,9 +253,15 @@ public static function anonymize(string $ip/* , int $v4Bytes = 1, int $v6Bytes = /** * Checks if an IPv4 or IPv6 address is contained in the list of private IP subnets. + * + * @throws \ValueError When $requestIp is not a valid IP address */ public static function isPrivateIp(string $requestIp): bool { + if (!filter_var($requestIp, \FILTER_VALIDATE_IP)) { + throw new \ValueError(\sprintf('"%s" is not a valid IP address.', $requestIp)); + } + return self::checkIp($requestIp, self::PRIVATE_SUBNETS); } diff --git a/core/vendor/symfony/http-foundation/ServerEvent.php b/core/vendor/symfony/http-foundation/ServerEvent.php index 7597058bd1..d79e7dc7e3 100644 --- a/core/vendor/symfony/http-foundation/ServerEvent.php +++ b/core/vendor/symfony/http-foundation/ServerEvent.php @@ -115,31 +115,46 @@ public function setComment(string $comment): static */ public function getIterator(): \Traversable { - static $lastRetry = null; - $head = ''; if ($this->comment) { - $head .= \sprintf(': %s', $this->comment)."\n"; + $head .= self::field(': ', $this->comment); } if ($this->id) { - $head .= \sprintf('id: %s', $this->id)."\n"; + $head .= \sprintf('id: %s', self::singleLine($this->id))."\n"; } - if ($this->retry > 0 && $this->retry !== $lastRetry) { - $head .= \sprintf('retry: %s', $lastRetry = $this->retry)."\n"; + if ($this->retry > 0) { + $head .= \sprintf('retry: %s', $this->retry)."\n"; } if ($this->type) { - $head .= \sprintf('event: %s', $this->type)."\n"; + $head .= \sprintf('event: %s', self::singleLine($this->type))."\n"; } yield $head; if (is_iterable($this->data)) { foreach ($this->data as $data) { - yield \sprintf('data: %s', $data)."\n"; + yield self::field('data: ', $data); } } elseif ('' !== $this->data) { - yield \sprintf('data: %s', $this->data)."\n"; + yield self::field('data: ', $this->data); } yield "\n"; } + + /** + * Renders a multi-line value as one prefixed line per line of the value. + */ + private static function field(string $prefix, string $value): string + { + return $prefix.implode("\n".$prefix, preg_split("/\r\n|[\r\n]/", $value))."\n"; + } + + /** + * Removes the line terminators that would end the field early, as the + * SSE specification defines these fields as single-line only. + */ + private static function singleLine(string $value): string + { + return str_replace(["\r", "\n"], '', $value); + } } diff --git a/core/vendor/symfony/http-foundation/UriSigner.php b/core/vendor/symfony/http-foundation/UriSigner.php index 690021b5bb..d31bafb9ce 100644 --- a/core/vendor/symfony/http-foundation/UriSigner.php +++ b/core/vendor/symfony/http-foundation/UriSigner.php @@ -182,18 +182,17 @@ private function doVerify(string $uri): int parse_str($url['query'], $params); } - if (empty($params[$this->hashParameter])) { + if (!\is_string($hash = $params[$this->hashParameter] ?? null) || '' === $hash) { return self::STATUS_MISSING; } - $hash = $params[$this->hashParameter]; unset($params[$this->hashParameter]); if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), strtr(rtrim($hash, '='), ['/' => '_', '+' => '-']))) { return self::STATUS_INVALID; } - if (!$expiration = $params[$this->expirationParameter] ?? false) { + if (null === $expiration = $params[$this->expirationParameter] ?? null) { return self::STATUS_VALID; } diff --git a/core/vendor/symfony/http-foundation/UrlHelper.php b/core/vendor/symfony/http-foundation/UrlHelper.php index f971cf6629..22fb9a1a1b 100644 --- a/core/vendor/symfony/http-foundation/UrlHelper.php +++ b/core/vendor/symfony/http-foundation/UrlHelper.php @@ -29,7 +29,7 @@ public function __construct( public function getAbsoluteUrl(string $path): string { - if (str_contains($path, '://') || str_starts_with($path, '//')) { + if (str_contains($path, '://') || str_starts_with($path, '//') || preg_match('/^[a-z][a-z0-9+.-]*:/i', $path)) { return $path; } diff --git a/core/vendor/symfony/http-kernel/DependencyInjection/ServicesResetter.php b/core/vendor/symfony/http-kernel/DependencyInjection/ServicesResetter.php index 57e394fcc5..b79ccd6941 100644 --- a/core/vendor/symfony/http-kernel/DependencyInjection/ServicesResetter.php +++ b/core/vendor/symfony/http-kernel/DependencyInjection/ServicesResetter.php @@ -36,6 +36,8 @@ public function __construct( public function reset(): void { + $throwable = null; + foreach ($this->resettableServices as $id => $service) { if ($service instanceof LazyObjectInterface && !$service->isLazyObjectInitialized(true)) { continue; @@ -54,8 +56,17 @@ public function reset(): void continue; } - $service->$resetMethod(); + try { + $service->$resetMethod(); + } catch (\Throwable $e) { + // failing to reset one service should not prevent resetting the remaining ones + $throwable ??= $e; + } } } + + if (null !== $throwable) { + throw $throwable; + } } } diff --git a/core/vendor/symfony/http-kernel/Kernel.php b/core/vendor/symfony/http-kernel/Kernel.php index c6e415e525..0c566611f5 100644 --- a/core/vendor/symfony/http-kernel/Kernel.php +++ b/core/vendor/symfony/http-kernel/Kernel.php @@ -74,11 +74,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '7.4.14'; - public const VERSION_ID = 70414; + public const VERSION = '7.4.16'; + public const VERSION_ID = 70416; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 14; + public const RELEASE_VERSION = 16; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '11/2028'; diff --git a/core/vendor/symfony/http-kernel/composer.json b/core/vendor/symfony/http-kernel/composer.json index 466e49d233..3b810e8c40 100644 --- a/core/vendor/symfony/http-kernel/composer.json +++ b/core/vendor/symfony/http-kernel/composer.json @@ -47,7 +47,7 @@ "symfony/var-dumper": "^6.4|^7.0|^8.0", "symfony/var-exporter": "^6.4|^7.0|^8.0", "psr/cache": "^1.0|^2.0|^3.0", - "twig/twig": "^3.12" + "twig/twig": "^3.12|^4.0" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" diff --git a/core/vendor/symfony/mime/Address.php b/core/vendor/symfony/mime/Address.php index 1f97306e45..f9b279d774 100644 --- a/core/vendor/symfony/mime/Address.php +++ b/core/vendor/symfony/mime/Address.php @@ -87,7 +87,7 @@ public function getEncodedName(): string return ''; } - return \sprintf('"%s"', preg_replace('/"/u', '\"', $this->getName())); + return \sprintf('"%s"', preg_replace('/["\\\\]/', '\\\\$0', $this->getName())); } public static function create(self|string $address): self diff --git a/core/vendor/symfony/mime/Email.php b/core/vendor/symfony/mime/Email.php index b6b8d39e95..d05115eb51 100644 --- a/core/vendor/symfony/mime/Email.php +++ b/core/vendor/symfony/mime/Email.php @@ -482,6 +482,7 @@ private function prepareParts(): ?array } $otherParts = $relatedParts = []; + $cidReplacements = []; foreach ($this->attachments as $part) { foreach ($names as $name) { if ($name !== $part->getName() && (!$part->hasContentId() || $name !== $part->getContentId())) { @@ -491,9 +492,7 @@ private function prepareParts(): ?array continue 2; } - if ($name !== $part->getContentId()) { - $html = str_replace('cid:'.$name, 'cid:'.$part->getContentId(), $html); - } + $cidReplacements['cid:'.$name] = 'cid:'.$part->getContentId(); $relatedParts[$name] = $part; $part->setName($part->getName() ?? $part->getContentId())->asInline(); @@ -502,6 +501,11 @@ private function prepareParts(): ?array $otherParts[] = $part; } + if ($cidReplacements) { + // all references are replaced at once as strtr() matches the longest name first and + // never replaces inside already substituted text, unlike successive str_replace() calls + $html = strtr($html, $cidReplacements); + } if (null !== $htmlPart) { $htmlPart = new TextPart($html, $this->htmlCharset, 'html'); } diff --git a/core/vendor/symfony/mime/Header/AbstractHeader.php b/core/vendor/symfony/mime/Header/AbstractHeader.php index 556c0aa0b4..62b302f5b4 100644 --- a/core/vendor/symfony/mime/Header/AbstractHeader.php +++ b/core/vendor/symfony/mime/Header/AbstractHeader.php @@ -180,17 +180,18 @@ protected function getEncodableWordTokens(string $string): array $tokens[] = $encodedToken; } - foreach ($tokens as $i => $token) { - // whitespace(s) between 2 encoded tokens - if ( - 0 < $i - && isset($tokens[$i + 1]) - && preg_match('~^[\t ]+$~', $token) - && $this->tokenNeedsEncoding($tokens[$i - 1]) - && $this->tokenNeedsEncoding($tokens[$i + 1]) - ) { - $tokens[$i - 1] .= $token.$tokens[$i + 1]; - array_splice($tokens, $i, 2); + $i = 1; + while (isset($tokens[$i + 1])) { + // whitespace-only token(s) between 2 encoded tokens; a gap of N spaces yields N - 1 of them + $j = $i; + while (preg_match('~^[\t ]+$~', $tokens[$j] ?? '')) { + ++$j; + } + if ($j > $i && isset($tokens[$j]) && $this->tokenNeedsEncoding($tokens[$i - 1]) && $this->tokenNeedsEncoding($tokens[$j])) { + $tokens[$i - 1] .= implode('', \array_slice($tokens, $i, 1 + $j - $i)); + array_splice($tokens, $i, 1 + $j - $i); + } else { + ++$i; } } diff --git a/core/vendor/symfony/polyfill-intl-grapheme/Grapheme.php b/core/vendor/symfony/polyfill-intl-grapheme/Grapheme.php index ad9366a8b4..cad3b8387c 100644 --- a/core/vendor/symfony/polyfill-intl-grapheme/Grapheme.php +++ b/core/vendor/symfony/polyfill-intl-grapheme/Grapheme.php @@ -45,6 +45,9 @@ final class Grapheme ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], ]; + // indexed by the $mode argument of grapheme_position() + private const POSITION_FUNCTIONS = ['grapheme_strpos', 'grapheme_stripos', 'grapheme_strrpos', 'grapheme_strripos']; + public static function grapheme_extract($s, $size, $type = \GRAPHEME_EXTR_COUNT, $start = 0, &$next = 0) { if (0 > $start) { @@ -197,10 +200,6 @@ public static function grapheme_strstr($s, $needle, $beforeNeedle = false) public static function grapheme_str_split($s, $len = 1) { if (0 > $len || 1073741823 < $len) { - if (80000 > \PHP_VERSION_ID) { - return false; - } - throw new \ValueError('grapheme_str_split(): Argument #2 ($length) must be greater than 0 and less than or equal to 1073741823.'); } @@ -232,10 +231,6 @@ public static function grapheme_levenshtein($s1, $s2, $insertion_cost = 1, $repl } if (0 > $insertion_cost || 0 > $replacement_cost || 0 > $deletion_cost) { - if (80000 > \PHP_VERSION_ID) { - return false; - } - throw new \ValueError('grapheme_levenshtein(): Argument #3 ($insertion_cost), #4 ($replacement_cost), and #5 ($deletion_cost) must be greater than or equal to 0'); } @@ -280,14 +275,25 @@ private static function grapheme_position($s, $needle, $offset, $mode) return false; } $s = (string) $s; - if (!preg_match('/./us', $s)) { + // let the empty string through: it accepts no offset but 0, which is checked below + if ('' !== $s && !preg_match('/./us', $s)) { + return false; + } + if ($offset && ($offset > ($len = self::grapheme_strlen($s)) || $offset < -$len)) { + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError(self::POSITION_FUNCTIONS[$mode].'(): Argument #3 ($offset) must be contained in argument #1 ($haystack)'); + } + if ('' === $s) { return false; } if ($offset > 0) { $s = self::grapheme_substr($s, $offset); } elseif ($offset < 0) { if (2 > $mode) { - $offset += self::grapheme_strlen($s); + $offset += $len; $s = self::grapheme_substr($s, $offset); if (0 > $offset) { $offset = 0; diff --git a/core/vendor/symfony/polyfill-intl-grapheme/bootstrap.php b/core/vendor/symfony/polyfill-intl-grapheme/bootstrap.php index 789147e48e..4fd8774194 100644 --- a/core/vendor/symfony/polyfill-intl-grapheme/bootstrap.php +++ b/core/vendor/symfony/polyfill-intl-grapheme/bootstrap.php @@ -15,6 +15,12 @@ return require __DIR__.'/bootstrap80.php'; } +if (!class_exists('ValueError', false)) { + class ValueError extends Error + { + } +} + if (!defined('GRAPHEME_EXTR_COUNT')) { define('GRAPHEME_EXTR_COUNT', 0); } diff --git a/core/vendor/symfony/polyfill-php85/bootstrap.php b/core/vendor/symfony/polyfill-php85/bootstrap.php index 00c823fa0b..c68b01b30b 100644 --- a/core/vendor/symfony/polyfill-php85/bootstrap.php +++ b/core/vendor/symfony/polyfill-php85/bootstrap.php @@ -41,6 +41,12 @@ function locale_is_right_to_left(string $locale): bool { return p\Php85::locale_ return; } +if (!class_exists('ValueError', false)) { + class ValueError extends Error + { + } +} + if (extension_loaded('intl') && !function_exists('grapheme_levenshtein')) { function grapheme_levenshtein(string $string1, string $string2, int $insertion_cost = 1, int $replacement_cost = 1, int $deletion_cost = 1, string $locale = '') { return p\Php85::grapheme_levenshtein($string1, $string2, $insertion_cost, $replacement_cost, $deletion_cost); } } diff --git a/core/vendor/symfony/routing/Generator/UrlGenerator.php b/core/vendor/symfony/routing/Generator/UrlGenerator.php index 5ee1058f2f..2809e12bf5 100644 --- a/core/vendor/symfony/routing/Generator/UrlGenerator.php +++ b/core/vendor/symfony/routing/Generator/UrlGenerator.php @@ -280,10 +280,16 @@ protected function doGenerate(array $variables, array $defaults, array $requirem $extra = array_udiff_assoc(array_diff_key($parameters, $variables), $defaults, static fn ($a, $b) => $a == $b ? 0 : 1); $extra = array_replace($extra, $queryParameters); - array_walk_recursive($extra, $caster = static function (&$v) use (&$caster) { + $seen = []; + array_walk_recursive($extra, $caster = static function (&$v) use (&$caster, &$seen, $name) { if (\is_object($v)) { + if (isset($seen[$id = spl_object_id($v)])) { + throw new InvalidParameterException(\sprintf('Parameters for route "%s" cannot contain a circular reference (in object of class "%s").', $name, get_debug_type($v))); + } if ($vars = get_object_vars($v)) { + $seen[$id] = true; array_walk_recursive($vars, $caster); + unset($seen[$id]); $v = $vars; } elseif ($v instanceof \Stringable) { $v = (string) $v; diff --git a/core/vendor/symfony/string/ByteString.php b/core/vendor/symfony/string/ByteString.php index 5cbfd6de44..bd721fc492 100644 --- a/core/vendor/symfony/string/ByteString.php +++ b/core/vendor/symfony/string/ByteString.php @@ -183,7 +183,11 @@ public function indexOf(string|iterable|AbstractString $needle, int $offset = 0) return null; } - $i = $this->ignoreCase ? stripos($this->string, $needle, $offset) : strpos($this->string, $needle, $offset); + try { + $i = $this->ignoreCase ? stripos($this->string, $needle, $offset) : strpos($this->string, $needle, $offset); + } catch (\ValueError) { + return null; + } return false === $i ? null : $i; } @@ -200,7 +204,11 @@ public function indexOfLast(string|iterable|AbstractString $needle, int $offset return null; } - $i = $this->ignoreCase ? strripos($this->string, $needle, $offset) : strrpos($this->string, $needle, $offset); + try { + $i = $this->ignoreCase ? strripos($this->string, $needle, $offset) : strrpos($this->string, $needle, $offset); + } catch (\ValueError) { + return null; + } return false === $i ? null : $i; } diff --git a/core/vendor/symfony/string/CodePointString.php b/core/vendor/symfony/string/CodePointString.php index 337bfc12ab..f699168e4d 100644 --- a/core/vendor/symfony/string/CodePointString.php +++ b/core/vendor/symfony/string/CodePointString.php @@ -126,7 +126,11 @@ public function indexOf(string|iterable|AbstractString $needle, int $offset = 0) return null; } - $i = $this->ignoreCase ? mb_stripos($this->string, $needle, $offset, 'UTF-8') : mb_strpos($this->string, $needle, $offset, 'UTF-8'); + try { + $i = $this->ignoreCase ? mb_stripos($this->string, $needle, $offset, 'UTF-8') : mb_strpos($this->string, $needle, $offset, 'UTF-8'); + } catch (\ValueError) { + return null; + } return false === $i ? null : $i; } @@ -143,7 +147,11 @@ public function indexOfLast(string|iterable|AbstractString $needle, int $offset return null; } - $i = $this->ignoreCase ? mb_strripos($this->string, $needle, $offset, 'UTF-8') : mb_strrpos($this->string, $needle, $offset, 'UTF-8'); + try { + $i = $this->ignoreCase ? mb_strripos($this->string, $needle, $offset, 'UTF-8') : mb_strrpos($this->string, $needle, $offset, 'UTF-8'); + } catch (\ValueError) { + return null; + } return false === $i ? null : $i; } @@ -202,7 +210,7 @@ public function splice(string $replacement, int $start = 0, ?int $length = null) $str = clone $this; $start = $start ? \strlen(mb_substr($this->string, 0, $start, 'UTF-8')) : 0; - $length = $length ? \strlen(mb_substr($this->string, $start, $length, 'UTF-8')) : $length; + $length = $length ? \strlen(mb_substr(substr($this->string, $start), 0, $length, 'UTF-8')) : $length; $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX); return $str; diff --git a/core/vendor/symfony/string/Slugger/AsciiSlugger.php b/core/vendor/symfony/string/Slugger/AsciiSlugger.php index f5967e47cd..689a404788 100644 --- a/core/vendor/symfony/string/Slugger/AsciiSlugger.php +++ b/core/vendor/symfony/string/Slugger/AsciiSlugger.php @@ -81,7 +81,7 @@ public function setLocale(string $locale): void public function getLocale(): string { - return $this->defaultLocale; + return $this->defaultLocale ?? ''; } /** diff --git a/core/vendor/symfony/string/UnicodeString.php b/core/vendor/symfony/string/UnicodeString.php index ca9e0d8965..ca3499ffea 100644 --- a/core/vendor/symfony/string/UnicodeString.php +++ b/core/vendor/symfony/string/UnicodeString.php @@ -184,7 +184,11 @@ public function indexOfLast(string|iterable|AbstractString $needle, int $offset $offset = 0; } - $i = $this->ignoreCase ? grapheme_strripos($string, $needle, $offset) : grapheme_strrpos($string, $needle, $offset); + try { + $i = $this->ignoreCase ? grapheme_strripos($string, $needle, $offset) : grapheme_strrpos($string, $needle, $offset); + } catch (\ValueError) { + return null; + } return false === $i ? null : $i; } @@ -290,7 +294,7 @@ public function splice(string $replacement, int $start = 0, ?int $length = null) $str = clone $this; $start = $start ? \strlen(grapheme_substr($this->string, 0, $start)) : 0; - $length = $length ? \strlen(grapheme_substr($this->string, $start, $length)) : $length; + $length = $length ? \strlen(grapheme_substr(substr($this->string, $start), 0, $length)) : $length; $str->string = substr_replace($this->string, $replacement, $start, $length ?? 2147483647); if (normalizer_is_normalized($str->string)) { diff --git a/core/vendor/symfony/translation/Dumper/PoFileDumper.php b/core/vendor/symfony/translation/Dumper/PoFileDumper.php index 6b0e46dfb2..0ab7c5899a 100644 --- a/core/vendor/symfony/translation/Dumper/PoFileDumper.php +++ b/core/vendor/symfony/translation/Dumper/PoFileDumper.php @@ -30,6 +30,8 @@ public function formatCatalogue(MessageCatalogue $messages, string $domain, arra $output .= "\n"; $newLine = false; + $isIntlDomain = str_ends_with($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX); + foreach ($messages->all($domain) as $source => $target) { if ($newLine) { $output .= "\n"; @@ -48,8 +50,9 @@ public function formatCatalogue(MessageCatalogue $messages, string $domain, arra $output .= $this->formatComments(implode(' ', (array) $metadata['sources']), ':'); } - $sourceRules = $this->getStandardRules($source); - $targetRules = $this->getStandardRules($target); + // in an ICU domain the pipe is an ordinary character, pluralization is expressed by the message itself + $sourceRules = $isIntlDomain ? [] : $this->getStandardRules($source); + $targetRules = $isIntlDomain ? [] : $this->getStandardRules($target); if (2 == \count($sourceRules) && [] !== $targetRules) { $output .= \sprintf('msgid "%s"'."\n", $this->escape($sourceRules[0])); $output .= \sprintf('msgid_plural "%s"'."\n", $this->escape($sourceRules[1])); diff --git a/core/vendor/symfony/translation/Loader/PoFileLoader.php b/core/vendor/symfony/translation/Loader/PoFileLoader.php index 4f8aeb2cb8..750209154d 100644 --- a/core/vendor/symfony/translation/Loader/PoFileLoader.php +++ b/core/vendor/symfony/translation/Loader/PoFileLoader.php @@ -55,6 +55,7 @@ class PoFileLoader extends FileLoader * - No support for comments spanning multiple lines. * - Translator and extracted comments are treated as being the same type. * - Message IDs are allowed to have other encodings as just US-ASCII. + * - Contexts (msgctxt) are parsed but discarded. * * Items with an empty id are ignored. */ @@ -65,6 +66,7 @@ protected function loadResource(string $resource): array $defaults = [ 'ids' => [], 'translated' => null, + 'context' => null, ]; $messages = []; @@ -76,23 +78,28 @@ protected function loadResource(string $resource): array if ('' === $line) { // Whitespace indicated current item is done - if (!\in_array('fuzzy', $flags, true)) { - $this->addMessage($messages, $item); - } - $item = $defaults; - $flags = []; + $this->saveItem($messages, $item, $flags, $defaults); } elseif (str_starts_with($line, '#,')) { + // flags belong to the next entry, so the previous one ends here + if (null !== $item['translated']) { + $this->saveItem($messages, $item, $flags, $defaults); + } $flags = array_map('trim', explode(',', substr($line, 2))); + } elseif (str_starts_with($line, 'msgctxt "')) { + if (null !== $item['translated']) { + $this->saveItem($messages, $item, $flags, $defaults); + } + $item['context'] = substr($line, 9, -1); } elseif (str_starts_with($line, 'msgid "')) { // We start a new msg so save previous - // TODO: this fails when comments or contexts are added - $this->addMessage($messages, $item); - $item = $defaults; + if ($item['ids']) { + $this->saveItem($messages, $item, $flags, $defaults); + } $item['ids']['singular'] = substr($line, 7, -1); } elseif (str_starts_with($line, 'msgstr "')) { $item['translated'] = substr($line, 8, -1); } elseif ('"' === $line[0]) { - $continues = isset($item['translated']) ? 'translated' : 'ids'; + $continues = isset($item['translated']) ? 'translated' : ($item['ids'] ? 'ids' : 'context'); if (\is_array($item[$continues])) { end($item[$continues]); @@ -108,14 +115,21 @@ protected function loadResource(string $resource): array } } // save last item - if (!\in_array('fuzzy', $flags, true)) { - $this->addMessage($messages, $item); - } + $this->saveItem($messages, $item, $flags, $defaults); fclose($stream); return $messages; } + private function saveItem(array &$messages, array &$item, array &$flags, array $defaults): void + { + if (!\in_array('fuzzy', $flags, true)) { + $this->addMessage($messages, $item); + } + $item = $defaults; + $flags = []; + } + /** * Save a translation item to the messages. * diff --git a/core/vendor/symfony/var-dumper/composer.json b/core/vendor/symfony/var-dumper/composer.json index bffa992ec6..f713284277 100644 --- a/core/vendor/symfony/var-dumper/composer.json +++ b/core/vendor/symfony/var-dumper/composer.json @@ -25,7 +25,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" }, "conflict": { "symfony/console": "<6.4" diff --git a/core/vendor/tracy/tracy/composer.json b/core/vendor/tracy/tracy/composer.json index 1c213cbfb6..a4f2e7530e 100644 --- a/core/vendor/tracy/tracy/composer.json +++ b/core/vendor/tracy/tracy/composer.json @@ -44,7 +44,8 @@ "minimum-stability": "dev", "scripts": { "phpstan": "phpstan analyse", - "tester": "tester tests" + "tester": "tester tests", + "compile-templates": "@php tools/latte-convert/compile.php src/Tracy" }, "extra": { "branch-alias": { diff --git a/core/vendor/tracy/tracy/phpstan-stubs.php b/core/vendor/tracy/tracy/phpstan-stubs.php new file mode 100644 index 0000000000..7c2db3dd4b --- /dev/null +++ b/core/vendor/tracy/tracy/phpstan-stubs.php @@ -0,0 +1,79 @@ +|null, + * fromEmail: string|null, + * emailSnooze: string|null, + * logSeverity: int|string|list|null, + * editor: string|false|null, + * browser: string|null, + * errorTemplate: string|null, + * strictMode: bool|int|string|list|null, + * showBar: bool|null, + * maxLength: int|null, + * maxDepth: int|null, + * maxItems: int|null, + * keysToHide: array|null, + * dumpTheme: string|null, + * showLocation: bool|null, + * scream: bool|int|string|list|null, + * bar: list, + * blueScreen: list, + * editorMapping: array|null, + * netteMailer: bool, + * } $config */ class TracyExtension extends Nette\DI\CompilerExtension { @@ -39,7 +62,7 @@ public function getConfigSchema(): Nette\Schema\Schema 'fromEmail' => Expect::email()->dynamic(), 'emailSnooze' => Expect::string()->dynamic(), 'logSeverity' => Expect::anyOf(Expect::int(), $errorSeverityExpr, Expect::listOf($errorSeverity)), - 'editor' => Expect::type('string|null')->dynamic(), + 'editor' => Expect::anyOf(Expect::string(), false, null)->dynamic(), 'browser' => Expect::string()->dynamic(), 'errorTemplate' => Expect::string()->dynamic(), 'strictMode' => Expect::anyOf(Expect::bool(), Expect::int(), $errorSeverityExpr, Expect::listOf($errorSeverity)), @@ -78,7 +101,6 @@ public function loadConfiguration(): void public function afterCompile(Nette\PhpGenerator\ClassType $class): void { $config = $this->config; - \assert($config instanceof \stdClass); $initialize = $this->initialization ?? new Nette\PhpGenerator\Closure; $initialize->addBody('if (!Tracy\Debugger::isEnabled()) { return; }'); @@ -89,7 +111,7 @@ public function afterCompile(Nette\PhpGenerator\ClassType $class): void $initialize->addBody($builder->formatPhp('$logger = ?;', [$logger])); if ( !$logger instanceof Nette\DI\Definitions\ServiceDefinition - || $logger->getFactory()->getEntity() !== [Tracy\Debugger::class, 'getLogger'] + || $logger->getEntity() !== [Tracy\Debugger::class, 'getLogger'] ) { $initialize->addBody('Tracy\Debugger::setLogger($logger);'); } @@ -103,22 +125,27 @@ public function afterCompile(Nette\PhpGenerator\ClassType $class): void } } + $special = [ + 'keysToHide' => <<<'XX' + $keysToHide = ?; + array_push(Tracy\Debugger::$keysToHide, ...$keysToHide); + array_push(Tracy\Debugger::getBlueScreen()->keysToHide, ...$keysToHide); + XX, + 'fromEmail' => 'if ($logger instanceof Tracy\Logger) $logger->fromEmail = ?', + 'emailSnooze' => 'if ($logger instanceof Tracy\Logger) $logger->emailSnooze = ?', + ]; + foreach ($options as $key => $value) { - if ($value !== null) { - $tbl = [ - 'keysToHide' => <<<'XX' - $keysToHide = ?; - array_push(Tracy\Debugger::$keysToHide, ...$keysToHide); - array_push(Tracy\Debugger::getBlueScreen()->keysToHide, ...$keysToHide); - XX, - 'fromEmail' => 'if ($logger instanceof Tracy\Logger) $logger->fromEmail = ?', - 'emailSnooze' => 'if ($logger instanceof Tracy\Logger) $logger->emailSnooze = ?', - ]; - $initialize->addBody($builder->formatPhp( - ($tbl[$key] ?? 'Tracy\Debugger::$' . $key . ' = ?') . ';', - Nette\DI\Helpers::filterArguments([$value]), - )); + if ($key === 'editor' && $value === false) { + $value = null; // 'editor: false' disables editor links + } elseif ($value === null) { + continue; } + + $initialize->addBody($builder->formatPhp( + ($special[$key] ?? 'Tracy\Debugger::$' . $key . ' = ?') . ';', + Nette\DI\Helpers::filterArguments([$value]), + )); } if ($config->netteMailer && $builder->getByType(Nette\Mail\IMailer::class)) { diff --git a/core/vendor/tracy/tracy/src/Bridges/Psr/TracyToPsrLoggerAdapter.php b/core/vendor/tracy/tracy/src/Bridges/Psr/TracyToPsrLoggerAdapter.php index e460082b14..bf927abce8 100644 --- a/core/vendor/tracy/tracy/src/Bridges/Psr/TracyToPsrLoggerAdapter.php +++ b/core/vendor/tracy/tracy/src/Bridges/Psr/TracyToPsrLoggerAdapter.php @@ -39,10 +39,15 @@ public function __construct( public function log($level, $message, array $context = []): void { $level = self::LevelMap[$level] ?? Tracy\ILogger::ERROR; + $message = (string) $message; if (isset($context['exception']) && $context['exception'] instanceof \Throwable) { - $this->tracyLogger->log($context['exception'], $level); + $exception = $context['exception']; unset($context['exception']); + $this->tracyLogger->log($exception, $level); + if (!$context && ($message === '' || $message === $exception->getMessage())) { + return; // exception entry already carries all the information + } } if ($context) { diff --git a/core/vendor/tracy/tracy/src/Tracy/Bar/assets/bar.js b/core/vendor/tracy/tracy/src/Tracy/Bar/assets/bar.js index f90637e812..a95eb3387b 100644 --- a/core/vendor/tracy/tracy/src/Tracy/Bar/assets/bar.js +++ b/core/vendor/tracy/tracy/src/Tracy/Bar/assets/bar.js @@ -2,9 +2,7 @@ * This file is part of the Tracy (https://tracy.nette.org) */ -if (navigator.webdriver) { - document.cookie = 'tracy-webdriver=1;path=/;SameSite=Lax'; -} +document.cookie = navigator.webdriver ? 'tracy-webdriver=1;path=/;SameSite=Lax' : 'tracy-webdriver=;path=/;Max-Age=0;SameSite=Lax'; let requestId = document.currentScript.dataset.id, ajaxCounter = 1, @@ -23,6 +21,14 @@ function getOption(key) { return global === undefined ? defaults[key] : global; } +function restoreJSON(key) { + try { + return JSON.parse(localStorage.getItem(key)); + } catch { + return null; // ignore corrupt data + } +} + class Panel { constructor(id) { this.id = id; @@ -144,8 +150,8 @@ class Panel { toWindow() { let offset = getOffset(this.elem); - offset.left += typeof window.screenLeft === 'number' ? window.screenLeft : (window.screenX + 10); - offset.top += typeof window.screenTop === 'number' ? window.screenTop : (window.screenY + 50); + offset.left += window.screenLeft; + offset.top += window.screenTop; let win = window.open('', this.id.replace(/-/g, '_'), 'left=' + offset.left + ',top=' + offset.top + ',width=' + this.elem.offsetWidth + ',height=' + this.elem.offsetHeight + ',resizable=yes,scrollbars=yes'); @@ -154,7 +160,8 @@ class Panel { } let doc = win.document; - doc.write(''); + doc.head.appendChild(doc.createElement('meta')).setAttribute('charset', 'utf-8'); + doc.body.id = 'tracy-debug'; let script = doc.createElement('script'); script.src = baseUrl + '_tracy_bar=js&XDEBUG_SESSION_STOP=1'; @@ -178,7 +185,7 @@ class Panel { }); doc.addEventListener('keyup', (e) => { - if (e.keyCode === 27 && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (e.key === 'Escape' && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) { win.close(); } }); @@ -221,7 +228,7 @@ class Panel { restorePosition() { let key = this.id.split(':')[0]; - let pos = JSON.parse(localStorage.getItem(key)); + let pos = restoreJSON(key); if (!pos) { this.elem.classList.add(Panel.PEEK); } else if (pos.window) { @@ -379,7 +386,7 @@ class Bar { restorePosition() { - let pos = JSON.parse(localStorage.getItem(this.id)); + let pos = restoreJSON(this.id); setPosition(this.elem, pos || { right: 0, bottom: 0 }); this.savePosition(); } @@ -492,19 +499,22 @@ class Debug { oldOpen.apply(this, arguments); if (getOption('AutoRefresh') && new URL(arguments[1], location.origin).host === location.host) { - let reqId = Tracy.getAjaxHeader(); - this.setRequestHeader('X-Tracy-Ajax', reqId); - this.addEventListener('load', function () { - if (this.getAllResponseHeaders().match(/^X-Tracy-Ajax: 1/mi)) { - Debug.loadScript(baseUrl + '_tracy_bar=content-ajax.' + reqId + '&XDEBUG_SESSION_STOP=1&v=' + Math.random()); - } - }); + this.tracyReqId = Tracy.getAjaxHeader(); + this.setRequestHeader('X-Tracy-Ajax', this.tracyReqId); + if (!this.tracyLoadListener) { // open() may be called repeatedly on the same instance + this.tracyLoadListener = true; + this.addEventListener('load', function () { + if (this.getAllResponseHeaders().match(/^X-Tracy-Ajax: 1/mi)) { + Debug.loadScript(baseUrl + '_tracy_bar=content-ajax.' + this.tracyReqId + '&XDEBUG_SESSION_STOP=1&v=' + Math.random()); + } + }); + } } }; let oldFetch = window.fetch; window.fetch = function (request, options) { - request = request instanceof Request ? request : new Request(request, options || {}); + request = new Request(request, options); let reqId = request.headers.get('X-Tracy-Ajax'); if (getOption('AutoRefresh') && !reqId && new URL(request.url, location.origin).host === location.host) { @@ -541,6 +551,7 @@ function evalScripts(elem) { let dolly = document.createElement('script'); dolly.textContent = script.textContent; (document.body || document.documentElement).appendChild(dolly); + dolly.remove(); script.tracyEvaluated = true; } }); diff --git a/core/vendor/tracy/tracy/src/Tracy/Bar/dist/info.panel.phtml b/core/vendor/tracy/tracy/src/Tracy/Bar/dist/info.panel.phtml index e026ff40c0..e87dfa97c7 100644 --- a/core/vendor/tracy/tracy/src/Tracy/Bar/dist/info.panel.phtml +++ b/core/vendor/tracy/tracy/src/Tracy/Bar/dist/info.panel.phtml @@ -56,7 +56,7 @@ if ($packages || $devPackages) /* pos 35:3 */ { echo '

Composer Packages ('; echo Tracy\Helpers::escapeHtml(count($packages)) /* pos 37:24 */; - echo Tracy\Helpers::escapeHtml($devPackages ? ' + ' . count($devPackages) . ' dev' : '') /* pos 37:42 */; + echo Tracy\Helpers::escapeHtml($devPackages ? ' + ' . count($devPackages) . ' dev' : null) /* pos 37:42 */; echo ')

@@ -72,7 +72,7 @@ if ($packages || $devPackages) /* pos 35:3 */ { echo ' '; echo Tracy\Helpers::escapeHtml($package->version) /* pos 44:11 */; - echo Tracy\Helpers::escapeHtml(strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : '') /* pos 44:30 */; + echo Tracy\Helpers::escapeHtml(strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : null) /* pos 44:30 */; echo ' '; @@ -95,7 +95,7 @@ if ($packages || $devPackages) /* pos 35:3 */ { echo ' '; echo Tracy\Helpers::escapeHtml($package->version) /* pos 54:12 */; - echo Tracy\Helpers::escapeHtml(strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : '') /* pos 54:31 */; + echo Tracy\Helpers::escapeHtml(strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : null) /* pos 54:31 */; echo ' '; diff --git a/core/vendor/tracy/tracy/src/Tracy/Bar/dist/warnings.panel.phtml b/core/vendor/tracy/tracy/src/Tracy/Bar/dist/warnings.panel.phtml index 78b6c0ed5d..fd722292a1 100644 --- a/core/vendor/tracy/tracy/src/Tracy/Bar/dist/warnings.panel.phtml +++ b/core/vendor/tracy/tracy/src/Tracy/Bar/dist/warnings.panel.phtml @@ -10,7 +10,7 @@ foreach ($data as $item => $count) /* pos 6:3 */ { [$file, $line, $message] = explode('|', $item, 3) /* pos 7:4 */; echo ' '; - echo Tracy\Helpers::escapeHtml($count ? $count . '×' : '') /* pos 9:29 */; + echo Tracy\Helpers::escapeHtml($count ? $count . '×' : null) /* pos 9:29 */; echo '
';
 	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 '
'; diff --git a/core/vendor/tracy/tracy/src/Tracy/Debugger/Debugger.php b/core/vendor/tracy/tracy/src/Tracy/Debugger/Debugger.php index 2b581311c8..b48dbb75ec 100644 --- a/core/vendor/tracy/tracy/src/Tracy/Debugger/Debugger.php +++ b/core/vendor/tracy/tracy/src/Tracy/Debugger/Debugger.php @@ -17,7 +17,7 @@ */ class Debugger { - public const Version = '2.12.0'; + public const Version = '2.12.1'; /** server modes for Debugger::enable() */ public const @@ -168,7 +168,7 @@ final public function __construct() /** * Enables displaying or logging errors and exceptions. * @param bool|string|string[] $mode use constant Debugger::Production, Development, Detect (autodetection) or IP address(es) whitelist. - * @param string $logDirectory error log directory + * @param ?string $logDirectory error log directory * @param string|string[]|null $email administrator email; enables email sending in production mode */ public static function enable( @@ -484,33 +484,22 @@ public static function getSessionStorage(): SessionStorage public static function dump(mixed $var, bool $return = false): mixed { if ($return) { - $options = [ - Dumper::DEPTH => self::$maxDepth, - Dumper::TRUNCATE => self::$maxLength, - Dumper::ITEMS => self::$maxItems, - ]; + $options = self::dumpOptions(); return Helpers::isCli() - ? Dumper::toText($var) + ? Dumper::toText($var, $options) : Helpers::capture(fn() => Dumper::dump($var, $options)); } elseif (!self::$productionMode) { $html = Helpers::isHtmlMode(); echo $html ? '' : ''; - Dumper::dump($var, [ - Dumper::DEPTH => self::$maxDepth, - Dumper::TRUNCATE => self::$maxLength, - Dumper::ITEMS => self::$maxItems, + Dumper::dump($var, self::dumpOptions() + [ Dumper::LOCATION => self::$showLocation, Dumper::THEME => self::$dumpTheme, - Dumper::KEYS_TO_HIDE => self::$keysToHide, ]); echo $html ? '' : ''; if ($html && Helpers::isAgent()) { - Helpers::consoleLog(Dumper::toText($var, [ - Dumper::DEPTH => 3, - Dumper::KEYS_TO_HIDE => self::$keysToHide, - ])); + Helpers::consoleLog(Dumper::toText($var, self::agentDumpOptions())); } } @@ -549,23 +538,38 @@ public static function barDump(mixed $var, ?string $title = null, array $options self::getBar()->addPanel($panel = new DefaultBarPanel('dumps'), 'Tracy:dumps'); } - $panel->data[] = ['title' => $title, 'dump' => Dumper::toHtml($var, $options + [ - Dumper::DEPTH => self::$maxDepth, - Dumper::ITEMS => self::$maxItems, - Dumper::TRUNCATE => self::$maxLength, + $panel->data[] = ['title' => $title, 'dump' => Dumper::toHtml($var, $options + self::dumpOptions() + [ Dumper::LOCATION => self::$showLocation ?: Dumper::LOCATION_CLASS | Dumper::LOCATION_SOURCE, Dumper::LAZY => true, - Dumper::KEYS_TO_HIDE => self::$keysToHide, - ]), 'text' => Helpers::isAgent() ? Dumper::toText($var, [ - Dumper::DEPTH => 3, - Dumper::KEYS_TO_HIDE => self::$keysToHide, - ]) : null]; + ]), 'text' => Helpers::isAgent() ? Dumper::toText($var, self::agentDumpOptions()) : null]; } return $var; } + /** @return array */ + private static function dumpOptions(): array + { + return [ + Dumper::DEPTH => self::$maxDepth, + Dumper::TRUNCATE => self::$maxLength, + Dumper::ITEMS => self::$maxItems, + Dumper::KEYS_TO_HIDE => self::$keysToHide, + ]; + } + + + /** @return array */ + private static function agentDumpOptions(): array + { + return [ + Dumper::DEPTH => 3, + Dumper::KEYS_TO_HIDE => self::$keysToHide, + ]; + } + + /** * Logs message or exception. */ diff --git a/core/vendor/tracy/tracy/src/Tracy/Debugger/DeferredContent.php b/core/vendor/tracy/tracy/src/Tracy/Debugger/DeferredContent.php index 2dfe9f688f..cb40be1982 100644 --- a/core/vendor/tracy/tracy/src/Tracy/Debugger/DeferredContent.php +++ b/core/vendor/tracy/tracy/src/Tracy/Debugger/DeferredContent.php @@ -67,14 +67,17 @@ public function addSetup(string $method, mixed $argument): void public function sendAssets(): bool { + $asset = $_GET['_tracy_bar'] ?? null; if (headers_sent($file, $line) || ob_get_length()) { + if ($asset === null && !$this->deferred) { // nothing to send, repeated enable() is a no-op + return false; + } + throw new \LogicException( __METHOD__ . '() called after some output has been sent. ' . ($file ? "Output started at $file:$line." : 'Try Tracy\OutputDebugger to find where output started.'), ); } - - $asset = $_GET['_tracy_bar'] ?? null; if ($asset === 'js') { header('Content-Type: application/javascript; charset=UTF-8'); header('Cache-Control: max-age=864000'); @@ -157,7 +160,7 @@ private function buildJsCss(): string public function clean(): void { foreach ($this->sessionStorage->getData() as &$items) { - $items = array_slice((array) $items, -10, null, preserve_keys: true); + $items = array_slice((array) $items, -10, preserve_keys: true); $items = array_filter($items, fn($item) => isset($item['time']) && $item['time'] > time() - 60); } } diff --git a/core/vendor/tracy/tracy/src/Tracy/Debugger/DevelopmentStrategy.php b/core/vendor/tracy/tracy/src/Tracy/Debugger/DevelopmentStrategy.php index 8c3a0a3658..8f81711fab 100644 --- a/core/vendor/tracy/tracy/src/Tracy/Debugger/DevelopmentStrategy.php +++ b/core/vendor/tracy/tracy/src/Tracy/Debugger/DevelopmentStrategy.php @@ -51,10 +51,14 @@ public function handleException(\Throwable $exception, bool $firstTime): void private function renderExceptionCli(\Throwable $exception): void { + $escape = fn(string $s): string => Helpers::isHtmlMode() // exception message may contain user input + ? '
' . 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 */ @@ -82,6 +82,7 @@ class Dumper \__PHP_Incomplete_Class::class => [Exposer::class, 'exposePhpIncompleteClass'], \Generator::class => [Exposer::class, 'exposeGenerator'], \Fiber::class => [Exposer::class, 'exposeFiber'], + \CurlHandle::class => [Exposer::class, 'exposeCurl'], \DOMNode::class => [Exposer::class, 'exposeDOMNode'], \DOMNodeList::class => [Exposer::class, 'exposeDOMNodeList'], \DOMNamedNodeMap::class => [Exposer::class, 'exposeDOMNodeList'], @@ -96,6 +97,9 @@ class Dumper Ds\Heap::class => [Exposer::class, 'exposeDsCollection'], Ds\Map::class => [Exposer::class, 'exposeDsMap'], \WeakMap::class => [Exposer::class, 'exposeWeakMap'], + \WeakReference::class => [Exposer::class, 'exposeWeakReference'], + Uri\Rfc3986\Uri::class => [Exposer::class, 'exposeUri'], + Uri\WhatWg\Url::class => [Exposer::class, 'exposeUri'], ]; /** @var array */ diff --git a/core/vendor/tracy/tracy/src/Tracy/Dumper/Exposer.php b/core/vendor/tracy/tracy/src/Tracy/Dumper/Exposer.php index 35a0165d7a..87106d7f35 100644 --- a/core/vendor/tracy/tracy/src/Tracy/Dumper/Exposer.php +++ b/core/vendor/tracy/tracy/src/Tracy/Dumper/Exposer.php @@ -9,7 +9,8 @@ use Dom; use Ds; -use function array_diff_key, array_key_exists, count, end, explode, get_mangled_object_vars, implode, iterator_to_array, preg_match_all, sort; +use Uri; +use function array_diff_key, array_key_exists, count, end, explode, get_debug_type, get_mangled_object_vars, implode, iterator_to_array, sort; use const PHP_VERSION_ID; @@ -55,13 +56,14 @@ public static function exposeObject(object $obj, Value $value, Describer $descri $describer->describeEnumProperty($class, $name, $values[$k]), ); } else { + $virtual = PHP_VERSION_ID >= 80400 && (new \ReflectionProperty($class, $name))->isVirtual(); $describer->addPropertyTo( $value, $name, null, $type, class: $class, - described: new Value(Value::TypeText, 'unset'), + described: new Value(Value::TypeText, $virtual ? '{virtual}' : 'unset'), ); } } @@ -115,6 +117,10 @@ public static function exposeClosure(\Closure $obj, Value $value, Describer $des $value->value .= '(' . implode(', ', $params) . ')'; + if ($this_ = $rc->getClosureThis()) { + $describer->addPropertyTo($value, 'this', null, described: new Value(Value::TypeText, get_debug_type($this_))); + } + $uses = []; $useValue = new Value(Value::TypeObject); $useValue->depth = $value->depth + 1; @@ -154,7 +160,13 @@ public static function exposeArrayObject(\ArrayObject $obj, Value $value, Descri public static function exposeDOMNode(\DOMNode|Dom\Node $obj, Value $value, Describer $describer): void { - $props = preg_match_all('#^\s*\[([^\]]+)\] =>#m', print_r($obj, return: true), $tmp) ? $tmp[1] : []; + $props = []; + foreach ((new \ReflectionClass($obj))->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) { + if (!$prop->isStatic()) { + $props[] = $prop->getName(); + } + } + sort($props); foreach ($props as $p) { $describer->addPropertyTo($value, $p, @$obj->$p, Value::PropertyPublic); // @ some props may be deprecated @@ -206,6 +218,34 @@ public static function exposeSplFileInfo(\SplFileInfo $obj): array } + /** @return array */ + public static function exposeCurl(\CurlHandle $curl): array + { + return curl_getinfo($curl); + } + + + public static function exposeWeakReference(\WeakReference $ref, Value $value, Describer $describer): void + { + if ($obj = $ref->get()) { + $describer->addPropertyTo($value, 'object', $obj); + } else { + $value->value .= ' (dead)'; + } + } + + + public static function exposeUri(Uri\Rfc3986\Uri|Uri\WhatWg\Url $uri, Value $value, Describer $describer): void + { + $describer->addPropertyTo($value, 'uri', $uri instanceof Uri\WhatWg\Url ? $uri->toAsciiString() : $uri->toRawString()); + foreach ($uri->__debugInfo() as $k => $v) { + if ($v !== null && $v !== '') { + $describer->addPropertyTo($value, $k, $v); + } + } + } + + public static function exposeSplObjectStorage(\SplObjectStorage $obj, Value $value, Describer $describer): void { $value->value .= ' (' . count($obj) . ')'; @@ -293,16 +333,25 @@ public static function exposeDsMap( private static function exposeLazyObject(object $obj, Describer $describer, Value $value): void { $rc = new \ReflectionClass($obj); - foreach ($rc->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) { - if (!$prop->isLazy($obj)) { - $describer->addPropertyTo( - $value, - $prop->getName(), - $prop->getValue($obj), - Value::PropertyPublic, - described: $describer->describeEnumProperty($obj::class, $prop->getName(), $prop->getValue($obj)), - ); + foreach ($rc->getProperties() as $prop) { + if ($prop->isStatic() || $prop->isLazy($obj) || !$prop->isInitialized($obj)) { + continue; } + + $type = match (true) { + $prop->isPrivate() => Value::PropertyPrivate, + $prop->isProtected() => Value::PropertyProtected, + default => Value::PropertyPublic, + }; + $v = $prop->getValue($obj); + $describer->addPropertyTo( + $value, + $prop->getName(), + $v, + $type, + class: $prop->getDeclaringClass()->getName(), + described: $describer->describeEnumProperty($prop->getDeclaringClass()->getName(), $prop->getName(), $v), + ); } $value->value .= ' (lazy)'; diff --git a/core/vendor/tracy/tracy/src/Tracy/Dumper/assets/dumper.js b/core/vendor/tracy/tracy/src/Tracy/Dumper/assets/dumper.js index 81417ad719..29bd5a80b5 100644 --- a/core/vendor/tracy/tracy/src/Tracy/Dumper/assets/dumper.js +++ b/core/vendor/tracy/tracy/src/Tracy/Dumper/assets/dumper.js @@ -19,23 +19,31 @@ class Dumper { static init(context) { // full lazy (context || document).querySelectorAll('[data-tracy-snapshot][data-tracy-dump]').forEach((pre) => { //
-			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) {
                     
 
 					
-						* :
+						* :
 						
 							
 					
diff --git a/manager/views/page/system_settings/security.blade.php b/manager/views/page/system_settings/security.blade.php
index a082eed3ac..ea7e4ed309 100644
--- a/manager/views/page/system_settings/security.blade.php
+++ b/manager/views/page/system_settings/security.blade.php
@@ -181,6 +181,19 @@
 
         
+ @include('manager::form.input', [ + 'name' => 'pwd_repair_minutes', + 'label' => __('global.pwd_repair_minutes_title'), + 'small' => '[(pwd_repair_minutes)]', + 'value' => $settings['pwd_repair_minutes'], + 'attributes' => 'onchange="documentDirty=true;"', + 'comment' => (isset($disabledSettings['pwd_repair_minutes']) ? __('global.setting_from_file') . '
' : '') . + __('global.pwd_repair_minutes_message'), + 'disabled' => $disabledSettings['pwd_repair_minutes'] ?? null + ]) + +
+ @include('manager::form.radio', [ 'name' => 'pwd_hash_algo', 'label' => __('global.pwd_hash_algo_title'),