diff --git a/.editorconfig b/.editorconfig index 5761cb154..e80b01571 100644 --- a/.editorconfig +++ b/.editorconfig @@ -45,7 +45,7 @@ indent_size = 4 [*.sh] indent_size = 2 -[{db/*.sql,includes/**}] +[{db/*.sql,includes/**,*.json}] max_line_length = unset [*.{yml,yaml}] diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2569f189a..7fa4cbf8d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,7 +34,7 @@ stages: needs: - composer install before_script: - - composer install --no-ansi --no-progress + - composer install --no-ansi --no-progress --ignore-platform-req=php # for jobs that depend on yarn .use_yarn: &use_yarn @@ -62,7 +62,7 @@ composer install: - composer audit - composer validate script: - - composer install --no-ansi --no-progress + - composer install --no-ansi --no-progress --ignore-platform-req=php composer audit: image: php:latest @@ -120,6 +120,7 @@ generate-version: (git describe --abbrev=0 --tags | tr -d '\n')\ && echo "-${CI_COMMIT_REF_NAME}+${CI_PIPELINE_ID}.${CI_COMMIT_SHORT_SHA}"\ )\ + || echo "0-${CI_COMMIT_REF_NAME}+${CI_PIPELINE_ID}.${CI_COMMIT_SHORT_SHA}"\ )" - echo "${VERSION}" - echo -n "${VERSION}" > storage/app/VERSION @@ -386,7 +387,7 @@ deploy: .kubectl_deployment: &kubectl_deployment stage: deploy image: - name: bitnami/kubectl:latest + name: bitnamisecure/kubectl:latest entrypoint: [ '' ] needs: - test diff --git a/.phpcs.xml b/.phpcs.xml index c04ded719..be822f4cc 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -30,6 +30,7 @@ /includes + /includes diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 69382b730..b1ed01260 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -229,3 +229,11 @@ If unspecific issues appear try using Docker version >= 20.10.14. ### `service "es_workspace" is not running` Make sure you're running your docker commands from the `docker/dev` directory, not from `docker` + +### `main` is broken after pulling the latest commits from upstream +Try running +```bash +composer install +``` +from this repository's root directory. +If dependencies have been updated in `composer.json` since you last synced `main`, this should fix it. diff --git a/bin/pre-commit b/bin/pre-commit index 252b92988..cd144a8c4 100755 --- a/bin/pre-commit +++ b/bin/pre-commit @@ -16,7 +16,7 @@ testing 'PHP ⚙️' composer validate composer phpcs composer phpstan -./vendor/bin/phpunit +php -d memory_limit=-1 ./vendor/bin/phpunit testing 'translations 🗺️' find resources/lang -type f -name '*.po' -exec sh -c 'msgfmt "${1%.*}.po" -o"${1%.*}.mo"' shell {} \; diff --git a/composer.json b/composer.json index 9962c11fd..0a77a72c1 100644 --- a/composer.json +++ b/composer.json @@ -24,8 +24,8 @@ "phpcbf -p" ], "phpstan": "phpstan", - "phpunit": "phpunit", - "phpunit:coverage": "phpunit --coverage-text --coverage-html ./public/coverage/" + "phpunit": "php -d memory_limit=-1 vendor/bin/phpunit", + "phpunit:coverage": "php -d memory_limit=-1 vendor/bin/phpunit --coverage-text --coverage-html ./public/coverage/" }, "require": { "php": ">=8.2.0", @@ -35,14 +35,17 @@ "ext-pdo": "*", "ext-simplexml": "*", "ext-xml": "*", - "erusev/parsedown": "^1.7", + "bacon/bacon-qr-code": "^3.0", + "firebase/php-jwt": "^6.11", "gettext/gettext": "^5.7", "gettext/translator": "^1.2", - "guzzlehttp/guzzle": "^7.9", - "illuminate/container": "^11.27", - "illuminate/database": "^11.27", - "illuminate/support": "^11.27", - "laravel/serializable-closure": "^1.3", + "guzzlehttp/guzzle": "^7.10", + "illuminate/container": "^12.39", + "illuminate/database": "^12.39", + "illuminate/pagination": "^12.39", + "illuminate/support": "^12.39", + "laravel/serializable-closure": "^2.0", + "league/commonmark": "^2.7", "league/oauth2-client": "^2.7", "league/openapi-psr7-validator": "^0.22.0", "nikic/fast-route": "^1.3", @@ -51,24 +54,24 @@ "psr/http-message": "^1.1", "psr/http-server-middleware": "^1.0", "psr/log": "^3.0", - "rcrowe/twigbridge": "^0.14.x-dev", - "respect/validation": "^1.1", - "symfony/http-foundation": "^7.1", - "symfony/mailer": "^7.1", - "symfony/psr-http-message-bridge": "^7.1", - "twig/twig": "^3.14", + "rcrowe/twigbridge": "^0.14", + "respect/validation": "^2.4", + "symfony/http-foundation": "^7.3", + "symfony/mailer": "^7.3", + "symfony/psr-http-message-bridge": "^7.3", + "twig/twig": "^3.22", "vlucas/phpdotenv": "^5.6" }, "require-dev": { "dms/phpunit-arraysubset-asserts": "^0.5.0", - "fakerphp/faker": "^1.23", - "fig/log-test": "^1.1", - "filp/whoops": "^2.16", - "phpstan/phpstan": "^1.12", + "fakerphp/faker": "^1.24", + "fig/log-test": "^1.2", + "filp/whoops": "^2.18", + "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^9.6", - "slevomat/coding-standard": "^8.15", - "squizlabs/php_codesniffer": "^3.10", - "symfony/var-dumper": "^7.1" + "slevomat/coding-standard": "^8.25", + "squizlabs/php_codesniffer": "^4.0", + "symfony/var-dumper": "^7.3" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 0bba00aa4..90a6db544 100644 --- a/composer.lock +++ b/composer.lock @@ -4,29 +4,84 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "29d2127c46a82264d42942c32817dc43", + "content-hash": "1b1bbcb0ba04a66eb133d7531fef1b18", "packages": [ + { + "name": "bacon/bacon-qr-code", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/36a1cb2b81493fa5b82e50bf8068bf84d1542563", + "reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^8.1" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.12", + "phpunit/phpunit": "^10.5.11 || ^11.0.4", + "spatie/phpunit-snapshot-assertions": "^5.1.5", + "spatie/pixelmatch-php": "^1.2.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.3" + }, + "time": "2025-11-19T17:15:36+00:00" + }, { "name": "brick/math", - "version": "0.12.1", + "version": "0.14.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", - "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.2" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^10.1", - "vimeo/psalm": "5.16.0" + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" }, "type": "library", "autoload": { @@ -56,7 +111,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.1" + "source": "https://github.com/brick/math/tree/0.14.1" }, "funding": [ { @@ -64,7 +119,7 @@ "type": "github" } ], - "time": "2023-11-29T23:19:16+00:00" + "time": "2025-11-24T14:40:29+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -135,23 +190,73 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, { "name": "devizzent/cebe-php-openapi", - "version": "1.1.0", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/DEVizzent/cebe-php-openapi.git", - "reference": "9ae960c072eda54d8d0eb9dc14c1e6d815167347" + "reference": "af42b77f339b6b2920b65bae5df748e47391e11d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DEVizzent/cebe-php-openapi/zipball/9ae960c072eda54d8d0eb9dc14c1e6d815167347", - "reference": "9ae960c072eda54d8d0eb9dc14c1e6d815167347", + "url": "https://api.github.com/repos/DEVizzent/cebe-php-openapi/zipball/af42b77f339b6b2920b65bae5df748e47391e11d", + "reference": "af42b77f339b6b2920b65bae5df748e47391e11d", "shasum": "" }, "require": { "ext-json": "*", - "justinrainbow/json-schema": "^5.2", + "justinrainbow/json-schema": "^5.2 || ^6.0", "php": ">=7.1.0", "symfony/yaml": "^3.4 || ^4 || ^5 || ^6 || ^7" }, @@ -209,37 +314,111 @@ "issues": "https://github.com/DEVizzent/cebe-php-openapi/issues", "source": "https://github.com/DEVizzent/cebe-php-openapi" }, - "time": "2024-10-30T05:57:15+00:00" + "time": "2025-01-16T11:12:34+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" }, { "name": "doctrine/inflector", - "version": "2.0.10", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11.0", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25 || ^5.4" + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + "Doctrine\\Inflector\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -284,7 +463,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.10" + "source": "https://github.com/doctrine/inflector/tree/2.1.0" }, "funding": [ { @@ -300,7 +479,7 @@ "type": "tidelift" } ], - "time": "2024-02-18T20:23:39+00:00" + "time": "2025-08-10T19:31:58+00:00" }, { "name": "doctrine/lexer", @@ -381,16 +560,16 @@ }, { "name": "egulias/email-validator", - "version": "4.0.2", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e" + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ebaaf5be6c0286928352e054f2d5125608e5405e", - "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", "shasum": "" }, "require": { @@ -436,7 +615,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/4.0.2" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" }, "funding": [ { @@ -444,70 +623,83 @@ "type": "github" } ], - "time": "2023-10-06T06:47:41+00:00" + "time": "2025-03-06T22:45:56+00:00" }, { - "name": "erusev/parsedown", - "version": "1.7.4", + "name": "firebase/php-jwt", + "version": "v6.11.1", "source": { "type": "git", - "url": "https://github.com/erusev/parsedown.git", - "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3" + "url": "https://github.com/firebase/php-jwt.git", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3", - "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", "shasum": "" }, "require": { - "ext-mbstring": "*", - "php": ">=5.3.0" + "php": "^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35" + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" }, "type": "library", "autoload": { - "psr-0": { - "Parsedown": "" + "psr-4": { + "Firebase\\JWT\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Emanuil Rusev", - "email": "hello@erusev.com", - "homepage": "http://erusev.com" + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" } ], - "description": "Parser for Markdown.", - "homepage": "http://parsedown.org", + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", "keywords": [ - "markdown", - "parser" + "jwt", + "php" ], "support": { - "issues": "https://github.com/erusev/parsedown/issues", - "source": "https://github.com/erusev/parsedown/tree/1.7.x" + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" }, - "time": "2019-12-30T22:54:17+00:00" + "time": "2025-04-09T20:32:01+00:00" }, { "name": "gettext/gettext", - "version": "v5.7.1", + "version": "v5.7.3", "source": { "type": "git", "url": "https://github.com/php-gettext/Gettext.git", - "reference": "a9f89e0cc9d9a67b422632b594b5f1afb16eccfc" + "reference": "95820f020e4f2f05e0bbaa5603e4c6ec3edc50f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-gettext/Gettext/zipball/a9f89e0cc9d9a67b422632b594b5f1afb16eccfc", - "reference": "a9f89e0cc9d9a67b422632b594b5f1afb16eccfc", + "url": "https://api.github.com/repos/php-gettext/Gettext/zipball/95820f020e4f2f05e0bbaa5603e4c6ec3edc50f1", + "reference": "95820f020e4f2f05e0bbaa5603e4c6ec3edc50f1", "shasum": "" }, "require": { @@ -552,7 +744,7 @@ "support": { "email": "oom@oscarotero.com", "issues": "https://github.com/php-gettext/Gettext/issues", - "source": "https://github.com/php-gettext/Gettext/tree/v5.7.1" + "source": "https://github.com/php-gettext/Gettext/tree/v5.7.3" }, "funding": [ { @@ -568,20 +760,20 @@ "type": "patreon" } ], - "time": "2024-07-24T22:05:18+00:00" + "time": "2024-12-01T10:18:08+00:00" }, { "name": "gettext/languages", - "version": "2.10.0", + "version": "2.12.1", "source": { "type": "git", "url": "https://github.com/php-gettext/Languages.git", - "reference": "4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab" + "reference": "0b0b0851c55168e1dfb14305735c64019732b5f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-gettext/Languages/zipball/4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab", - "reference": "4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab", + "url": "https://api.github.com/repos/php-gettext/Languages/zipball/0b0b0851c55168e1dfb14305735c64019732b5f1", + "reference": "0b0b0851c55168e1dfb14305735c64019732b5f1", "shasum": "" }, "require": { @@ -591,7 +783,8 @@ "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4" }, "bin": [ - "bin/export-plural-rules" + "bin/export-plural-rules", + "bin/import-cldr-data" ], "type": "library", "autoload": { @@ -630,7 +823,7 @@ ], "support": { "issues": "https://github.com/php-gettext/Languages/issues", - "source": "https://github.com/php-gettext/Languages/tree/2.10.0" + "source": "https://github.com/php-gettext/Languages/tree/2.12.1" }, "funding": [ { @@ -642,20 +835,20 @@ "type": "github" } ], - "time": "2022-10-18T15:00:10+00:00" + "time": "2025-03-19T11:14:02+00:00" }, { "name": "gettext/translator", - "version": "v1.2.0", + "version": "v1.2.1", "source": { "type": "git", "url": "https://github.com/php-gettext/Translator.git", - "reference": "a4fa5ed740f304a0ed7b3e169b2b554a195c7570" + "reference": "8ae0ac79053bcb732a6c584cd86f7a82ef183161" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-gettext/Translator/zipball/a4fa5ed740f304a0ed7b3e169b2b554a195c7570", - "reference": "a4fa5ed740f304a0ed7b3e169b2b554a195c7570", + "url": "https://api.github.com/repos/php-gettext/Translator/zipball/8ae0ac79053bcb732a6c584cd86f7a82ef183161", + "reference": "8ae0ac79053bcb732a6c584cd86f7a82ef183161", "shasum": "" }, "require": { @@ -700,7 +893,7 @@ "support": { "email": "oom@oscarotero.com", "issues": "https://github.com/php-gettext/Translator/issues", - "source": "https://github.com/php-gettext/Translator/tree/v1.2.0" + "source": "https://github.com/php-gettext/Translator/tree/v1.2.1" }, "funding": [ { @@ -716,7 +909,7 @@ "type": "patreon" } ], - "time": "2023-11-06T15:42:03+00:00" + "time": "2025-01-09T09:20:22+00:00" }, { "name": "graham-campbell/result-type", @@ -782,22 +975,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.9.2", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -888,7 +1081,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" }, "funding": [ { @@ -904,20 +1097,20 @@ "type": "tidelift" } ], - "time": "2024-07-24T11:22:20+00:00" + "time": "2025-08-23T22:36:01+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.4", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + "reference": "481557b130ef3790cf82b713667b43030dc9c957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { @@ -925,7 +1118,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "type": "library", "extra": { @@ -971,7 +1164,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.4" + "source": "https://github.com/guzzle/promises/tree/2.3.0" }, "funding": [ { @@ -987,20 +1180,20 @@ "type": "tidelift" } ], - "time": "2024-10-17T10:06:22+00:00" + "time": "2025-08-22T14:34:08+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.0", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + "reference": "21dc724a0583619cd1652f673303492272778051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", "shasum": "" }, "require": { @@ -1016,7 +1209,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -1087,7 +1280,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" + "source": "https://github.com/guzzle/psr7/tree/2.8.0" }, "funding": [ { @@ -1103,36 +1296,36 @@ "type": "tidelift" } ], - "time": "2024-07-18T11:15:46+00:00" + "time": "2025-08-23T21:21:41+00:00" }, { "name": "illuminate/bus", - "version": "v11.30.0", + "version": "v12.40.2", "source": { "type": "git", "url": "https://github.com/illuminate/bus.git", - "reference": "ed8d93dd49d57887ccf82dbd284b80934288cbba" + "reference": "7845b735651ffb734b8b064e7d0349490adf4564" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/bus/zipball/ed8d93dd49d57887ccf82dbd284b80934288cbba", - "reference": "ed8d93dd49d57887ccf82dbd284b80934288cbba", + "url": "https://api.github.com/repos/illuminate/bus/zipball/7845b735651ffb734b8b064e7d0349490adf4564", + "reference": "7845b735651ffb734b8b064e7d0349490adf4564", "shasum": "" }, "require": { - "illuminate/collections": "^11.0", - "illuminate/contracts": "^11.0", - "illuminate/pipeline": "^11.0", - "illuminate/support": "^11.0", + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/pipeline": "^12.0", + "illuminate/support": "^12.0", "php": "^8.2" }, "suggest": { - "illuminate/queue": "Required to use closures when chaining jobs (^7.0)." + "illuminate/queue": "Required to use closures when chaining jobs (^12.0)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "12.x-dev" } }, "autoload": { @@ -1156,39 +1349,43 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-10-11T15:12:02+00:00" + "time": "2025-11-04T15:31:54+00:00" }, { "name": "illuminate/collections", - "version": "v11.30.0", + "version": "v12.40.2", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", - "reference": "2d99ccbb19e34450508ff3ab2f62ba90aa2e9793" + "reference": "3a794986bad4caf369d17ae19d4ef20a38dd8b0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/2d99ccbb19e34450508ff3ab2f62ba90aa2e9793", - "reference": "2d99ccbb19e34450508ff3ab2f62ba90aa2e9793", + "url": "https://api.github.com/repos/illuminate/collections/zipball/3a794986bad4caf369d17ae19d4ef20a38dd8b0c", + "reference": "3a794986bad4caf369d17ae19d4ef20a38dd8b0c", "shasum": "" }, "require": { - "illuminate/conditionable": "^11.0", - "illuminate/contracts": "^11.0", - "illuminate/macroable": "^11.0", - "php": "^8.2" + "illuminate/conditionable": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "php": "^8.2", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" }, "suggest": { - "symfony/var-dumper": "Required to use the dump method (^7.0)." + "illuminate/http": "Required to convert collections to API resources (^12.0).", + "symfony/var-dumper": "Required to use the dump method (^7.2)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "12.x-dev" } }, "autoload": { "files": [ + "functions.php", "helpers.php" ], "psr-4": { @@ -1211,29 +1408,29 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-10-10T19:23:07+00:00" + "time": "2025-11-24T14:13:52+00:00" }, { "name": "illuminate/conditionable", - "version": "v11.30.0", + "version": "v12.40.2", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", - "reference": "362dd761b9920367bca1427a902158225e9e3a23" + "reference": "ec677967c1f2faf90b8428919124d2184a4c9b49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/conditionable/zipball/362dd761b9920367bca1427a902158225e9e3a23", - "reference": "362dd761b9920367bca1427a902158225e9e3a23", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/ec677967c1f2faf90b8428919124d2184a4c9b49", + "reference": "ec677967c1f2faf90b8428919124d2184a4c9b49", "shasum": "" }, "require": { - "php": "^8.0.2" + "php": "^8.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "12.x-dev" } }, "autoload": { @@ -1257,34 +1454,44 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-06-28T20:10:30+00:00" + "time": "2025-05-13T15:08:45+00:00" }, { "name": "illuminate/container", - "version": "v11.30.0", + "version": "v12.40.2", "source": { "type": "git", "url": "https://github.com/illuminate/container.git", - "reference": "06dfc614aff58384b28ba5ad191f6a02d6b192cb" + "reference": "17ec6c2f741b11564420acc737dea9334d69988c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/container/zipball/06dfc614aff58384b28ba5ad191f6a02d6b192cb", - "reference": "06dfc614aff58384b28ba5ad191f6a02d6b192cb", + "url": "https://api.github.com/repos/illuminate/container/zipball/17ec6c2f741b11564420acc737dea9334d69988c", + "reference": "17ec6c2f741b11564420acc737dea9334d69988c", "shasum": "" }, "require": { - "illuminate/contracts": "^11.0", + "illuminate/contracts": "^12.0", "php": "^8.2", - "psr/container": "^1.1.1|^2.0.1" + "psr/container": "^1.1.1|^2.0.1", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" }, "provide": { "psr/container-implementation": "1.1|2.0" }, + "suggest": { + "illuminate/auth": "Required to use the Auth attribute", + "illuminate/cache": "Required to use the Cache attribute", + "illuminate/config": "Required to use the Config attribute", + "illuminate/database": "Required to use the DB attribute", + "illuminate/filesystem": "Required to use the Storage attribute", + "illuminate/log": "Required to use the Log or Context attributes" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "12.x-dev" } }, "autoload": { @@ -1308,20 +1515,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-10-11T15:30:11+00:00" + "time": "2025-11-14T15:29:05+00:00" }, { "name": "illuminate/contracts", - "version": "v11.30.0", + "version": "v12.40.2", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "56312862af937bd6da8e6dc8bbd88188dfb478f8" + "reference": "b97a94df448f196f23d646e21999bfd5d86ae23b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/56312862af937bd6da8e6dc8bbd88188dfb478f8", - "reference": "56312862af937bd6da8e6dc8bbd88188dfb478f8", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/b97a94df448f196f23d646e21999bfd5d86ae23b", + "reference": "b97a94df448f196f23d646e21999bfd5d86ae23b", "shasum": "" }, "require": { @@ -1332,7 +1539,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "12.x-dev" } }, "autoload": { @@ -1356,46 +1563,48 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-09-22T15:08:08+00:00" + "time": "2025-11-26T16:51:20+00:00" }, { "name": "illuminate/database", - "version": "v11.30.0", + "version": "v12.40.2", "source": { "type": "git", "url": "https://github.com/illuminate/database.git", - "reference": "29500e97a251419a6e42aeebe4c07ccb174af5b3" + "reference": "0f67c294e46ab2a836adf9a55ab47b05c9d614a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/database/zipball/29500e97a251419a6e42aeebe4c07ccb174af5b3", - "reference": "29500e97a251419a6e42aeebe4c07ccb174af5b3", + "url": "https://api.github.com/repos/illuminate/database/zipball/0f67c294e46ab2a836adf9a55ab47b05c9d614a6", + "reference": "0f67c294e46ab2a836adf9a55ab47b05c9d614a6", "shasum": "" }, "require": { - "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", + "brick/math": "^0.11|^0.12|^0.13|^0.14", "ext-pdo": "*", - "illuminate/collections": "^11.0", - "illuminate/container": "^11.0", - "illuminate/contracts": "^11.0", - "illuminate/macroable": "^11.0", - "illuminate/support": "^11.0", - "php": "^8.2" + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "laravel/serializable-closure": "^1.3|^2.0", + "php": "^8.2", + "symfony/polyfill-php85": "^1.33" }, "suggest": { "ext-filter": "Required to use the Postgres database driver.", - "fakerphp/faker": "Required to use the eloquent factory builder (^1.21).", - "illuminate/console": "Required to use the database commands (^11.0).", - "illuminate/events": "Required to use the observers with Eloquent (^11.0).", - "illuminate/filesystem": "Required to use the migrations (^11.0).", - "illuminate/pagination": "Required to paginate the result set (^11.0).", - "laravel/serializable-closure": "Required to handle circular references in model serialization (^1.3).", - "symfony/finder": "Required to use Eloquent model factories (^7.0)." + "fakerphp/faker": "Required to use the eloquent factory builder (^1.24).", + "illuminate/console": "Required to use the database commands (^12.0).", + "illuminate/events": "Required to use the observers with Eloquent (^12.0).", + "illuminate/filesystem": "Required to use the migrations (^12.0).", + "illuminate/http": "Required to convert Eloquent models to API resources (^12.0).", + "illuminate/pagination": "Required to paginate the result set (^12.0).", + "symfony/finder": "Required to use Eloquent model factories (^7.2)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "12.x-dev" } }, "autoload": { @@ -1425,35 +1634,35 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-10-29T20:30:27+00:00" + "time": "2025-11-26T14:35:45+00:00" }, { "name": "illuminate/events", - "version": "v11.30.0", + "version": "v12.40.2", "source": { "type": "git", "url": "https://github.com/illuminate/events.git", - "reference": "cfd8a636234cc5b5f736f2987f33b0d471d974b3" + "reference": "e0de667c68040d59a6ffc09e914536a1186870f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/events/zipball/cfd8a636234cc5b5f736f2987f33b0d471d974b3", - "reference": "cfd8a636234cc5b5f736f2987f33b0d471d974b3", + "url": "https://api.github.com/repos/illuminate/events/zipball/e0de667c68040d59a6ffc09e914536a1186870f0", + "reference": "e0de667c68040d59a6ffc09e914536a1186870f0", "shasum": "" }, "require": { - "illuminate/bus": "^11.0", - "illuminate/collections": "^11.0", - "illuminate/container": "^11.0", - "illuminate/contracts": "^11.0", - "illuminate/macroable": "^11.0", - "illuminate/support": "^11.0", + "illuminate/bus": "^12.0", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", "php": "^8.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "12.x-dev" } }, "autoload": { @@ -1480,47 +1689,47 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-08-07T14:43:54+00:00" + "time": "2025-10-21T15:10:34+00:00" }, { "name": "illuminate/filesystem", - "version": "v11.30.0", + "version": "v12.40.2", "source": { "type": "git", "url": "https://github.com/illuminate/filesystem.git", - "reference": "ce7013a350fb06bc65e8a2cf15fd2015f49e476d" + "reference": "b1fbb20010e868f838feac86aeac8ba439fca10d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/filesystem/zipball/ce7013a350fb06bc65e8a2cf15fd2015f49e476d", - "reference": "ce7013a350fb06bc65e8a2cf15fd2015f49e476d", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/b1fbb20010e868f838feac86aeac8ba439fca10d", + "reference": "b1fbb20010e868f838feac86aeac8ba439fca10d", "shasum": "" }, "require": { - "illuminate/collections": "^11.0", - "illuminate/contracts": "^11.0", - "illuminate/macroable": "^11.0", - "illuminate/support": "^11.0", + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", "php": "^8.2", - "symfony/finder": "^7.0" + "symfony/finder": "^7.2.0" }, "suggest": { "ext-fileinfo": "Required to use the Filesystem class.", "ext-ftp": "Required to use the Flysystem FTP driver.", "ext-hash": "Required to use the Filesystem class.", - "illuminate/http": "Required for handling uploaded files (^7.0).", - "league/flysystem": "Required to use the Flysystem local driver (^3.0.16).", - "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.0).", - "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.0).", - "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.0).", + "illuminate/http": "Required for handling uploaded files (^12.0).", + "league/flysystem": "Required to use the Flysystem local driver (^3.25.1).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).", - "symfony/mime": "Required to enable support for guessing extensions (^7.0)." + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/mime": "Required to enable support for guessing extensions (^7.2)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "12.x-dev" } }, "autoload": { @@ -1547,20 +1756,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-09-22T15:10:50+00:00" + "time": "2025-10-29T15:59:33+00:00" }, { "name": "illuminate/macroable", - "version": "v11.30.0", + "version": "v12.40.2", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", - "reference": "e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed" + "reference": "e862e5648ee34004fa56046b746f490dfa86c613" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/macroable/zipball/e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed", - "reference": "e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/e862e5648ee34004fa56046b746f490dfa86c613", + "reference": "e862e5648ee34004fa56046b746f490dfa86c613", "shasum": "" }, "require": { @@ -1569,7 +1778,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "12.x-dev" } }, "autoload": { @@ -1593,31 +1802,85 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-06-28T20:10:30+00:00" + "time": "2024-07-23T16:31:01+00:00" + }, + { + "name": "illuminate/pagination", + "version": "v12.41.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/pagination.git", + "reference": "87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/pagination/zipball/87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b", + "reference": "87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Pagination\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Pagination package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-11-16T14:36:17+00:00" }, { "name": "illuminate/pipeline", - "version": "v11.30.0", + "version": "v12.40.2", "source": { "type": "git", "url": "https://github.com/illuminate/pipeline.git", - "reference": "b359be74adc3ba4a637ca01c3645a26724a4c8a0" + "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/pipeline/zipball/b359be74adc3ba4a637ca01c3645a26724a4c8a0", - "reference": "b359be74adc3ba4a637ca01c3645a26724a4c8a0", + "url": "https://api.github.com/repos/illuminate/pipeline/zipball/b6a14c20d69a44bf0a6fba664a00d23ca71770ee", + "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee", "shasum": "" }, "require": { - "illuminate/contracts": "^11.0", - "illuminate/support": "^11.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", "php": "^8.2" }, + "suggest": { + "illuminate/database": "Required to use database transactions (^12.0)." + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "12.x-dev" } }, "autoload": { @@ -1641,20 +1904,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-10-18T13:11:08+00:00" + "time": "2025-08-20T13:36:50+00:00" }, { "name": "illuminate/support", - "version": "v11.30.0", + "version": "v12.40.2", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "69453485fa4c76589b5a1a98ebef0e8fee749220" + "reference": "20a64e34d9ee8bb7b28b242155e9c31f86e5804b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/69453485fa4c76589b5a1a98ebef0e8fee749220", - "reference": "69453485fa4c76589b5a1a98ebef0e8fee749220", + "url": "https://api.github.com/repos/illuminate/support/zipball/20a64e34d9ee8bb7b28b242155e9c31f86e5804b", + "reference": "20a64e34d9ee8bb7b28b242155e9c31f86e5804b", "shasum": "" }, "require": { @@ -1662,13 +1925,15 @@ "ext-ctype": "*", "ext-filter": "*", "ext-mbstring": "*", - "illuminate/collections": "^11.0", - "illuminate/conditionable": "^11.0", - "illuminate/contracts": "^11.0", - "illuminate/macroable": "^11.0", - "nesbot/carbon": "^2.72.2|^3.0", + "illuminate/collections": "^12.0", + "illuminate/conditionable": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "nesbot/carbon": "^3.8.4", "php": "^8.2", - "voku/portable-ascii": "^2.0" + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33", + "voku/portable-ascii": "^2.0.2" }, "conflict": { "tightenco/collect": "<5.5.33" @@ -1677,19 +1942,20 @@ "spatie/once": "*" }, "suggest": { - "illuminate/filesystem": "Required to use the composer class (^11.0).", - "laravel/serializable-closure": "Required to use the once function (^1.3).", - "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.0.2).", + "illuminate/filesystem": "Required to use the Composer class (^12.0).", + "laravel/serializable-closure": "Required to use the once function (^1.3|^2.0).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.7).", + "league/uri": "Required to use the Uri class (^7.5.1).", "ramsey/uuid": "Required to use Str::uuid() (^4.7).", - "symfony/process": "Required to use the composer class (^7.0).", - "symfony/uid": "Required to use Str::ulid() (^7.0).", - "symfony/var-dumper": "Required to use the dd function (^7.0).", - "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.4.1)." + "symfony/process": "Required to use the Composer class (^7.2).", + "symfony/uid": "Required to use Str::ulid() (^7.2).", + "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)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "12.x-dev" } }, "autoload": { @@ -1717,37 +1983,37 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-10-29T20:21:52+00:00" + "time": "2025-11-26T14:47:26+00:00" }, { "name": "illuminate/view", - "version": "v11.30.0", + "version": "v12.40.2", "source": { "type": "git", "url": "https://github.com/illuminate/view.git", - "reference": "b0ca05925b882b7887f9c2591497a1b2835f9144" + "reference": "f065c5fc1ad29aaf5734c5f99f69fc4cde9a255f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/view/zipball/b0ca05925b882b7887f9c2591497a1b2835f9144", - "reference": "b0ca05925b882b7887f9c2591497a1b2835f9144", + "url": "https://api.github.com/repos/illuminate/view/zipball/f065c5fc1ad29aaf5734c5f99f69fc4cde9a255f", + "reference": "f065c5fc1ad29aaf5734c5f99f69fc4cde9a255f", "shasum": "" }, "require": { "ext-tokenizer": "*", - "illuminate/collections": "^11.0", - "illuminate/container": "^11.0", - "illuminate/contracts": "^11.0", - "illuminate/events": "^11.0", - "illuminate/filesystem": "^11.0", - "illuminate/macroable": "^11.0", - "illuminate/support": "^11.0", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/events": "^12.0", + "illuminate/filesystem": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", "php": "^8.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "12.x-dev" } }, "autoload": { @@ -1771,34 +2037,44 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-10-24T14:26:35+00:00" + "time": "2025-11-16T14:42:54+00:00" }, { "name": "justinrainbow/json-schema", - "version": "5.3.0", + "version": "6.6.2", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" + "reference": "3c25fe750c1599716ef26aa997f7c026cee8c4b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/3c25fe750c1599716ef26aa997f7c026cee8c4b7", + "reference": "3c25fe750c1599716ef26aa997f7c026cee8c4b7", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", - "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "^23.2", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" }, "bin": [ "bin/validate-json" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, "autoload": { "psr-4": { "JsonSchema\\": "src/JsonSchema/" @@ -1827,45 +2103,45 @@ } ], "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", + "homepage": "https://github.com/jsonrainbow/json-schema", "keywords": [ "json", "schema" ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.2" }, - "time": "2024-07-06T21:00:26+00:00" + "time": "2025-11-28T15:24:03+00:00" }, { "name": "laravel/serializable-closure", - "version": "v1.3.5", + "version": "v2.0.7", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c" + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c", - "reference": "1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd", + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd", "shasum": "" }, "require": { - "php": "^7.3|^8.0" + "php": "^8.1" }, "require-dev": { - "illuminate/support": "^8.0|^9.0|^10.0|^11.0", - "nesbot/carbon": "^2.61|^3.0", - "pestphp/pest": "^1.21.3", - "phpstan/phpstan": "^1.8.2", - "symfony/var-dumper": "^5.4.11|^6.2.0|^7.0.0" + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -1897,39 +2173,223 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2024-09-23T13:33:08+00:00" + "time": "2025-11-21T20:52:36+00:00" }, { - "name": "league/oauth2-client", - "version": "2.7.0", + "name": "league/commonmark", + "version": "2.8.0", "source": { "type": "git", - "url": "https://github.com/thephpleague/oauth2-client.git", - "reference": "160d6274b03562ebeb55ed18399281d8118b76c8" + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/160d6274b03562ebeb55ed18399281d8118b76c8", - "reference": "160d6274b03562ebeb55ed18399281d8118b76c8", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/4efa10c1e56488e658d10adf7b7b7dcd19940bfb", + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb", "shasum": "" }, "require": { - "guzzlehttp/guzzle": "^6.0 || ^7.0", - "paragonie/random_compat": "^1 || ^2 || ^9.99", - "php": "^5.6 || ^7.0 || ^8.0" + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" }, "require-dev": { - "mockery/mockery": "^1.3.5", - "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5", - "squizlabs/php_codesniffer": "^2.3 || ^3.0" + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" }, "type": "library", "extra": { "branch-alias": { - "dev-2.x": "2.0.x-dev" + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2025-11-26T21:48:24+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/oauth2-client", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "26e8c5da4f3d78cede7021e09b1330a0fc093d5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/26e8c5da4f3d78cede7021e09b1330a0fc093d5e", + "reference": "26e8c5da4f3d78cede7021e09b1330a0fc093d5e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "php": "^7.1 || >=8.0.0 <8.6.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.11" }, + "type": "library", "autoload": { "psr-4": { "League\\OAuth2\\Client\\": "src/" @@ -1965,9 +2425,9 @@ ], "support": { "issues": "https://github.com/thephpleague/oauth2-client/issues", - "source": "https://github.com/thephpleague/oauth2-client/tree/2.7.0" + "source": "https://github.com/thephpleague/oauth2-client/tree/2.9.0" }, - "time": "2023-04-16T18:19:15+00:00" + "time": "2025-11-25T22:17:17+00:00" }, { "name": "league/openapi-psr7-validator", @@ -2033,33 +2493,38 @@ }, { "name": "league/uri", - "version": "7.4.1", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "bedb6e55eff0c933668addaa7efa1e1f2c417cc4" + "reference": "f625804987a0a9112d954f9209d91fec52182344" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/bedb6e55eff0c933668addaa7efa1e1f2c417cc4", - "reference": "bedb6e55eff0c933668addaa7efa1e1f2c417cc4", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344", + "reference": "f625804987a0a9112d954f9209d91fec52182344", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.3", - "php": "^8.1" + "league/uri-interfaces": "^7.6", + "php": "^8.1", + "psr/http-factory": "^1" }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", "league/uri-components": "Needed to easily manipulate URI objects components", + "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2087,6 +2552,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -2099,9 +2565,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -2111,7 +2579,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.4.1" + "source": "https://github.com/thephpleague/uri/tree/7.6.0" }, "funding": [ { @@ -2119,26 +2587,25 @@ "type": "github" } ], - "time": "2024-03-23T07:42:40+00:00" + "time": "2025-11-18T12:17:23+00:00" }, { "name": "league/uri-interfaces", - "version": "7.4.1", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "8d43ef5c841032c87e2de015972c06f3865ef718" + "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/8d43ef5c841032c87e2de015972c06f3865ef718", - "reference": "8d43ef5c841032c87e2de015972c06f3865ef718", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ccbfb51c0445298e7e0b7f4481b942f589665368", + "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -2146,6 +2613,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2170,7 +2638,7 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interfaces and classes for URI representation and interaction", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", "homepage": "https://uri.thephpleague.com", "keywords": [ "data-uri", @@ -2195,7 +2663,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.4.1" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.6.0" }, "funding": [ { @@ -2203,20 +2671,93 @@ "type": "github" } ], - "time": "2024-03-23T07:42:40+00:00" + "time": "2025-11-18T12:17:23+00:00" + }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + }, + "time": "2025-09-14T11:18:39+00:00" }, { "name": "nesbot/carbon", - "version": "3.8.2", + "version": "3.10.3", "source": { "type": "git", - "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947" + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e1268cdbc486d97ce23fef2c666dc3c6b6de9947", - "reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", "shasum": "" }, "require": { @@ -2224,9 +2765,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3 || ^7.0", + "symfony/clock": "^6.3.12 || ^7.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -2234,24 +2775,19 @@ "require-dev": { "doctrine/dbal": "^3.6.3 || ^4.0", "doctrine/orm": "^2.15.2 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.57.2", + "friendsofphp/php-cs-fixer": "^v3.87.1", "kylekatarnls/multi-tester": "^2.5.3", - "ondrejmirtes/better-reflection": "^6.25.0.4", "phpmd/phpmd": "^2.15.0", - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.11.2", - "phpunit/phpunit": "^10.5.20", - "squizlabs/php_codesniffer": "^3.9.0" + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4" }, "bin": [ "bin/carbon" ], "type": "library", "extra": { - "branch-alias": { - "dev-master": "3.x-dev", - "dev-2.x": "2.x-dev" - }, "laravel": { "providers": [ "Carbon\\Laravel\\ServiceProvider" @@ -2261,6 +2797,10 @@ "includes": [ "extension.neon" ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" } }, "autoload": { @@ -2292,8 +2832,8 @@ ], "support": { "docs": "https://carbon.nesbot.com/docs", - "issues": "https://github.com/briannesbitt/Carbon/issues", - "source": "https://github.com/briannesbitt/Carbon" + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" }, "funding": [ { @@ -2309,7 +2849,161 @@ "type": "tidelift" } ], - "time": "2024-11-07T17:46:48+00:00" + "time": "2025-09-06T13:39:36+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.3", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/2befc2f42d7c715fd9d95efc31b1081e5d765004", + "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/tester": "^2.5.2", + "phpstan/phpstan-nette": "^2.0@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.3" + }, + "time": "2025-10-30T22:57:59+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", + "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/tester": "^2.5", + "phpstan/phpstan-nette": "^2.0@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.0" + }, + "time": "2025-12-01T17:49:23+00:00" }, { "name": "nikic/fast-route", @@ -2439,68 +3133,18 @@ ], "time": "2024-09-09T07:06:30+00:00" }, - { - "name": "paragonie/random_compat", - "version": "v9.99.100", - "source": { - "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", - "shasum": "" - }, - "require": { - "php": ">= 7" - }, - "require-dev": { - "phpunit/phpunit": "4.*|5.*", - "vimeo/psalm": "^1" - }, - "suggest": { - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com" - } - ], - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", - "keywords": [ - "csprng", - "polyfill", - "pseudorandom", - "random" - ], - "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/random_compat/issues", - "source": "https://github.com/paragonie/random_compat" - }, - "time": "2020-10-15T08:29:30+00:00" - }, { "name": "phpoption/phpoption", - "version": "1.9.3", + "version": "1.9.4", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", "shasum": "" }, "require": { @@ -2508,7 +3152,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" }, "type": "library", "extra": { @@ -2550,7 +3194,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" }, "funding": [ { @@ -2562,7 +3206,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:41:07+00:00" + "time": "2025-08-21T11:53:16+00:00" }, { "name": "psr/cache", @@ -3184,44 +3828,43 @@ }, { "name": "rcrowe/twigbridge", - "version": "dev-master", + "version": "v0.14.6", "source": { "type": "git", "url": "https://github.com/rcrowe/TwigBridge.git", - "reference": "418de7c4fbfa67678802093a8c3e1c319d6ad3fc" + "reference": "0798ee4b5e5b943d0200850acaa87ccd82e2fe45" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/418de7c4fbfa67678802093a8c3e1c319d6ad3fc", - "reference": "418de7c4fbfa67678802093a8c3e1c319d6ad3fc", + "url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/0798ee4b5e5b943d0200850acaa87ccd82e2fe45", + "reference": "0798ee4b5e5b943d0200850acaa87ccd82e2fe45", "shasum": "" }, "require": { - "illuminate/support": "^9|^10|^11", - "illuminate/view": "^9|^10|^11", + "illuminate/support": "^9|^10|^11|^12", + "illuminate/view": "^9|^10|^11|^12", "php": "^8.1", - "twig/twig": "~3.12" + "twig/twig": "~3.21" }, "require-dev": { "ext-json": "*", - "laravel/framework": "^9|^10|^11", + "laravel/framework": "^9|^10|^11|^12", "mockery/mockery": "^1.3.1", - "phpunit/phpunit": "^8.5.8 || ^9.3.7", + "phpunit/phpunit": "^8.5.8 || ^9.3.7 || ^10.0 || ^11.0 || ^12.0", "squizlabs/php_codesniffer": "^3.6" }, - "default-branch": true, "type": "library", "extra": { - "branch-alias": { - "dev-master": "0.14-dev" - }, "laravel": { - "providers": [ - "TwigBridge\\ServiceProvider" - ], "aliases": { "Twig": "TwigBridge\\Facade\\Twig" - } + }, + "providers": [ + "TwigBridge\\ServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "0.14-dev" } }, "autoload": { @@ -3250,50 +3893,105 @@ "twig" ], "support": { - "issues": "https://github.com/rcrowe/TwigBridge/issues", - "source": "https://github.com/rcrowe/TwigBridge/tree/master" + "issues": "https://github.com/rcrowe/TwigBridge/issues", + "source": "https://github.com/rcrowe/TwigBridge/tree/v0.14.6" + }, + "time": "2025-08-20T11:25:49+00:00" + }, + { + "name": "respect/stringifier", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/Respect/Stringifier.git", + "reference": "e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Respect/Stringifier/zipball/e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59", + "reference": "e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.8", + "malukenho/docheader": "^0.1.7", + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "autoload": { + "files": [ + "src/stringify.php" + ], + "psr-4": { + "Respect\\Stringifier\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Respect/Stringifier Contributors", + "homepage": "https://github.com/Respect/Stringifier/graphs/contributors" + } + ], + "description": "Converts any value to a string", + "homepage": "http://respect.github.io/Stringifier/", + "keywords": [ + "respect", + "stringifier", + "stringify" + ], + "support": { + "issues": "https://github.com/Respect/Stringifier/issues", + "source": "https://github.com/Respect/Stringifier/tree/0.2.0" }, - "time": "2024-09-15T13:48:49+00:00" + "time": "2017-12-29T19:39:25+00:00" }, { "name": "respect/validation", - "version": "1.1.31", + "version": "2.4.4", "source": { "type": "git", "url": "https://github.com/Respect/Validation.git", - "reference": "45d109fc830644fecc1145200d6351ce4f2769d0" + "reference": "f13f10f19978aea33af2a102a2f58f2db1e63619" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Respect/Validation/zipball/45d109fc830644fecc1145200d6351ce4f2769d0", - "reference": "45d109fc830644fecc1145200d6351ce4f2769d0", + "url": "https://api.github.com/repos/Respect/Validation/zipball/f13f10f19978aea33af2a102a2f58f2db1e63619", + "reference": "f13f10f19978aea33af2a102a2f58f2db1e63619", "shasum": "" }, "require": { - "php": ">=5.4", + "php": ">=8.1", + "respect/stringifier": "^0.2.0", "symfony/polyfill-mbstring": "^1.2" }, "require-dev": { - "egulias/email-validator": "~1.2 || ~2.1", - "mikey179/vfsstream": "^1.5", - "phpunit/phpunit": "~4.0 || ~5.0", - "symfony/validator": "~2.6.9", - "zendframework/zend-validator": "~2.3" + "egulias/email-validator": "^3.0", + "giggsey/libphonenumber-for-php-lite": "^8.13 || ^9.0", + "malukenho/docheader": "^1.0", + "mikey179/vfsstream": "^1.6", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.6", + "psr/http-message": "^1.0", + "respect/coding-standard": "^4.0", + "squizlabs/php_codesniffer": "^3.7" }, "suggest": { - "egulias/email-validator": "Strict (RFC compliant) email validation", + "egulias/email-validator": "Improves the Email rule if available", "ext-bcmath": "Arbitrary Precision Mathematics", + "ext-fileinfo": "File Information", "ext-mbstring": "Multibyte String Functions", - "friendsofphp/php-cs-fixer": "Fix PSR2 and other coding style issues", - "symfony/validator": "Use Symfony validator through Respect\\Validation", - "zendframework/zend-validator": "Use Zend Framework validator through Respect\\Validation" + "giggsey/libphonenumber-for-php-lite": "Enables the phone rule if available" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, "autoload": { "psr-4": { "Respect\\Validation\\": "library/" @@ -3301,7 +3999,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { @@ -3318,22 +4016,22 @@ ], "support": { "issues": "https://github.com/Respect/Validation/issues", - "source": "https://github.com/Respect/Validation/tree/1.1.31" + "source": "https://github.com/Respect/Validation/tree/2.4.4" }, - "time": "2019-05-28T06:10:06+00:00" + "time": "2025-06-07T00:07:21+00:00" }, { "name": "riverline/multipart-parser", - "version": "2.1.2", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/Riverline/multipart-parser.git", - "reference": "7a9f4646db5181516c61b8e0225a343189beedcd" + "reference": "1410f23a8fd416a0cf5c8867ea9c95544016c831" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Riverline/multipart-parser/zipball/7a9f4646db5181516c61b8e0225a343189beedcd", - "reference": "7a9f4646db5181516c61b8e0225a343189beedcd", + "url": "https://api.github.com/repos/Riverline/multipart-parser/zipball/1410f23a8fd416a0cf5c8867ea9c95544016c831", + "reference": "1410f23a8fd416a0cf5c8867ea9c95544016c831", "shasum": "" }, "require": { @@ -3374,22 +4072,22 @@ ], "support": { "issues": "https://github.com/Riverline/multipart-parser/issues", - "source": "https://github.com/Riverline/multipart-parser/tree/2.1.2" + "source": "https://github.com/Riverline/multipart-parser/tree/2.2.0" }, - "time": "2024-03-12T16:46:05+00:00" + "time": "2025-04-29T08:38:14+00:00" }, { "name": "symfony/clock", - "version": "v7.1.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "97bebc53548684c17ed696bc8af016880f0f098d" + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/97bebc53548684c17ed696bc8af016880f0f098d", - "reference": "97bebc53548684c17ed696bc8af016880f0f098d", + "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110", "shasum": "" }, "require": { @@ -3434,7 +4132,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.1.6" + "source": "https://github.com/symfony/clock/tree/v7.4.0" }, "funding": [ { @@ -3445,25 +4143,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-11-12T15:39:26+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -3471,12 +4173,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -3501,7 +4203,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -3517,20 +4219,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.1.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "87254c78dd50721cfd015b62277a8281c5589702" + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/87254c78dd50721cfd015b62277a8281c5589702", - "reference": "87254c78dd50721cfd015b62277a8281c5589702", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d", + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d", "shasum": "" }, "require": { @@ -3547,13 +4249,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3581,7 +4284,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.1.6" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.0" }, "funding": [ { @@ -3592,25 +4295,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-10-28T09:38:46+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { @@ -3619,12 +4326,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -3657,7 +4364,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -3673,27 +4380,27 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/finder", - "version": "v7.1.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8" + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2cb89664897be33f78c65d3d2845954c8d7a43b8", - "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8", + "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3721,7 +4428,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.1.6" + "source": "https://github.com/symfony/finder/tree/v7.4.0" }, "funding": [ { @@ -3732,45 +4439,50 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-01T08:31:23+00:00" + "time": "2025-11-05T05:42:40+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.1.7", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "5183b61657807099d98f3367bcccb850238b17a9" + "reference": "769c1720b68e964b13b58529c17d4a385c62167b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5183b61657807099d98f3367bcccb850238b17a9", - "reference": "5183b61657807099d98f3367bcccb850238b17a9", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/769c1720b68e964b13b58529c17d4a385c62167b", + "reference": "769c1720b68e964b13b58529c17d4a385c62167b", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php83": "^1.27" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" }, "conflict": { "doctrine/dbal": "<3.6", - "symfony/cache": "<6.4" + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" }, "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0" + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3798,7 +4510,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.1.7" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.0" }, "funding": [ { @@ -3809,25 +4521,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-06T09:02:46+00:00" + "time": "2025-11-13T08:49:24+00:00" }, { "name": "symfony/mailer", - "version": "v7.1.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "69c9948451fb3a6a4d47dc8261d1794734e76cdd" + "reference": "a3d9eea8cfa467ece41f0f54ba28185d74bd53fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/69c9948451fb3a6a4d47dc8261d1794734e76cdd", - "reference": "69c9948451fb3a6a4d47dc8261d1794734e76cdd", + "url": "https://api.github.com/repos/symfony/mailer/zipball/a3d9eea8cfa467ece41f0f54ba28185d74bd53fd", + "reference": "a3d9eea8cfa467ece41f0f54ba28185d74bd53fd", "shasum": "" }, "require": { @@ -3835,8 +4551,8 @@ "php": ">=8.2", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -3847,10 +4563,10 @@ "symfony/twig-bridge": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/twig-bridge": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3878,7 +4594,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.1.6" + "source": "https://github.com/symfony/mailer/tree/v7.4.0" }, "funding": [ { @@ -3889,29 +4605,34 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-11-21T15:26:00+00:00" }, { "name": "symfony/mime", - "version": "v7.1.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "caa1e521edb2650b8470918dfe51708c237f0598" + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/caa1e521edb2650b8470918dfe51708c237f0598", - "reference": "caa1e521edb2650b8470918dfe51708c237f0598", + "url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a", + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -3926,11 +4647,11 @@ "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3" + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" }, "type": "library", "autoload": { @@ -3962,7 +4683,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.1.6" + "source": "https://github.com/symfony/mime/tree/v7.4.0" }, "funding": [ { @@ -3973,16 +4694,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:11:02+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -4006,8 +4731,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4041,7 +4766,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -4052,6 +4777,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -4061,16 +4790,16 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", "shasum": "" }, "require": { @@ -4083,8 +4812,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4124,7 +4853,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" }, "funding": [ { @@ -4135,16 +4864,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-09-10T14:38:51+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -4165,8 +4898,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4205,7 +4938,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -4216,6 +4949,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -4225,19 +4962,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -4249,8 +4987,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4285,7 +5023,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -4296,25 +5034,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -4323,8 +5065,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4365,7 +5107,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -4376,25 +5118,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { - "name": "symfony/polyfill-php81", - "version": "v1.31.0", + "name": "symfony/polyfill-php83", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -4403,8 +5149,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4412,7 +5158,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" + "Symfony\\Polyfill\\Php83\\": "" }, "classmap": [ "Resources/stubs" @@ -4432,7 +5178,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -4441,7 +5187,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -4452,25 +5198,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { - "name": "symfony/polyfill-php83", - "version": "v1.31.0", + "name": "symfony/polyfill-php84", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", "shasum": "" }, "require": { @@ -4479,8 +5229,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4488,7 +5238,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php83\\": "" + "Symfony\\Polyfill\\Php84\\": "" }, "classmap": [ "Resources/stubs" @@ -4508,7 +5258,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -4517,7 +5267,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" }, "funding": [ { @@ -4528,31 +5278,115 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v7.1.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "f16471bb19f6685b9ccf0a2c03c213840ae68cd6" + "reference": "0101ff8bd0506703b045b1670960302d302a726c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/f16471bb19f6685b9ccf0a2c03c213840ae68cd6", - "reference": "f16471bb19f6685b9ccf0a2c03c213840ae68cd6", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/0101ff8bd0506703b045b1670960302d302a726c", + "reference": "0101ff8bd0506703b045b1670960302d302a726c", "shasum": "" }, "require": { "php": ">=8.2", "psr/http-message": "^1.0|^2.0", - "symfony/http-foundation": "^6.4|^7.0" + "symfony/http-foundation": "^6.4|^7.0|^8.0" }, "conflict": { "php-http/discovery": "<1.15", @@ -4562,11 +5396,12 @@ "nyholm/psr7": "^1.1", "php-http/discovery": "^1.15", "psr/log": "^1.1.4|^2|^3", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0" + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0" }, "type": "symfony-bridge", "autoload": { @@ -4600,7 +5435,7 @@ "psr-7" ], "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.1.6" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.4.0" }, "funding": [ { @@ -4611,25 +5446,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-11-13T08:38:49+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -4642,12 +5481,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -4683,7 +5522,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -4694,33 +5533,39 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/translation", - "version": "v7.1.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "b9f72ab14efdb6b772f85041fa12f820dee8d55f" + "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/b9f72ab14efdb6b772f85041fa12f820dee8d55f", - "reference": "b9f72ab14efdb6b772f85041fa12f820dee8d55f", + "url": "https://api.github.com/repos/symfony/translation/zipball/2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", + "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "symfony/translation-contracts": "^2.5.3|^3.3" }, "conflict": { + "nikic/php-parser": "<5.0", "symfony/config": "<6.4", "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", @@ -4734,19 +5579,19 @@ "symfony/translation-implementation": "2.3|3.0" }, "require-dev": { - "nikic/php-parser": "^4.18|^5.0", + "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4777,7 +5622,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.1.6" + "source": "https://github.com/symfony/translation/tree/v7.4.0" }, "funding": [ { @@ -4788,25 +5633,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-28T12:35:13+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.5.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a" + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", - "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", "shasum": "" }, "require": { @@ -4814,12 +5663,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -4855,7 +5704,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" }, "funding": [ { @@ -4866,36 +5715,41 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/yaml", - "version": "v7.1.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "3ced3f29e4f0d6bce2170ff26719f1fe9aacc671" + "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/3ced3f29e4f0d6bce2170ff26719f1fe9aacc671", - "reference": "3ced3f29e4f0d6bce2170ff26719f1fe9aacc671", + "url": "https://api.github.com/repos/symfony/yaml/zipball/6c84a4b55aee4cd02034d1c528e83f69ddf63810", + "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -4926,7 +5780,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.1.6" + "source": "https://github.com/symfony/yaml/tree/v7.4.0" }, "funding": [ { @@ -4937,35 +5791,39 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "twig/twig", - "version": "v3.14.2", + "version": "v3.22.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a" + "reference": "1de2ec1fc43ab58a4b7e80b214b96bfc895750f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a", - "reference": "0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/1de2ec1fc43ab58a4b7e80b214b96bfc895750f3", + "reference": "1de2ec1fc43ab58a4b7e80b214b96bfc895750f3", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php81": "^1.29" + "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { + "phpstan/phpstan": "^2.0", "psr/container": "^1.0|^2.0", "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, @@ -5009,7 +5867,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.14.2" + "source": "https://github.com/twigphp/Twig/tree/v3.22.1" }, "funding": [ { @@ -5021,20 +5879,20 @@ "type": "tidelift" } ], - "time": "2024-11-07T12:36:22+00:00" + "time": "2025-11-16T16:01:12+00:00" }, { "name": "vlucas/phpdotenv", - "version": "v5.6.1", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2" + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2", - "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", "shasum": "" }, "require": { @@ -5093,7 +5951,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" }, "funding": [ { @@ -5105,20 +5963,20 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:52:34+00:00" + "time": "2025-04-30T23:37:27+00:00" }, { "name": "voku/portable-ascii", - "version": "2.0.1", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/voku/portable-ascii.git", - "reference": "b56450eed252f6801410d810c8e1727224ae0743" + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b56450eed252f6801410d810c8e1727224ae0743", - "reference": "b56450eed252f6801410d810c8e1727224ae0743", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", "shasum": "" }, "require": { @@ -5143,7 +6001,7 @@ "authors": [ { "name": "Lars Moelleken", - "homepage": "http://www.moelleken.org/" + "homepage": "https://www.moelleken.org/" } ], "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", @@ -5155,7 +6013,7 @@ ], "support": { "issues": "https://github.com/voku/portable-ascii/issues", - "source": "https://github.com/voku/portable-ascii/tree/2.0.1" + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" }, "funding": [ { @@ -5179,32 +6037,32 @@ "type": "tidelift" } ], - "time": "2022-03-08T17:03:00+00:00" + "time": "2024-11-21T01:49:47+00:00" }, { "name": "webmozart/assert", - "version": "1.11.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", "shasum": "" }, "require": { "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", "php": "^7.2 || ^8.0" }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" }, "type": "library", "extra": { @@ -5235,37 +6093,37 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" + "source": "https://github.com/webmozarts/assert/tree/1.12.1" }, - "time": "2022-06-03T18:03:27+00:00" + "time": "2025-10-29T15:56:20+00:00" } ], "packages-dev": [ { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.0.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "4be43904336affa5c2f70744a348312336afd0da" + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", - "reference": "4be43904336affa5c2f70744a348312336afd0da", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", + "composer-plugin-api": "^2.2", "php": ">=5.4", - "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" }, "require-dev": { - "composer/composer": "*", + "composer/composer": "^2.2", "ext-json": "*", "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", "yoast/phpunit-polyfills": "^1.0" }, "type": "composer-plugin", @@ -5284,9 +6142,9 @@ "authors": [ { "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" }, { "name": "Contributors", @@ -5294,7 +6152,6 @@ } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", "keywords": [ "PHPCodeSniffer", "PHP_CodeSniffer", @@ -5315,9 +6172,28 @@ ], "support": { "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2023-01-05T11:28:13+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-11T04:32:07+00:00" }, { "name": "dms/phpunit-arraysubset-asserts", @@ -5435,16 +6311,16 @@ }, { "name": "fakerphp/faker", - "version": "v1.24.0", + "version": "v1.24.1", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "a136842a532bac9ecd8a1c723852b09915d7db50" + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/a136842a532bac9ecd8a1c723852b09915d7db50", - "reference": "a136842a532bac9ecd8a1c723852b09915d7db50", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", "shasum": "" }, "require": { @@ -5492,22 +6368,22 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.24.0" + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" }, - "time": "2024-11-07T15:11:20+00:00" + "time": "2024-11-21T13:46:39+00:00" }, { "name": "fig/log-test", - "version": "1.1.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/php-fig/log-test.git", - "reference": "02d6eaf8b09784b7adacf57a3c951bad95229a7f" + "reference": "83acb6c12875ea4f349dc4fe8b7d8e239c2b3715" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log-test/zipball/02d6eaf8b09784b7adacf57a3c951bad95229a7f", - "reference": "02d6eaf8b09784b7adacf57a3c951bad95229a7f", + "url": "https://api.github.com/repos/php-fig/log-test/zipball/83acb6c12875ea4f349dc4fe8b7d8e239c2b3715", + "reference": "83acb6c12875ea4f349dc4fe8b7d8e239c2b3715", "shasum": "" }, "require": { @@ -5515,7 +6391,7 @@ "psr/log": "^2.0 | ^3.0" }, "require-dev": { - "phpunit/phpunit": "^8.0 | ^9.0", + "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0 || ^11.0", "squizlabs/php_codesniffer": "^3.6" }, "type": "library", @@ -5536,24 +6412,27 @@ } ], "description": "Test utilities for the psr/log package that backs the PSR-3 specification.", + "keywords": [ + "testing" + ], "support": { "issues": "https://github.com/php-fig/log-test/issues", - "source": "https://github.com/php-fig/log-test/tree/1.1.0" + "source": "https://github.com/php-fig/log-test/tree/1.2.1" }, - "time": "2022-10-18T05:33:27+00:00" + "time": "2025-11-11T10:33:05+00:00" }, { "name": "filp/whoops", - "version": "2.16.0", + "version": "2.18.4", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "befcdc0e5dce67252aa6322d82424be928214fa2" + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/befcdc0e5dce67252aa6322d82424be928214fa2", - "reference": "befcdc0e5dce67252aa6322d82424be928214fa2", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", "shasum": "" }, "require": { @@ -5603,7 +6482,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.16.0" + "source": "https://github.com/filp/whoops/tree/2.18.4" }, "funding": [ { @@ -5611,20 +6490,20 @@ "type": "github" } ], - "time": "2024-09-25T12:00:00+00:00" + "time": "2025-08-08T12:00:00+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.12.1", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -5663,7 +6542,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -5671,20 +6550,20 @@ "type": "tidelift" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "3a454ca033b9e06b63282ce19562e892747449bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb", "shasum": "" }, "require": { @@ -5703,7 +6582,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -5727,9 +6606,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2025-10-21T19:32:17+00:00" }, { "name": "phar-io/manifest", @@ -5851,30 +6730,30 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.33.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140" + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/82a311fd3690fb2bf7b64d5c98f912b3dd746140", - "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { "doctrine/annotations": "^2.0", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^5.3.0", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.5", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", "symfony/process": "^5.2" }, "type": "library", @@ -5892,26 +6771,21 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.33.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" }, - "time": "2024-10-13T11:25:22+00:00" + "time": "2025-08-30T15:50:23+00:00" }, { "name": "phpstan/phpstan", - "version": "1.12.8", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "f6a60a4d66142b8156c9da923f1972657bc4748c" - }, + "version": "2.1.32", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f6a60a4d66142b8156c9da923f1972657bc4748c", - "reference": "f6a60a4d66142b8156c9da923f1972657bc4748c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227", + "reference": "e126cad1e30a99b137b8ed75a85a676450ebb227", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -5952,7 +6826,7 @@ "type": "github" } ], - "time": "2024-11-06T19:06:49+00:00" + "time": "2025-11-11T15:18:17+00:00" }, { "name": "phpunit/php-code-coverage", @@ -6275,16 +7149,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.21", + "version": "9.6.29", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", "shasum": "" }, "require": { @@ -6295,7 +7169,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -6306,11 +7180,11 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", + "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", "sebastian/type": "^3.2.1", @@ -6358,7 +7232,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" }, "funding": [ { @@ -6369,12 +7243,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2024-09-19T10:50:18+00:00" + "time": "2025-09-24T06:29:11+00:00" }, { "name": "sebastian/cli-parser", @@ -6545,16 +7427,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "4.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", "shasum": "" }, "require": { @@ -6607,15 +7489,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2025-08-10T06:51:50+00:00" }, { "name": "sebastian/complexity", @@ -6805,16 +7699,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -6870,28 +7764,40 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -6934,15 +7840,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -7115,16 +8033,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { @@ -7166,15 +8084,27 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", @@ -7341,32 +8271,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.15.0", + "version": "8.25.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "7d1d957421618a3803b593ec31ace470177d7817" + "reference": "4caa5ec5a30b84b2305e80159c710d437f40cc40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/7d1d957421618a3803b593ec31ace470177d7817", - "reference": "7d1d957421618a3803b593ec31ace470177d7817", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/4caa5ec5a30b84b2305e80159c710d437f40cc40", + "reference": "4caa5ec5a30b84b2305e80159c710d437f40cc40", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", - "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": "^1.23.1", - "squizlabs/php_codesniffer": "^3.9.0" + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.2.0", + "php": "^7.4 || ^8.0", + "phpstan/phpdoc-parser": "^2.3.0", + "squizlabs/php_codesniffer": "^4.0.1" }, "require-dev": { - "phing/phing": "2.17.4", - "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.10.60", - "phpstan/phpstan-deprecation-rules": "1.1.4", - "phpstan/phpstan-phpunit": "1.3.16", - "phpstan/phpstan-strict-rules": "1.5.2", - "phpunit/phpunit": "8.5.21|9.6.8|10.5.11" + "phing/phing": "3.0.1|3.1.0", + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/phpstan": "2.1.32", + "phpstan/phpstan-deprecation-rules": "2.0.3", + "phpstan/phpstan-phpunit": "2.0.8", + "phpstan/phpstan-strict-rules": "2.0.7", + "phpunit/phpunit": "9.6.8|10.5.48|11.4.4|11.5.36|12.4.4" }, "type": "phpcodesniffer-standard", "extra": { @@ -7390,7 +8320,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.15.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.25.1" }, "funding": [ { @@ -7402,41 +8332,36 @@ "type": "tidelift" } ], - "time": "2024-03-09T15:20:58+00:00" + "time": "2025-11-25T18:01:43+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.10.3", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "62d32998e820bddc40f99f8251958aed187a5c9c" + "reference": "0525c73950de35ded110cffafb9892946d7771b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/62d32998e820bddc40f99f8251958aed187a5c9c", - "reference": "62d32998e820bddc40f99f8251958aed187a5c9c", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0525c73950de35ded110cffafb9892946d7771b5", + "reference": "0525c73950de35ded110cffafb9892946d7771b5", "shasum": "" }, "require": { "ext-simplexml": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": ">=5.4.0" + "php": ">=7.2.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + "phpunit/phpunit": "^8.4.0 || ^9.3.4 || ^10.5.32 || 11.3.3 - 11.5.28 || ^11.5.31" }, "bin": [ "bin/phpcbf", "bin/phpcs" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -7455,7 +8380,7 @@ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "description": "PHP_CodeSniffer tokenizes PHP files and detects violations of a defined set of coding standards.", "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ "phpcs", @@ -7480,38 +8405,42 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2024-09-18T10:38:58+00:00" + "time": "2025-11-10T16:43:36+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.1.7", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "f6ea51f669760cacd7464bf7eaa0be87b8072db1" + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/f6ea51f669760cacd7464bf7eaa0be87b8072db1", - "reference": "f6ea51f669760cacd7464bf7eaa0be87b8072db1", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece", + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "ext-iconv": "*", - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", - "twig/twig": "^3.0.4" + "symfony/console": "^6.4|^7.0|^8.0", + "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" }, "bin": [ "Resources/bin/var-dump-server" @@ -7549,7 +8478,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.1.7" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.0" }, "funding": [ { @@ -7560,25 +8489,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-05T15:34:55+00:00" + "time": "2025-10-27T20:36:44+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -7607,7 +8540,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -7615,14 +8548,12 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "rcrowe/twigbridge": 20 - }, + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -7635,5 +8566,5 @@ "ext-xml": "*" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/app.php b/config/app.php index 500597655..08bae40f3 100644 --- a/config/app.php +++ b/config/app.php @@ -23,6 +23,7 @@ \Engelsystem\Helpers\Translation\TranslationServiceProvider::class, \Engelsystem\Http\ResponseServiceProvider::class, \Engelsystem\Http\Psr7ServiceProvider::class, + \Engelsystem\Helpers\CacheServiceProvider::class, \Engelsystem\Helpers\AuthenticatorServiceProvider::class, \Engelsystem\Helpers\AssetsServiceProvider::class, \Engelsystem\Renderer\TwigServiceProvider::class, @@ -30,6 +31,7 @@ \Engelsystem\Middleware\RequestHandlerServiceProvider::class, \Engelsystem\Http\Validation\ValidationServiceProvider::class, \Engelsystem\Http\RedirectServiceProvider::class, + \Engelsystem\Http\PaginationServiceProvider::class, // Additional services \Engelsystem\Helpers\VersionServiceProvider::class, diff --git a/config/config.default.php b/config/config.default.php index 1e639fbdf..a90694ac2 100644 --- a/config/config.default.php +++ b/config/config.default.php @@ -14,7 +14,7 @@ 'password' => env_secret('MYSQL_PASSWORD', ''), ], - // For accessing /metrics (and /stats) + // For accessing /metrics 'api_key' => env('API_KEY', ''), // Enable maintenance mode (show a static page to all users) @@ -135,24 +135,45 @@ 'enable_password' => false, // Allow registration even if disabled in config (optional) 'allow_registration' => null, + // Allow disconnecting user accounts from the oauth provider (optional) + 'allow_user_disconnect' => true, // Auto join teams // Info groups field (optional) 'groups' => 'groups', - // Groups to team (angeltype) mapping (optional) + // Groups to team (angel type) mapping (optional) 'teams' => [ - '/Lorem' => 4, // 4 being the ID of the team (angeltype) - '/Foo Mod' => ['id' => 5, 'supporter' => true], // 5 being the ID of the team (angeltype) + '/Lorem' => 4, // 4 being the ID of the team (angel type) + '/Foo Mod' => ['id' => 5, 'supporter' => true], // 5 being the ID of the team (angel type) ], ], */ ], + // Random, long (at least 32 characters) alphanumeric or base64 encoded key, used for signing + 'app_key' => env_secret('APP_KEY'), + + // see https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 + 'jwt_algorithm' => env('JWT_ALGORITHM', 'HS256'), + + // Number of minutes after a JWT must expire for example max angel type join time + 'jwt_expiration_time' => env('JWT_EXPIRATION_TIME', 60 * 24 * 7), + // Default theme, 1 = theme1.scss etc. 'theme' => env('THEME', 1), // Supported themes // To disable a theme in config.php, you can set its value to null 'themes' => [ + 20 => [ + 'name' => 'Engelsystem eh22-light (2025)', + 'type' => 'light', + 'navbar_classes' => 'navbar-light', + ], + 19 => [ + 'name' => 'Engelsystem eh22-dark (2025)', + 'type' => 'dark', + 'navbar_classes' => 'navbar-dark', + ], 18 => [ 'name' => 'Engelsystem 38c3 (2024) - Lila, Lachs und Kurven', 'type' => 'dark', @@ -251,12 +272,15 @@ ], // Redirect to this site after logging in or when clicking the page name - // Must be one of news, meetings, user_shifts, angeltypes, questions + // Must be one of news, meetings, user_shifts, angel types, questions 'home_site' => env('HOME_SITE', 'news'), - // Number of News shown on one site and for feed readers (minimum 1) + // Number of news shown on one site and for feed readers (minimum 1) 'display_news' => env('DISPLAY_NEWS', 10), + // Number of users shown on one admin page table + 'display_users' => env('DISPLAY_USERS', 100), + // Users are able to sign up 'registration_enabled' => (bool) env('REGISTRATION_ENABLED', true), @@ -273,13 +297,16 @@ 'dect' => (bool) env('DECT_REQUIRED', false), ], + // Allow joining angel type via generated QR code + 'join_qr_code' => (bool) env('JOIN_QR_CODE', true), + // Only arrived users can sign up for shifts 'signup_requires_arrival' => (bool) env('SIGNUP_REQUIRES_ARRIVAL', false), // Whether newly-registered users should automatically be marked as arrived 'autoarrive' => (bool) env('AUTOARRIVE', false), - // Supporters of a team (angeltype) can promote other users of the team (angeltype) to supporter + // Supporters of a team (angel type) can promote other users of the team (angel type) to supporter 'supporters_can_promote' => (bool) env('SUPPORTERS_CAN_PROMOTE', false), // Only allow shift signup this number of hours in advance @@ -337,6 +364,9 @@ // Whether force active should be enabled 'enable_force_active' => (bool) env('ENABLE_FORCE_ACTIVE', true), + // Whether force food should be enabled + 'enable_force_food' => (bool) env('ENABLE_FORCE_FOOD', false), + // Allow users with sufficient permission to add worklogs for themselves 'enable_self_worklog' => (bool) env('ENABLE_SELF_WORKLOG', true), @@ -358,7 +388,7 @@ // Local timezone 'timezone' => env('TIMEZONE', 'America/Los_Angeles'), - // Multiply 'night shifts' and freeloaded shifts (start or end between 2 and 8 exclusive) by 2 in goodie score + // Multiply 'night shifts' between start and end (numbers as hours) by multiplier in goodie score // Goodies must be enabled to use this feature 'night_shifts' => [ 'enabled' => (bool) env('NIGHT_SHIFTS', true), // Disable to weigh every shift the same @@ -394,7 +424,7 @@ // The default locale to use 'default_locale' => env('DEFAULT_LOCALE', 'en_US'), - // Available T-Shirt sizes + // Available T-shirt sizes // To disable a t-shirt size in config.php, you can set its value to null 'tshirt_sizes' => [ 'S' => 'Small Straight-Cut', diff --git a/config/routes.php b/config/routes.php index 5db187c4d..de22a996e 100644 --- a/config/routes.php +++ b/config/routes.php @@ -72,13 +72,15 @@ function (RouteCollector $route): void { } ); -// Stats +// Metrics $route->get('/metrics', 'Metrics\\Controller@metrics'); -$route->get('/stats', 'Metrics\\Controller@stats'); // Angeltypes $route->addGroup('/angeltypes', function (RouteCollector $route): void { $route->get('/about', 'AngelTypesController@about'); + $route->get('/{angel_type_id:\d+}/qr', 'AngelTypesController@qrCode'); + $route->post('/{angel_type_id:\d+}/qr', 'AngelTypesController@qrCode'); + $route->get('/{angel_type_id:\d+}/join', 'AngelTypesController@join'); }); // Shifts @@ -114,6 +116,7 @@ function (RouteCollector $route): void { $route->get('/angeltypes', 'Api\AngelTypeController@index'); $route->get('/angeltypes/{angeltype_id:\d+}/shifts', 'Api\ShiftsController@entriesByAngeltype'); + $route->get('/angeltypes/{angeltype_id:\d+}/users', 'Api\UsersController@entriesByAngeltype'); $route->get('/locations', 'Api\LocationsController@index'); $route->get('/locations/{location_id:\d+}/shifts', 'Api\ShiftsController@entriesByLocation'); @@ -123,9 +126,11 @@ function (RouteCollector $route): void { $route->get('/shifttypes', 'Api\ShiftTypeController@index'); $route->get('/shifttypes/{shifttype_id:\d+}/shifts', 'Api\ShiftsController@entriesByShiftType'); + $route->get('/users', 'Api\UsersController@index'); $route->get('/users/{user_id:(?:\d+|self)}', 'Api\UsersController@user'); $route->get('/users/{user_id:(?:\d+|self)}/angeltypes', 'Api\AngelTypeController@ofUser'); $route->get('/users/{user_id:(?:\d+|self)}/shifts', 'Api\ShiftsController@entriesByUser'); + $route->get('/users/{user_id:(?:\d+|self)}/worklogs', 'Api\UsersController@worklogs'); $route->addRoute( ['POST', 'PUT', 'DELETE', 'PATCH'], @@ -213,6 +218,16 @@ function (RouteCollector $route): void { } ); + // Tag + $route->addGroup( + '/tags', + function (RouteCollector $route): void { + $route->get('', 'Admin\\TagController@list'); + $route->get('/edit[/{tag_id:\d+}]', 'Admin\\TagController@edit'); + $route->post('/edit[/{tag_id:\d+}]', 'Admin\\TagController@save'); + } + ); + // Questions $route->addGroup( '/questions', @@ -263,6 +278,15 @@ function (RouteCollector $route): void { ); } ); + + // Vouchers + $route->addGroup( + '/voucher', + function (RouteCollector $route): void { + $route->get('', 'Admin\\UserVoucherController@editVoucher'); + $route->post('', 'Admin\\UserVoucherController@saveVoucher'); + } + ); } ); diff --git a/db/factories/Shifts/ScheduleShiftFactory.php b/db/factories/Shifts/ScheduleShiftFactory.php new file mode 100644 index 000000000..9b5d5bb9d --- /dev/null +++ b/db/factories/Shifts/ScheduleShiftFactory.php @@ -0,0 +1,25 @@ + Shift::factory(), + 'schedule_id' => Schedule::factory(), + 'guid' => $this->faker->uuid(), + ]; + } +} diff --git a/db/factories/User/StateFactory.php b/db/factories/User/StateFactory.php index b87c3b2eb..34911b65b 100644 --- a/db/factories/User/StateFactory.php +++ b/db/factories/User/StateFactory.php @@ -20,11 +20,11 @@ public function definition(): array return [ 'user_id' => User::factory(), - 'arrived' => (bool) $arrival, 'arrival_date' => $arrival ? Carbon::instance($arrival) : null, 'user_info' => $this->faker->optional(.1)->text(), 'active' => $this->faker->boolean(.3), 'force_active' => $this->faker->boolean(.1), + 'force_food' => $this->faker->boolean(.1), 'got_goodie' => $this->faker->boolean(), 'got_voucher' => $this->faker->numberBetween(0, 10), ]; @@ -38,7 +38,6 @@ public function arrived(): self return $this->state( function (array $attributes) { return [ - 'arrived' => true, 'arrival_date' => Carbon::instance($this->faker->dateTimeThisMonth()), ]; } diff --git a/db/factories/WorklogFactory.php b/db/factories/WorklogFactory.php index c88833b2b..990d8ad00 100644 --- a/db/factories/WorklogFactory.php +++ b/db/factories/WorklogFactory.php @@ -16,11 +16,12 @@ class WorklogFactory extends Factory public function definition(): array { return [ - 'user_id' => User::factory(), - 'creator_id' => User::factory(), - 'hours' => $this->faker->randomFloat(2, 0.01, 10), - 'comment' => $this->faker->text(30), - 'worked_at' => $this->faker->dateTimeThisMonth(), + 'user_id' => User::factory(), + 'creator_id' => User::factory(), + 'hours' => $this->faker->randomFloat(2, 0.01, 10), + 'description' => $this->faker->text(30), + 'worked_at' => $this->faker->dateTimeThisMonth(), + 'night_shift' => $this->faker->boolean(), ]; } } diff --git a/db/migrations/2021_05_23_000000_create_first_user.php b/db/migrations/2021_05_23_000000_create_first_user.php index d01473a1e..679430e7c 100644 --- a/db/migrations/2021_05_23_000000_create_first_user.php +++ b/db/migrations/2021_05_23_000000_create_first_user.php @@ -40,6 +40,6 @@ public function up(): void foreach (['users_contact', 'users_personal_data', 'users_state'] as $table) { $db->table($table)->insert(['user_id' => $admin->id]); } - $db->table('users_settings')->insert(['user_id' => $admin->id, 'language' => 'en_US', 'theme' => 0]); + $db->table('users_settings')->insert(['user_id' => $admin->id, 'language' => 'en_US', 'theme' => 1]); } } diff --git a/db/migrations/2024_12_13_000000_add_tag_permission.php b/db/migrations/2024_12_13_000000_add_tag_permission.php new file mode 100644 index 000000000..ef635a4e7 --- /dev/null +++ b/db/migrations/2024_12_13_000000_add_tag_permission.php @@ -0,0 +1,53 @@ +db = $this->schema->getConnection(); + } + + /** + * Run the migration + */ + public function up(): void + { + $this->db->table('privileges') + ->insert([ + 'name' => 'tag.edit', + 'description' => 'Edit tags', + ]); + $privilegeId = $this->db->table('privileges') + ->where('name', 'tag.edit') + ->get(['id']) + ->first() + ->id; + + $this->db->table('group_privileges') + ->insertOrIgnore([ + ['group_id' => $this->shiftCoordinatorId, 'privilege_id' => $privilegeId], + ]); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->db->table('privileges') + ->where('name', 'tag.edit') + ->delete(); + } +} diff --git a/db/migrations/2024_12_29_000000_goodie_manager_show_user_info.php b/db/migrations/2024_12_29_000000_goodie_manager_show_user_info.php new file mode 100644 index 000000000..89df682fc --- /dev/null +++ b/db/migrations/2024_12_29_000000_goodie_manager_show_user_info.php @@ -0,0 +1,49 @@ +schema->getConnection(); + $privilege = $this->getPrivilege($this->privilegeName); + + $db->table('group_privileges') + ->insertOrIgnore([ + ['group_id' => $this->groupId, 'privilege_id' => $privilege->id], + ]); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $db = $this->schema->getConnection(); + $privilege = $this->getPrivilege($this->privilegeName); + + $db->table('group_privileges') + ->where('group_id', $this->groupId) + ->where('privilege_id', $privilege->id) + ->delete(); + } + + protected function getPrivilege(string $name): mixed + { + return $this->schema->getConnection() + ->table('privileges') + ->where('name', $name) + ->first(); + } +} diff --git a/db/migrations/2025_01_16_000000_add_worklog_nightshift.php b/db/migrations/2025_01_16_000000_add_worklog_nightshift.php new file mode 100644 index 000000000..ca293d0e5 --- /dev/null +++ b/db/migrations/2025_01_16_000000_add_worklog_nightshift.php @@ -0,0 +1,31 @@ +schema->table('worklogs', function (Blueprint $table): void { + $table->boolean('night_shift')->default(false)->after('worked_at'); + }); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('worklogs', function (Blueprint $table): void { + $table->dropColumn('night_shift'); + }); + } +} diff --git a/db/migrations/2025_02_09_000000_add_user_info_show_info_permission.php b/db/migrations/2025_02_09_000000_add_user_info_show_info_permission.php new file mode 100644 index 000000000..903bad525 --- /dev/null +++ b/db/migrations/2025_02_09_000000_add_user_info_show_info_permission.php @@ -0,0 +1,67 @@ +db = $this->schema->getConnection(); + } + + /** + * Run the migration + */ + public function up(): void + { + $adminArriveId = $this->getPrivilegeId('admin_arrive'); + + $this->db->table('privileges') + ->insertOrIgnore([ + 'name' => 'user.info.hint', + 'description' => 'Show hint that user info exists', + ]); + $userInfoShowInfoId = $this->getPrivilegeId('user.info.hint'); + + $groups = $this->db->table('group_privileges') + ->select('group_id') + ->where('privilege_id', $adminArriveId) + ->get('group_id'); + + $insertValues = []; + foreach ($groups as $group) { + $insertValues[] = ['group_id' => $group->group_id, 'privilege_id' => $userInfoShowInfoId]; + } + + $this->db->table('group_privileges') + ->insertOrIgnore($insertValues); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->db->table('privileges') + ->where('name', 'user.info.hint') + ->delete(); + } + + private function getPrivilegeId(string $privilege): int + { + return $this->db->table('privileges') + ->where('name', $privilege) + ->get(['id']) + ->first() + ->id; + } +} diff --git a/db/migrations/2025_02_09_000000_edit_permission_descriptions.php b/db/migrations/2025_02_09_000000_edit_permission_descriptions.php new file mode 100644 index 000000000..fb3bf68aa --- /dev/null +++ b/db/migrations/2025_02_09_000000_edit_permission_descriptions.php @@ -0,0 +1,70 @@ +db = $this->schema->getConnection(); + + // name, new description, old description + $this->permissions = [ + ['admin_angel_types', 'Edit angel types', 'Engel Typen administrieren'], + ['login', 'Login', 'Logindialog'], + ['logout', 'Logout', 'User darf sich ausloggen'], + ['news', 'View news', 'Anzeigen der News-Seite'], + ['news.highlight', 'Highlight news', 'Highlight News'], + ['register', 'Register users', 'Einen neuen Engel registerieren'], + ['start', 'Start page', 'Startseite für Gäste/Nicht eingeloggte User'], + ['user.drive.edit', 'Edit driving license', 'Edit Driving License'], + ['user.fa.edit', 'Edit user force active state', 'Edit User Force Active State'], + ['user.ifsg.edit', 'Edit IfSG certificate', 'Edit IfSG Certificate'], + ['user.info.edit', 'Edit user info', 'Edit User Info'], + ['user.info.show', 'View user info', 'Show User Info'], + ]; + } + + /** + * Run the migration + */ + public function up(): void + { + foreach ($this->permissions as $permission) { + $this->updatePrivilegeDescription($permission[0], $permission[1]); + } + $this->db->table('privileges') + ->where('name', 'user.info.show') + ->update(['name' => 'user.info.view']); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->db->table('privileges') + ->where('name', 'user.info.view') + ->update(['name' => 'user.info.show']); + foreach ($this->permissions as $permission) { + $this->updatePrivilegeDescription($permission[0], $permission[2]); + } + } + + private function updatePrivilegeDescription(string $privilege, string $description): void + { + $this->db->table('privileges') + ->where('name', $privilege) + ->update(['description' => $description]); + } +} diff --git a/db/migrations/2025_09_23_000000_worklog_rename_comment_to_description.php b/db/migrations/2025_09_23_000000_worklog_rename_comment_to_description.php new file mode 100644 index 000000000..c6ede0dbe --- /dev/null +++ b/db/migrations/2025_09_23_000000_worklog_rename_comment_to_description.php @@ -0,0 +1,31 @@ +schema->table('worklogs', function (Blueprint $table): void { + $table->renameColumn('comment', 'description'); + }); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('worklogs', function (Blueprint $table): void { + $table->renameColumn('description', 'comment'); + }); + } +} diff --git a/db/migrations/2025_09_24_000000_add_force_food.php b/db/migrations/2025_09_24_000000_add_force_food.php new file mode 100644 index 000000000..c38172a45 --- /dev/null +++ b/db/migrations/2025_09_24_000000_add_force_food.php @@ -0,0 +1,57 @@ +db = $this->schema->getConnection(); + } + + /** + * Run the migration + */ + public function up(): void + { + $this->schema->table('users_state', function (Blueprint $table): void { + $table->boolean('force_food')->after('force_active'); + }); + $this->db->table('privileges')->insertOrIgnore([ + 'name' => 'user.ff.edit', + 'description' => 'Edit user force food state', + ]); + $permissionId = $this->db->table('privileges') + ->where('name', 'user.ff.edit') + ->get(['id']) + ->first()->id; + + // add permission to group Shift Coordinator + $this->db->table('group_privileges') + ->insertOrIgnore([ + ['group_id' => $this->shiftCoordinatorId, 'privilege_id' => $permissionId], + ]); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('users_state', function (Blueprint $table): void { + $table->dropColumn('force_food'); + }); + $this->db->table('privileges')->where('name', 'user.ff.edit')->delete(); + } +} diff --git a/db/migrations/2025_10_19_000000_remove_user_arrived_state.php b/db/migrations/2025_10_19_000000_remove_user_arrived_state.php new file mode 100644 index 000000000..1e8fd96e1 --- /dev/null +++ b/db/migrations/2025_10_19_000000_remove_user_arrived_state.php @@ -0,0 +1,44 @@ +db = $this->schema->getConnection(); + } + + /** + * Run the migration + */ + public function up(): void + { + $this->schema->table('users_state', function (Blueprint $table): void { + $table->dropColumn('arrived'); + }); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('users_state', function (Blueprint $table): void { + $table->boolean('arrived')->default(false)->after('user_id'); + }); + $this->db->table('users_state') + ->whereNotNull('arrival_date') + ->update(['arrived' => true]); + } +} diff --git a/db/migrations/2025_12_12_000000_change_oauth_identifier_database_encoding_to_bin.php b/db/migrations/2025_12_12_000000_change_oauth_identifier_database_encoding_to_bin.php new file mode 100644 index 000000000..c8826dad6 --- /dev/null +++ b/db/migrations/2025_12_12_000000_change_oauth_identifier_database_encoding_to_bin.php @@ -0,0 +1,49 @@ +schema->getConnection(); + if (!$connection->getQueryGrammar() instanceof MySqlGrammar) { + return; + } + + $this->schema->table('oauth', function (Blueprint $table): void { + // Set collation to binary + $table->string('identifier')->collation('utf8mb4_bin')->change(); + + // Recreate index to use new collation + $table->dropUnique(['provider', 'identifier']); + $table->unique(['provider', 'identifier']); + }); + } + + public function down(): void + { + $connection = $this->schema->getConnection(); + if (!$connection->getQueryGrammar() instanceof MySqlGrammar) { + return; + } + + $this->schema->table('oauth', function (Blueprint $table): void { + // Reset collation to unicode + $table->string('identifier')->collation('utf8mb4_unicode_ci')->change(); + + // Recreate index to use new collation + $table->dropUnique(['provider', 'identifier']); + $table->unique(['provider', 'identifier']); + }); + } +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e8f776a18..fe4594bd7 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -26,7 +26,7 @@ services: MYSQL_DATABASE: engelsystem MYSQL_USER: engelsystem MYSQL_PASSWORD: engelsystem - MYSQL_RANDOM_ROOT_PASSWORD: 1 + MYSQL_RANDOM_ROOT_PASSWORD: "1" MYSQL_INITDB_SKIP_TZINFO: "yes" volumes: - db:/var/lib/mysql diff --git a/docker/nginx.conf b/docker/nginx.conf index 0f5ca4af0..189152dd5 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -28,6 +28,11 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; + proxy_connect_timeout 600; + proxy_send_timeout 600; + proxy_read_timeout 600; + send_timeout 600; + index index.php; root /var/www/public; @@ -38,6 +43,7 @@ http { location ~ \.php$ { fastcgi_pass localhost:9000; fastcgi_index index.php; + fastcgi_read_timeout 600s; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } diff --git a/includes/controller/locations_controller.php b/includes/controller/locations_controller.php index 3d2d20be0..84cf8c25c 100644 --- a/includes/controller/locations_controller.php +++ b/includes/controller/locations_controller.php @@ -24,10 +24,9 @@ function location_controller(): array $request = request(); $location = load_location(); - $all_shifts = $location->shifts->sortBy('start'); + $days_list = Days_by_Location_id($location->id); $days = []; - foreach ($all_shifts as $shift) { - $day = $shift->start->format('Y-m-d'); + foreach ($days_list as $day) { if (!isset($days[$day])) { $days[$day] = dateWithEventDay($day); } diff --git a/includes/controller/public_dashboard_controller.php b/includes/controller/public_dashboard_controller.php index 223705776..ea80f6f89 100644 --- a/includes/controller/public_dashboard_controller.php +++ b/includes/controller/public_dashboard_controller.php @@ -76,7 +76,7 @@ function public_dashboard_controller() * * @return array */ -function public_dashboard_controller_free_shift(Shift $shift, ShiftsFilter $filter = null) +function public_dashboard_controller_free_shift(Shift $shift, ?ShiftsFilter $filter = null) { // ToDo move to model and return one $free_shift = [ @@ -109,7 +109,7 @@ function public_dashboard_controller_free_shift(Shift $shift, ShiftsFilter $filt * * @return array */ -function public_dashboard_needed_angels($needed_angels, ShiftsFilter $filter = null) +function public_dashboard_needed_angels($needed_angels, ?ShiftsFilter $filter = null) { $result = []; foreach ($needed_angels as $needed_angel) { diff --git a/includes/controller/shift_entries_controller.php b/includes/controller/shift_entries_controller.php index 3eded31a7..5791fb7f5 100644 --- a/includes/controller/shift_entries_controller.php +++ b/includes/controller/shift_entries_controller.php @@ -113,13 +113,16 @@ function shift_entry_create_controller_admin(Shift $shift, ?AngelType $angeltype } /** @var User[]|Collection $users */ - $users = User::with('userAngelTypes')->orderBy('name')->get(); + $users = User::with(['userAngelTypes', 'shiftEntries'])->orderBy('name')->get(); $users_select = []; foreach ($users as $user) { $name = $user->displayName; if ($user->userAngelTypes->where('id', $angeltype->id)->isEmpty()) { $name = __('%s (not "%s")', [$name, $angeltype->name]); } + if ($user->shiftEntries->where('shift_id', $shift->id)->isNotEmpty()) { + $name = __('%s (already in shift)', [$name]); + } $users_select[$user->id] = $name; } diff --git a/includes/controller/shifts_controller.php b/includes/controller/shifts_controller.php index 040f9fbe3..4d187b355 100644 --- a/includes/controller/shifts_controller.php +++ b/includes/controller/shifts_controller.php @@ -107,14 +107,14 @@ function shift_edit_controller() error(__('Please select a shift type.')); } - if ($request->has('start') && $tmp = DateTime::createFromFormat('Y-m-d H:i', $request->input('start'))) { + if ($request->has('start') && $tmp = DateTime::createFromFormat('Y-m-d\TH:i', $request->input('start'))) { $start = $tmp; } else { $valid = false; error(__('Please enter a valid starting time for the shifts.')); } - if ($request->has('end') && $tmp = DateTime::createFromFormat('Y-m-d H:i', $request->input('end'))) { + if ($request->has('end') && $tmp = DateTime::createFromFormat('Y-m-d\TH:i', $request->input('end'))) { $end = $tmp; } else { $valid = false; @@ -190,13 +190,16 @@ function shift_edit_controller() $angel_types_spinner = ''; foreach ($angeltypes as $angeltype_id => $angeltype_name) { - $angel_types_spinner .= form_spinner( - 'angeltype_count_' . $angeltype_id, - htmlspecialchars($angeltype_name), - $needed_angel_types[$angeltype_id], - [], - (bool) ScheduleShift::whereShiftId($shift->id)->first(), - ); + $angel_types_spinner .= + '' + . form_spinner( + 'angeltype_count_' . $angeltype_id, + htmlspecialchars($angeltype_name), + $needed_angel_types[$angeltype_id], + [], + (bool) ScheduleShift::whereShiftId($shift->id)->first(), + ) + . ''; } $link = button(url('/shifts', ['action' => 'view', 'shift_id' => $shift_id]), icon('chevron-left'), 'btn-sm', '', __('general.back')); @@ -208,18 +211,38 @@ function shift_edit_controller() . info(__('This page is much more comfortable with javascript.'), true) . '', form([ - form_select('shifttype_id', __('Shift type'), $shifttypes, $shifttype_id), - form_text('title', __('title.title'), $title), - form_select('rid', __('Location:'), $locations, $rid), - form_text('start', __('Start:'), $start->format('Y-m-d H:i')), - form_text('end', __('End:'), $end->format('Y-m-d H:i')), - form_textarea('description', __('Additional description'), $description), - form_info( - '', - __('This description is for single shifts, otherwise please use the description in shift type.') - ), - '' . __('Needed angels') . '', - $angel_types_spinner, + div('row', [ + div('col-md-6 col-xl-5', [ + form_select('shifttype_id', __('Shift type'), $shifttypes, $shifttype_id), + form_text('title', __('title.title'), $title), + form_select('rid', __('Location'), $locations, $rid), + ]), + div('col-md-6 col-xl-7', [ + form_textarea('description', __('Additional description'), $description), + form_info( + '', + __('This description is for single shifts, otherwise please use the description in shift type.') + ), + ]), + ]), + div('row', [ + div('col-md-6 col-xl-5', [ + div('row', [ + div('col-lg-6', [ + form_datetime('start', __('shifts.start'), $start), + ]), + div('col-lg-6', [ + form_datetime('end', __('shifts.end'), $end), + ]), + ]), + ]), + div('col-md-6 col-xl-7', [ + '' . __('Needed angels') . '', + div('row', [ + $angel_types_spinner, + ]), + ]), + ]), form_submit('submit', icon('save') . __('form.save')), ]), ] @@ -280,7 +303,9 @@ function shift_controller() throw_redirect(url('/user-shifts')); } - $shift = Shift($request->input('shift_id')); + $shift = Shift::with(['shiftEntries.user.state', 'shiftEntries.angelType']) + ->findOrFail($request->input('shift_id')); + $shift = Shift($shift); if (empty($shift)) { error(__('Shift could not be found.')); throw_redirect(url('/user-shifts')); diff --git a/includes/controller/user_angeltypes_controller.php b/includes/controller/user_angeltypes_controller.php index 68196fd63..7d5791b79 100644 --- a/includes/controller/user_angeltypes_controller.php +++ b/includes/controller/user_angeltypes_controller.php @@ -286,6 +286,14 @@ function user_angeltype_update_controller(): array if ($request->hasPostData('submit')) { $user_angeltype->supporter = $supporter; + if ($supporter && !$user_angeltype->confirm_user_id) { + $user_angeltype->confirmUser()->associate(auth()->user()); + engelsystem_log(sprintf( + '%s confirmed for angel type %s', + User_Nick_render($user_source, true), + AngelType_name_render($angeltype, true) + )); + } $user_angeltype->save(); $msg = $supporter @@ -314,6 +322,7 @@ function user_angeltype_update_controller(): array */ function user_angeltype_add_controller(): array { + /** @var AngelType $angeltype */ $angeltype = AngelType::findOrFail(request()->input('angeltype_id')); // User is joining by itself @@ -354,30 +363,38 @@ function user_angeltype_add_controller(): array if ($request->hasPostData('submit')) { $user_source = load_user(); - if (!$angeltype->userAngelTypes()->wherePivot('user_id', $user_source->id)->exists()) { - $userAngelType = new UserAngelType(); - $userAngelType->user()->associate($user_source); - $userAngelType->angelType()->associate($angeltype); + if ( + !$angeltype + ->userAngelTypes() + ->wherePivot('user_id', $user_source->id) + ->wherePivotNotNull('confirm_user_id') + ->exists() + ) { + /** @var UserAngelType $userAngelType */ + $userAngelType = UserAngelType::firstOrCreate([ + 'user_id' => $user_source->id, + 'angel_type_id' => $angeltype->id, + ]); + + $setSupporter = $request->hasPostData('set_supporter') + && (auth()->can('admin_angel_types') || config('supporters_can_promote')); + if ($setSupporter) { + $userAngelType->supporter = true; + } + if ($request->hasPostData('auto_confirm_user') || $setSupporter) { + $userAngelType->confirmUser()->associate($user_source); + } $userAngelType->save(); engelsystem_log(sprintf( - 'User %s added to %s.', + 'User %s added to angel type %s, confirmed: %s, supporter: %s.', User_Nick_render($user_source, true), - AngelType_name_render($angeltype, true) + AngelType_name_render($angeltype, true), + $userAngelType->confirm_user_id ? 'true' : 'false', + $userAngelType->supporter ? 'true' : 'false', )); - success(sprintf(__('User %s added to %s.'), $user_source->displayName, $angeltype->name)); - - if ($request->hasPostData('auto_confirm_user')) { - $userAngelType->confirmUser()->associate($user_source); - $userAngelType->save(); - - engelsystem_log(sprintf( - 'User %s confirmed as %s.', - User_Nick_render($user_source, true), - AngelType_name_render($angeltype, true) - )); - } + success(sprintf(__('User %s added to %s.'), $user_source->displayName, $angeltype->name)); user_angeltype_add_email($user_source, $angeltype); throw_redirect(url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angeltype->id])); diff --git a/includes/controller/users_controller.php b/includes/controller/users_controller.php index 9780b7bcc..90a87ebe1 100644 --- a/includes/controller/users_controller.php +++ b/includes/controller/users_controller.php @@ -1,6 +1,7 @@ user_controller(), 'delete' => user_delete_controller(), - 'edit_vouchers' => user_edit_vouchers_controller(), 'list' => users_list_controller(), default => users_list_controller(), }; @@ -93,7 +94,7 @@ function user_delete_controller() mail_user_delete($user_source); success(__('User deleted.')); - engelsystem_log(sprintf('Deleted %s', User_Nick_render($user_source, true))); + engelsystem_log(sprintf('Deleted user %s', User_Nick_render($user_source, true))); throw_redirect(users_link()); } @@ -140,62 +141,6 @@ function user_link($userId) return url('/users', ['action' => 'view', 'user_id' => $userId]); } -/** - * @return array - */ -function user_edit_vouchers_controller() -{ - $user = auth()->user(); - $request = request(); - - if ($request->has('user_id')) { - $user_source = User::find($request->input('user_id')); - } else { - $user_source = $user; - } - - if ( - (!auth()->can('admin_user') && !auth()->can('voucher.edit')) - || !config('enable_voucher') - ) { - throw_redirect(url('/')); - } - - if ($request->hasPostData('submit')) { - $valid = true; - - $vouchers = ''; - if ( - $request->has('vouchers') - && test_request_int('vouchers') - && trim($request->input('vouchers')) >= 0 - ) { - $vouchers = trim($request->input('vouchers')); - } else { - $valid = false; - error(__('Please enter a valid number of vouchers.')); - } - - if ($valid) { - $user_source->state->got_voucher = $vouchers; - $user_source->state->save(); - - success(__('Saved the number of vouchers.')); - engelsystem_log(User_Nick_render($user_source, true) . ': ' . sprintf( - 'Got %s vouchers', - $user_source->state->got_voucher - )); - - throw_redirect(user_link($user_source->id)); - } - } - - return [ - sprintf(__('%s\'s vouchers'), htmlspecialchars($user_source->displayName)), - User_edit_vouchers_view($user_source), - ]; -} - /** * @return array */ @@ -228,16 +173,14 @@ function user_controller() ); $neededAngeltypes = $shift->needed_angeltypes; foreach ($neededAngeltypes as &$needed_angeltype) { - $needed_angeltype['users'] = Db::select( - ' - SELECT `shift_entries`.`freeloaded_by`, `users`.* - FROM `shift_entries` - JOIN `users` ON `shift_entries`.`user_id`=`users`.`id` - WHERE `shift_entries`.`shift_id` = ? - AND `shift_entries`.`angel_type_id` = ? - ', - [$shift->id, $needed_angeltype['id']] - ); + $needed_angeltype['users'] = User::query() + ->select(['users.*', 'shift_entries.freeloaded_by']) + ->from('shift_entries') + ->join('users', 'shift_entries.user_id', 'users.id') + ->where('shift_entries.shift_id', $shift->id) + ->where('shift_entries.angel_type_id', $needed_angeltype['id']) + ->with('state') + ->get(); } $shift->needed_angeltypes = $neededAngeltypes; } @@ -246,9 +189,9 @@ function user_controller() auth()->resetApiKey($user_source); } - $goodie_score = sprintf('%.2f', User_goodie_score($user_source->id)) . ' h'; + $goodie_score = sprintf('%.2f', Goodie::userScore($user_source)) . ' h'; if ($user_source->state->force_active && config('enable_force_active')) { - $goodie_score = '' . __('Enough') . ''; + $goodie_score = '' . __('user.goodie_score.enough') . ''; } $worklogs = $user_source->worklogs() @@ -314,6 +257,7 @@ function users_list_controller() 'freeloads', 'active', 'force_active', + 'force_food', 'got_goodie', 'shirt_size', 'planned_arrival_date', @@ -324,12 +268,18 @@ function users_list_controller() $order_by = $request->input('OrderBy'); } - /** @var User[]|Collection $users */ + $perPage = $request->get('c', config('display_users')); + if ($perPage == 'all') { + $perPage = PHP_INT_MAX; + } + $perPage = is_numeric($perPage) ? (int) $perPage : config('display_users'); + + /** @var User[]|Collection|LengthAwarePaginator $users */ $users = User::with(['contact', 'personalData', 'state', 'shiftEntries' => function (HasMany $query) { $query->whereNotNull('freeloaded_by'); }]) ->orderBy('name') - ->get(); + ->paginate($perPage); foreach ($users as $user) { $user->setAttribute( 'freeloads', @@ -339,7 +289,7 @@ function users_list_controller() ); } - $users = $users->sortBy(function (User $user) use ($order_by) { + $sortedUsers = $users->sortBy(function (User $user) use ($order_by) { $userData = $user->toArray(); $data = []; array_walk_recursive($userData, function ($value, $key) use (&$data) { @@ -348,6 +298,7 @@ function users_list_controller() return isset($data[$order_by]) ? Str::lower($data[$order_by]) : null; }); + $users->setCollection($sortedUsers); return [ __('All users'), @@ -357,9 +308,11 @@ function users_list_controller() State::whereArrived(true)->count(), State::whereActive(true)->count(), State::whereForceActive(true)->count(), + State::whereForceFood(true)->count(), ShiftEntry::whereNotNull('freeloaded_by')->count(), State::whereGotGoodie(true)->count(), - State::query()->sum('got_voucher') + State::query()->sum('got_voucher'), + auth()->can('admin_user'), ), ]; } diff --git a/includes/includes.php b/includes/includes.php index d68579cf6..da25d0307 100644 --- a/includes/includes.php +++ b/includes/includes.php @@ -17,7 +17,6 @@ __DIR__ . '/../includes/model/ShiftsFilter.php', __DIR__ . '/../includes/model/ShiftSignupState.php', __DIR__ . '/../includes/model/Stats.php', - __DIR__ . '/../includes/model/User_model.php', __DIR__ . '/../includes/view/AngelTypes_view.php', __DIR__ . '/../includes/view/PublicDashboard_view.php', diff --git a/includes/model/ShiftEntry_model.php b/includes/model/ShiftEntry_model.php index f5b6fbf08..bf5288eec 100644 --- a/includes/model/ShiftEntry_model.php +++ b/includes/model/ShiftEntry_model.php @@ -72,7 +72,7 @@ function ShiftEntries_upcoming_for_user(User $user): Collection * @param Carbon|null $sinceTime * @return ShiftEntry[]|Collection */ -function ShiftEntries_finished_by_user(User $user, Carbon $sinceTime = null): Collection +function ShiftEntries_finished_by_user(User $user, ?Carbon $sinceTime = null): Collection { $query = $user->shiftEntries() ->with(['shift', 'shift.shiftType']) diff --git a/includes/model/Shifts_model.php b/includes/model/Shifts_model.php index 5f71647d0..83fe0217f 100644 --- a/includes/model/Shifts_model.php +++ b/includes/model/Shifts_model.php @@ -59,7 +59,7 @@ function Shifts_by_angeltype(AngelType $angeltype) * * @return Collection|Shift[] */ -function Shifts_free($start, $end, ShiftsFilter $filter = null) +function Shifts_free($start, $end, ?ShiftsFilter $filter = null) { $start = Carbon::createFromTimestamp($start, Carbon::now()->timezone); $end = Carbon::createFromTimestamp($end, Carbon::now()->timezone); @@ -350,6 +350,60 @@ function NeededAngeltype_by_Shift_and_Angeltype(Shift $shift, AngelType $angelty ); } +/** + * returns all days with shifts needing angels for a location + * + * @param int $location_id + * @return list + */ +function Days_by_Location_id(int $location_id): array +{ + $sql = ' + SELECT + DATE(`shifts`.`start`) AS `day` + FROM `shifts` + JOIN `needed_angel_types` ON `needed_angel_types`.`shift_id`=`shifts`.`id` + LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id + WHERE `shifts`.`location_id` = ? + AND s.shift_id IS NULL + + UNION + + /* By shift type */ + SELECT + DATE(`shifts`.`start`) AS `day` + FROM `shifts` + JOIN `needed_angel_types` ON `needed_angel_types`.`shift_type_id`=`shifts`.`shift_type_id` + LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE `shifts`.`location_id` = ? + AND NOT s.shift_id IS NULL + AND se.needed_from_shift_type = TRUE + + UNION + + /* By location */ + SELECT + DATE(`shifts`.`start`) AS `day` + FROM `shifts` + JOIN `needed_angel_types` ON `needed_angel_types`.`location_id`=`shifts`.`location_id` + LEFT JOIN schedule_shift AS s on shifts.id = s.shift_id + LEFT JOIN schedules AS se on s.schedule_id = se.id + WHERE `shifts`.`location_id` = ? + AND NOT s.shift_id IS NULL + AND se.needed_from_shift_type = FALSE + '; + + return array_column(Db::select( + $sql, + [ + $location_id, + $location_id, + $location_id, + ] + ), 'day'); +} + /** * @param ShiftsFilter $shiftsFilter * @return ShiftEntry[]|Collection @@ -536,9 +590,10 @@ function Shift_signup_allowed_admin(AngelType $needed_angeltype, $shift_entries) * @param Shift $shift The shift * @param AngelType $angeltype The angeltype * @param int $signout_user_id The user that was signed up for the shift + * @param ?bool $isAngeltypeSupporter User is supporter for angeltype * @return bool */ -function Shift_signout_allowed(Shift $shift, AngelType $angeltype, $signout_user_id) +function Shift_signout_allowed(Shift $shift, AngelType $angeltype, $signout_user_id, ?bool $isAngeltypeSupporter = null) { $user = auth()->user(); @@ -548,9 +603,10 @@ function Shift_signout_allowed(Shift $shift, AngelType $angeltype, $signout_user } // angeltype supporter can sign out any user at any time from their supported angeltype - if ( - $user->isAngelTypeSupporter($angeltype) || auth()->can('admin_user_angeltypes') - ) { + $isAngeltypeSupporter = !is_null($isAngeltypeSupporter) + ? $isAngeltypeSupporter + : $user->isAngelTypeSupporter($angeltype); + if ($isAngeltypeSupporter || auth()->can('admin_user_angeltypes')) { return true; } @@ -612,6 +668,12 @@ function Shift_signup_allowed( */ function Shifts_by_user($userId, $include_freeloaded_comments = false) { + # Cache static content per request + static $cached; + if (!empty($cached[$userId][$include_freeloaded_comments])) { + return $cached[$userId][$include_freeloaded_comments]; + } + $shiftsData = Db::select( ' SELECT @@ -632,6 +694,7 @@ function Shifts_by_user($userId, $include_freeloaded_comments = false) JOIN `shift_types` ON (`shift_types`.`id` = `shifts`.`shift_type_id`) JOIN `locations` ON (`shifts`.`location_id` = `locations`.`id`) WHERE shift_entries.`user_id` = ? + GROUP BY shifts.id ORDER BY `start` ', [ @@ -644,6 +707,7 @@ function Shifts_by_user($userId, $include_freeloaded_comments = false) $shifts[] = (new Shift())->forceFill($data); } $shifts->load(['shiftType', 'location']); + $cached[$userId][$include_freeloaded_comments] = $shifts; return $shifts; } diff --git a/includes/model/Stats.php b/includes/model/Stats.php index 9157337cd..2b52df209 100644 --- a/includes/model/Stats.php +++ b/includes/model/Stats.php @@ -11,7 +11,7 @@ * * @return int|string */ -function stats_currently_working(ShiftsFilter $filter = null): int|string +function stats_currently_working(?ShiftsFilter $filter = null): int|string { $result = Db::selectOne( ' @@ -37,7 +37,7 @@ function stats_currently_working(ShiftsFilter $filter = null): int|string * * @return int|string */ -function stats_hours_to_work(ShiftsFilter $filter = null): int|string +function stats_hours_to_work(?ShiftsFilter $filter = null): int|string { $result = Db::selectOne( ' @@ -92,7 +92,7 @@ function stats_hours_to_work(ShiftsFilter $filter = null): int|string * * @return int|string */ -function stats_angels_needed_three_hours(ShiftsFilter $filter = null): int|string +function stats_angels_needed_three_hours(?ShiftsFilter $filter = null): int|string { $in3hours = Carbon::now()->addHours(3)->toDateTimeString(); $result = Db::selectOne(' @@ -200,7 +200,7 @@ function stats_angels_needed_three_hours(ShiftsFilter $filter = null): int|strin * * @return int|string */ -function stats_angels_needed_for_nightshifts(ShiftsFilter $filter = null): int|string +function stats_angels_needed_for_nightshifts(?ShiftsFilter $filter = null): int|string { $nightShiftsConfig = config('night_shifts'); $nightStartTime = $nightShiftsConfig['start']; diff --git a/includes/model/User_model.php b/includes/model/User_model.php deleted file mode 100644 index 3bbaec583..000000000 --- a/includes/model/User_model.php +++ /dev/null @@ -1,128 +0,0 @@ - 0]; - } - - $worklogHours = Worklog::query() - ->where('user_id', $userId) - ->where('worked_at', '<=', Carbon::Now()) - ->sum('hours'); - - return $result_shifts['goodie_score'] + $worklogHours; -} - -/** - * @param User $user - * @return float - */ -function User_get_eligable_voucher_count($user) -{ - $voucher_settings = config('voucher_settings'); - $start = $voucher_settings['voucher_start'] - ? Carbon::createFromFormat('Y-m-d', $voucher_settings['voucher_start'])->setTime(0, 0) - : null; - - $shiftEntries = ShiftEntries_finished_by_user($user, $start); - $worklog = $user->worklogs() - ->whereDate('worked_at', '>=', $start ?: 0) - ->with(['user', 'creator']) - ->get(); - $shifts_done = - count($shiftEntries) - + $worklog->count(); - - $shiftsTime = 0; - foreach ($shiftEntries as $shiftEntry) { - $shiftsTime += ($shiftEntry->shift->end->timestamp - $shiftEntry->shift->start->timestamp) / 60 / 60; - } - foreach ($worklog as $entry) { - $shiftsTime += $entry->hours; - } - - $vouchers = $voucher_settings['initial_vouchers']; - if ($voucher_settings['shifts_per_voucher']) { - $vouchers += $shifts_done / $voucher_settings['shifts_per_voucher']; - } - if ($voucher_settings['hours_per_voucher']) { - $vouchers += $shiftsTime / $voucher_settings['hours_per_voucher']; - } - - $vouchers -= $user->state->got_voucher; - $vouchers = floor($vouchers); - if ($vouchers < 0) { - return 0; - } - - return $vouchers; -} - -/** - * Generates the query to sum night shifts - * - * @return string - */ -function User_get_shifts_sum_query() -{ - $nightShifts = config('night_shifts'); - if (!$nightShifts['enabled']) { - return 'COALESCE(SUM(UNIX_TIMESTAMP(shifts.end) - UNIX_TIMESTAMP(shifts.start)), 0)'; - } - - /* @see \Engelsystem\Models\Shifts\Shift::isNightShift to keep it in sync */ - return sprintf( - ' - COALESCE(SUM( - (1 + ( - /* Starts during night */ - HOUR(shifts.start) >= %1$d AND HOUR(shifts.start) < %2$d - /* Ends during night */ - OR ( - HOUR(shifts.end) > %1$d - || HOUR(shifts.end) = %1$d AND MINUTE(shifts.end) > 0 - ) AND HOUR(shifts.end) <= %2$d - /* Starts before and ends after night */ - OR HOUR(shifts.start) <= %1$d AND HOUR(shifts.end) >= %2$d - )) - * (UNIX_TIMESTAMP(shifts.end) - UNIX_TIMESTAMP(shifts.start)) - * (1 - (%3$d + 1) - * (CASE - WHEN `shift_entries`.`freeloaded_by` IS NULL THEN 0 - ELSE 1 - END)) - ), 0) - ', - $nightShifts['start'], - $nightShifts['end'], - $nightShifts['multiplier'] - ); -} diff --git a/includes/pages/admin_active.php b/includes/pages/admin_active.php index 585f0667e..f95d1722a 100644 --- a/includes/pages/admin_active.php +++ b/includes/pages/admin_active.php @@ -1,7 +1,9 @@ getValue(Db::connection()->getQueryGrammar()); + $worklog_sum_formula = Goodie::worklogScoreQuery()->getValue(Db::connection()->getQueryGrammar()); $request = request(); $goodie = GoodieType::from(config('goodie_type')); $goodie_enabled = $goodie !== GoodieType::None; @@ -68,20 +71,21 @@ function admin_active() users.*, COUNT(shift_entries.id) AS shift_count, (%s + ( - SELECT COALESCE(SUM(`hours`) * 3600, 0) + SELECT %s * 3600 FROM `worklogs` WHERE `user_id`=`users`.`id` AND `worked_at` <= NOW() )) AS `shift_length` ', - $shift_sum_formula + $shift_sum_formula, + $worklog_sum_formula ) ) ->leftJoin('shift_entries', 'users.id', '=', 'shift_entries.user_id') ->leftJoin('shifts', 'shift_entries.shift_id', '=', 'shifts.id') ->leftJoin('users_state', 'users.id', '=', 'users_state.user_id') - ->where('users_state.arrived', '=', true) + ->whereNotNull('users_state.arrival_date') ->orWhere(function (EloquentBuilder $userinfo) { - $userinfo->where('users_state.arrived', '=', false) + $userinfo->whereNull('users_state.arrival_date') ->whereNotNull('users_state.user_info') ->whereNot('users_state.user_info', ''); }) @@ -150,11 +154,9 @@ function admin_active() } else { $user_source->state->got_goodie = true; $user_source->state->save(); - engelsystem_log('User ' . User_Nick_render($user_source, true) . ' has tshirt now.'); + engelsystem_log('User ' . User_Nick_render($user_source, true) . ' has goodie now.'); $msg = success( - ($goodie_tshirt - ? __('Angel has got a T-shirt.') - : __('Angel has got a goodie.')), + __('Angel got a goodie.'), true ); } @@ -167,11 +169,9 @@ function admin_active() if ($user_source) { $user_source->state->got_goodie = false; $user_source->state->save(); - engelsystem_log('User ' . User_Nick_render($user_source, true) . ' has NO tshirt.'); + engelsystem_log('User ' . User_Nick_render($user_source, true) . ' has NO goodie.'); $msg = success( - ($goodie_tshirt - ? __('Angel has got no T-shirt.') - : __('Angel has got no goodie.')), + __('Angel got no goodie.'), true ); } else { @@ -180,19 +180,20 @@ function admin_active() } } - $query = User::with(['personalData', 'state', 'worklogs']) + $query = User::with(['personalData', 'state', 'worklogs', 'shiftEntries']) ->selectRaw( sprintf( ' users.*, COUNT(shift_entries.id) AS shift_count, (%s + ( - SELECT COALESCE(SUM(`hours`) * 3600, 0) + SELECT %s * 3600 FROM `worklogs` WHERE `user_id`=`users`.`id` AND `worked_at` <= NOW() )) AS `shift_length` ', - $shift_sum_formula + $shift_sum_formula, + $worklog_sum_formula ) ) ->leftJoin('shift_entries', 'users.id', '=', 'shift_entries.user_id') @@ -208,9 +209,9 @@ function admin_active() } }) ->leftJoin('users_state', 'users.id', '=', 'users_state.user_id') - ->where('users_state.arrived', '=', true) + ->whereNotNull('users_state.arrival_date') ->orWhere(function (EloquentBuilder $userinfo) { - $userinfo->where('users_state.arrived', '=', false) + $userinfo->whereNull('users_state.arrival_date') ->whereNotNull('users_state.user_info') ->whereNot('users_state.user_info', ''); }) @@ -251,9 +252,8 @@ function admin_active() $timeSum = 0; /** @var ShiftEntry[] $shiftEntries */ - $shiftEntries = $user->shiftEntries() - ->with('shift') - ->get(); + $shiftEntries = $user->shiftEntries + ->load('shift'); foreach ($shiftEntries as $entry) { if ($entry->freeloaded_by || $entry->shift->start > Carbon::now()) { continue; @@ -280,6 +280,7 @@ function admin_active() . ' min (' . sprintf('%.2f', $user['shift_length'] / 3600) . ' h)'; $userData['active'] = icon_bool($user->state->active); $userData['force_active'] = icon_bool($user->state->force_active); + $userData['force_food'] = icon_bool($user->state->force_food); $userData['tshirt'] = icon_bool($user->state->got_goodie); $userData['shift_count'] = $user['shift_count']; @@ -349,7 +350,7 @@ function admin_active() [form_submit( 'submit', icon('gift') - . ($goodie_tshirt ? __('user.got_shirt') : __('user.got_goodie')), + . __('user.got_goodie'), 'btn-sm', false, 'secondary' @@ -374,7 +375,7 @@ function admin_active() [form_submit( 'submit', icon('gift') - . ($goodie_tshirt ? __('Remove T-shirt') : __('Remove goodie')), + . __('Remove goodie'), 'btn-sm', false, 'secondary' @@ -400,24 +401,31 @@ function admin_active() } $goodie_statistics = []; + $total = 0; if ($goodie_tshirt) { foreach (array_keys($tshirt_sizes) as $size) { - $gc = State::query() + $query = State::query() ->leftJoin('users_settings', 'users_state.user_id', '=', 'users_settings.user_id') ->leftJoin('users_personal_data', 'users_state.user_id', '=', 'users_personal_data.user_id') - ->where('users_state.got_goodie', '=', true) ->where('users_personal_data.shirt_size', '=', $size) - ->count(); + ; + $given = $query->clone()->where('users_state.got_goodie', true)->count(); + $notGiven = $query->clone()->where('users_state.got_goodie', false)->count(); + + $totalSum = $given + $notGiven; + $total += $totalSum; $goodie_statistics[] = [ 'size' => $size, - 'given' => $gc, + 'given' => $given, + 'total' => $totalSum, ]; } } $goodie_statistics[] = array_merge( ($goodie_tshirt ? ['size' => '' . __('Sum') . ''] : []), - ['given' => '' . State::whereGotGoodie(true)->count() . ''] + ['given' => '' . State::whereGotGoodie(true)->count() . ''], + ['total' => '' . $total . ''], ); return page_with_title(admin_active_title(), [ @@ -442,25 +450,23 @@ function admin_active() 'shift_count' => __('general.shifts'), 'work_time' => __('Length'), ], - ($goodie_enabled ? ['score' => ($goodie_tshirt - ? __('T-shirt score') - : __('Goodie score') - )] : []), + ($goodie_enabled ? ['score' => __('Goodie score')] : []), [ 'active' => __('Active'), ], (config('enable_force_active') ? ['force_active' => __('Forced'),] : []), - ($goodie_enabled ? ['tshirt' => ($goodie_tshirt ? __('T-shirt') : __('Goodie'))] : []), + ($goodie_enabled ? ['tshirt' => __('Goodie')] : []), [ 'actions' => __('general.actions'), ] ), $matched_users ), - $goodie_enabled ? '' . ($goodie_tshirt ? __('T-shirt statistic') : __('Goodie statistic')) . '' : '', + $goodie_enabled ? '' . __('Goodie statistic') . '' : '', $goodie_enabled ? table(array_merge( ($goodie_tshirt ? ['size' => __('Size')] : []), - ['given' => $goodie_tshirt ? __('Given T-shirts') : __('Given goodies')] + ['given' => __('Given goodies')], + $goodie_tshirt ? ['total' => __('Configured T-shirts')] : [], ), $goodie_statistics) : '', ]); } diff --git a/includes/pages/admin_arrive.php b/includes/pages/admin_arrive.php index f73504d5e..4df5926af 100644 --- a/includes/pages/admin_arrive.php +++ b/includes/pages/admin_arrive.php @@ -1,7 +1,10 @@ can('admin_arrive'); + $exactSearch = $request->has('exact'); if ($request->has('search')) { $search = strip_request_item('search'); $search = trim($search); @@ -31,38 +35,48 @@ function admin_arrive() if ( $action == 'reset' && preg_match('/^\d+$/', $request->input('user')) - && $request->hasPostData('submit') + && $request->hasPostData('send') ) { $user_id = $request->input('user'); $user_source = User::find($user_id); if ($user_source) { - $user_source->state->arrived = false; $user_source->state->arrival_date = null; $user_source->state->save(); engelsystem_log('User set to not arrived: ' . User_Nick_render($user_source, true)); + + if (in_array('application/json', $request->getAcceptableContentTypes())) { + // This was an async request, send a JSON response. + return arrive_respond_json($user_source); + } + success(__('Reset done. Angel has not arrived.')); - throw_redirect(back()->getHeaderLine('redirect')); + throw_redirect(back()->getHeaderLine('location')); } else { $msg = error(__('Angel not found.'), true); } } elseif ( $action == 'arrived' && preg_match('/^\d+$/', $request->input('user')) - && $request->hasPostData('submit') + && $request->hasPostData('send') ) { $user_id = $request->input('user'); $user_source = User::find($user_id); if ($user_source) { - $user_source->state->arrived = true; $user_source->state->arrival_date = new Carbon\Carbon(); $user_source->state->save(); - engelsystem_log('User set has arrived: ' . User_Nick_render($user_source, true)); + engelsystem_log('User set as arrived: ' . User_Nick_render($user_source, true)); + + if (in_array('application/json', $request->getAcceptableContentTypes())) { + // This was an async request, send a JSON response. + return arrive_respond_json($user_source); + } + success(__('Angel has been marked as arrived.')); - throw_redirect(back()->getHeaderLine('redirect')); + throw_redirect(back()->getHeaderLine('location')); } else { $msg = error(__('Angel not found.'), true); } @@ -82,24 +96,30 @@ function admin_arrive() } foreach ($users as $usr) { if (count($tokens) > 0) { - $match = false; - $data = collect($usr->toArray())->flatten()->filter(function ($value) { - // Remove empty values - return !empty($value) && - // Skip datetime - !preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{6}Z$/', (string) $value); - }); - $index = join(' ', $data->toArray()); - foreach ($tokens as $token) { - $token = trim($token); - if (!empty($token) && stristr($index, $token)) { - $match = true; - break; - } + if ($exactSearch && Str::lower($usr->name) != Str::lower(implode(' ', $tokens))) { + continue; } - if (!$match) { - continue; + if (!$exactSearch) { + $match = false; + $data = collect($usr->toArray())->flatten()->filter(function ($value) { + // Remove empty values + return !empty($value) && + // Skip datetime + !preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{6}Z$/', (string) $value); + }); + $index = join(' ', $data->toArray()); + foreach ($tokens as $token) { + $token = trim($token); + if (!empty($token) && stristr($index, $token)) { + $match = true; + break; + } + } + + if (!$match) { + continue; + } } } @@ -119,7 +139,7 @@ function admin_arrive() form_hidden('action', $usr->state->arrived ? 'reset' : 'arrived'), form_hidden('user', $usr->id), form_submit( - 'submit', + 'send', $usr->state->arrived ? icon('arrow-counterclockwise') : icon('house'), @@ -134,7 +154,7 @@ function admin_arrive() 'confirm_button_text' => __('Reset'), ] : [], ), - ]); + ], '', '', false, 'arrive_form'); if ($usr->state->arrival_date) { $day = $usr->state->arrival_date->format('Y-m-d'); @@ -213,7 +233,10 @@ function admin_arrive() $msg . msg(), form([ form_text('search', __('form.search'), $search), - form_submit('submit', icon('search') . __('form.search')), + div('row mb-3 align-items-center', [ + div('col-sm-auto', [form_submit('submit', icon('search') . __('form.search'), '', false)]), + div('col', [form_checkbox('exact', __('form.exact_match'), $exactSearch)]), + ]), ], url('/admin-arrive')), table(array_merge( ['name' => __('general.name'),], @@ -274,3 +297,31 @@ function admin_arrive() ] : []), ]); } + +function arrive_respond_json(Model $user_source): Response +{ + return response() + ->withHeader('content-type', 'application/json') + ->withContent(json_encode([ + 'state' => $user_source->state->arrived ? 'arrived' : 'not_arrived', + 'arrival_date' => $user_source->state->arrival_date?->format( + __('general.date') + ) ?: '-', + 'button' => form_submit( + 'send', + $user_source->state->arrived + ? icon('arrow-counterclockwise') + : icon('house'), + 'btn-sm', + false, + $user_source->state->arrived ? 'danger' : 'primary', + $user_source->state->arrived + ? __('Reset') + : __('user.arrive'), + $user_source->state->arrived ? [ + 'confirm_submit_title' => __('Reset arrival state for %s?', [$user_source->name]), + 'confirm_button_text' => __('Reset'), + ] : [], + ), + ])); +} diff --git a/includes/pages/admin_free.php b/includes/pages/admin_free.php index 2fc2ce291..8fcd61c4c 100644 --- a/includes/pages/admin_free.php +++ b/includes/pages/admin_free.php @@ -50,7 +50,7 @@ function admin_free() ->where('shifts.start', '<', Carbon::now()) ->where('shifts.end', '>', Carbon::now()); }) - ->where('users_state.arrived', '=', 1) + ->whereNotNull('users_state.arrival_date') ->whereNull('shifts.id') ->orderBy('users.name') ->groupBy('users.id'); diff --git a/includes/pages/admin_user.php b/includes/pages/admin_user.php index cf757a9fe..ec6a02e3a 100644 --- a/includes/pages/admin_user.php +++ b/includes/pages/admin_user.php @@ -1,5 +1,6 @@ '; $html .= '' - . '' . '' . "\n"; $html .= ' ' . __('Last login') . '' @@ -70,30 +67,30 @@ function admin_user() . '' . "\n"; if (config('enable_full_name')) { $html .= ' ' . __('settings.profile.firstname') . '' - . '' + . '' . '' . "\n"; $html .= ' ' . __('settings.profile.lastname') . '' - . '' + . '' . '' . "\n"; } $html .= ' ' . __('settings.profile.mobile') . '' - . '' + . '' . '' . "\n"; if (config('enable_dect')) { $html .= ' ' . __('general.dect') . '' - . '' + . '' . '' . "\n"; } if ($user_source->settings->email_human) { $html .= ' ' . __('general.email') . '' - . '' + . '' . '' . "\n"; } if ($goodie_tshirt && $user_goodie_edit) { $html .= ' ' . __('user.shirt_size') . '' . html_select_key( 'size', - 'eSize', + 'shirt_size', $tshirt_sizes, $user_source->personalData->shirt_size, __('form.select_placeholder') @@ -130,7 +127,7 @@ function admin_user() // Active? $html .= ' ' . __('user.active') . '' . "\n"; $html .= $user_goodie_edit - ? html_options('eAktiv', $options, $user_source->state->active) + ? html_options('active', $options, $user_source->state->active) : icon_bool($user_source->state->active); $html .= '' . "\n"; @@ -143,13 +140,22 @@ function admin_user() $html .= '' . "\n"; } + // Forced food? + if (config('enable_force_food')) { + $html .= ' ' . __('Force food') . '' . "\n"; + $html .= auth()->can('user.ff.edit') + ? html_options('force_food', $options, $user_source->state->force_food) + : icon_bool($user_source->state->force_food); + $html .= '' . "\n"; + } + if ($goodie_enabled) { // got goodie? $html .= ' ' - . ($goodie_tshirt ? __('T-shirt') : __('Goodie')) + . __('Goodie') . '' . "\n"; $html .= $user_goodie_edit - ? html_options('eTshirt', $options, $user_source->state->got_goodie) + ? html_options('goodie', $options, $user_source->state->got_goodie) : icon_bool($user_source->state->got_goodie); $html .= '' . "\n"; } @@ -236,6 +242,7 @@ function admin_user() } else { switch ($request->input('action')) { case 'save_groups': + /** @var User $angel */ $angel = User::findOrFail($user_id); if ($angel->id != $user->id || auth()->can('admin_groups')) { /** @var Group $my_highest_group */ @@ -263,6 +270,22 @@ function admin_user() $groupsRequest = []; } + $defaultGroup = auth()->getDefaultRole(); + if ( + !in_array($defaultGroup, $groupsRequest) + && $angel->groups->where('id', $defaultGroup)->count() + ) { + if (!auth()->can('admin_groups') && !config('default_group_removable')) { + $html .= error(__('You cannot remove the default group.'), true); + break; + } else { + $html .= warning( + __('You removed the default group, this has unintended side effects!'), + true + ); + } + } + $angel->groups()->detach(); $user_groups_info = []; foreach ($groupsRequest as $group) { @@ -286,11 +309,15 @@ function admin_user() break; case 'save': - $user_source = User::find($user_id); + /** @var User $user_source */ + $user_source = User::findOrFail($user_id); $changed_email = false; - $email = $request->postData('eemail'); - if (($user_source->email !== $email) && User::whereEmail($email)->exists()) { + $email = $request->postData('mail'); + if ( + $user_source->email !== $email + && User::whereEmail($email)->whereNot('id', $user_source->id)->exists() + ) { $html .= error(__('settings.profile.email.already-taken') . "\n", true); break; } @@ -300,63 +327,77 @@ function admin_user() } $changed_nick = false; - $nick = trim((string) $request->get('eNick')); + $nick = trim((string) $request->get('nick')); $nickValid = (new Username())->validate($nick); - if (($user_source->name !== $nick) && User::whereName($nick)->exists()) { + if ( + $user_source->name !== $nick + && User::whereName($nick)->whereNot('id', $user_source->id)->exists() + ) { $html .= error(__('settings.profile.nick.already-taken') . "\n", true); break; } $old_nick = $user_source->name; if ($nickValid && $user_nick_edit) { - $changed_nick = ($user_source->name !== $nick) || User::whereName($nick)->exists(); + $changed_nick = $user_source->name !== $nick + && !User::whereName($nick)->whereNot('id', $user_source->id)->exists(); $user_source->name = $nick; } $user_source->save(); if (config('enable_full_name')) { - $user_source->personalData->first_name = $request->postData('eVorname'); - $user_source->personalData->last_name = $request->postData('eName'); + $user_source->personalData->first_name = $request->postData('first_name'); + $user_source->personalData->last_name = $request->postData('last_name'); } if ($goodie_tshirt && $user_goodie_edit) { - $user_source->personalData->shirt_size = $request->postData('eSize'); + $user_source->personalData->shirt_size = $request->postData('shirt_size'); } $user_source->personalData->save(); - $user_source->contact->mobile = $request->postData('eHandy'); + $user_source->contact->mobile = $request->postData('mobile'); if (config('enable_dect')) { - $user_source->contact->dect = $request->postData('eDECT'); + $user_source->contact->dect = $request->postData('dect'); } $user_source->contact->save(); if ($goodie_enabled && $user_goodie_edit) { - $user_source->state->got_goodie = $request->postData('eTshirt'); + $user_source->state->got_goodie = $request->postData('goodie'); } if ($user_info_edit) { $user_source->state->user_info = $request->postData('userInfo'); } if ($admin_arrive) { - $user_source->state->arrived = $request->postData('arrive'); + if ($user_source->state->arrived != $request->postData('arrive')) { + if ($request->postData('arrive')) { + $user_source->state->arrival_date = new Carbon(); + } else { + $user_source->state->arrival_date = null; + } + } } if ($user_goodie_edit) { - $user_source->state->active = $request->postData('eAktiv'); + $user_source->state->active = $request->postData('active'); } if (auth()->can('user.fa.edit') && config('enable_force_active')) { $user_source->state->force_active = $request->input('force_active'); } + if (auth()->can('user.ff.edit') && config('enable_force_food')) { + $user_source->state->force_food = $request->input('force_food'); + } $user_source->state->save(); engelsystem_log( 'Updated user: ' . ($changed_nick - ? ('nick modified form ' . $old_nick . ' to ' . $user_source->name) + ? ('nick modified from ' . $old_nick . ' (' . $user_source->id . ') to ' . $user_source->name) : $user_source->name) . ' (' . $user_source->id . ')' - . ($changed_email ? ', email modified' : '') - . ($goodie_tshirt ? ', t-shirt-size: ' . $user_source->personalData->shirt_size : '') + . ($changed_email ? ', e-mail modified' : '') + . ($goodie_tshirt ? ', T-shirt size: ' . $user_source->personalData->shirt_size : '') . ', arrived: ' . $user_source->state->arrived . ', active: ' . $user_source->state->active - . ', force-active: ' . $user_source->state->force_active - . ($goodie_tshirt ? ', t-shirt: ' : ', goodie: ' . $user_source->state->got_goodie) + . (config('enable_force_active') ? (', force-active: ' . $user_source->state->force_active) : '') + . (config('enable_force_food') ? (', force-food: ' . $user_source->state->force_food) : '') + . ($goodie_enabled ? ', goodie: ' . $user_source->state->got_goodie : '') . ($user_info_edit ? ', user-info: ' . $user_source->state->user_info : '') ); $html .= success(__('Changes were saved.') . "\n", true); diff --git a/includes/pages/user_myshifts.php b/includes/pages/user_myshifts.php index 92e621343..f5fd8248d 100644 --- a/includes/pages/user_myshifts.php +++ b/includes/pages/user_myshifts.php @@ -24,7 +24,7 @@ function user_myshifts() if ($request->has('edit')) { $id = $request->input('edit'); $shiftEntry = ShiftEntry::where('id', $id) - ->where('user_id', User::find($request->input('id'))->id) + ->where('user_id', User::find($request->input('id'))?->id) ->first(); $is_angeltype_supporter = $shiftEntry && auth()->user()->isAngelTypeSupporter($shiftEntry->angelType); } diff --git a/includes/pages/user_shifts.php b/includes/pages/user_shifts.php index 30d39dc54..a826e329b 100644 --- a/includes/pages/user_shifts.php +++ b/includes/pages/user_shifts.php @@ -194,7 +194,14 @@ function load_types() NOT `user_angel_type`.`confirm_user_id` IS NULL OR `user_angel_type`.`id` IS NULL ) - ) AS `enabled` + ) AS `enabled`, + ( + `user_angel_type`.`id` IS NOT NULL + AND ( + `angel_types`.`restricted`=0 + OR `user_angel_type`.`confirm_user_id` IS NOT NULL + ) + ) AS `own` FROM `angel_types` LEFT JOIN `user_angel_type` ON ( @@ -204,7 +211,7 @@ function load_types() . ($isShico ? '' : 'WHERE angel_types.hide_on_shift_view = 0 OR user_angel_type.user_id IS NOT NULL ') . - 'ORDER BY `angel_types`.`name` + 'ORDER BY `own` DESC, `angel_types`.`name` ', [ $user->id, @@ -296,7 +303,7 @@ function view_user_shifts() return dateWithEventDay(Carbon::make($value)->format('Y-m-d')); })->toArray(); - $link = button(url('/admin-shifts'), icon('plus-lg'), 'add'); + $link = button(url('/admin-shifts'), icon('plus-lg'), 'btn-sm add'); return page([ div('col-md-12', [ @@ -307,7 +314,7 @@ function view_user_shifts() $locations, $shiftsFilter->getLocations(), 'locations', - icon('pin-map-fill') . __('Locations') + icon('pin-map-fill') . __('location.locations') ), 'start_select' => html_select_key( 'start_day', @@ -327,10 +334,9 @@ function view_user_shifts() $types, $shiftsFilter->getTypes(), 'types', - icon('person-lines-fill') . __('angeltypes.angeltypes') - . ' ', + icon('person-lines-fill') . __('angeltypes.angeltypes') . ' ' + . '' + . icon('question-circle') . '', $ownAngelTypes ), 'filled_select' => make_select( @@ -352,7 +358,8 @@ function view_user_shifts() 'set_next_8h' => __('next 8h'), 'random' => auth()->can('user_shifts') && $canSignUpForShifts ? button( url('/shifts/random'), - icon('dice-4-fill') . __('shifts.random') + icon('shuffle') . __('shifts.random'), + 'btn-primary' ) : '', 'dashboard' => button( public_dashboard_link(), @@ -411,14 +418,16 @@ function make_select($items, $selected, $name, $title = null, $ownSelect = []) $html .= '' . "\n"; $htmlItems = []; - foreach ($items as $i) { - $id = $name . '_' . $i['id']; + foreach ($items as $i => $item) { + $break = isset($item['own'], $items[$i + 1]) && $item['own'] && !$items[$i + 1]['own']; + $id = $name . '_' . $item['id']; $htmlItems[] = '' - . '' . htmlspecialchars($i['name']) . '' - . (!isset($i['enabled']) || $i['enabled'] ? '' : icon('mortarboard-fill')) - . ''; + . '' . htmlspecialchars($item['name']) . '' + . (!isset($item['enabled']) || $item['enabled'] ? '' : icon('mortarboard-fill')) + . '' + . ($break ? '' : ''); } $html .= implode("\n", $htmlItems); diff --git a/includes/sys_form.php b/includes/sys_form.php index 92a798865..e2f8d277e 100644 --- a/includes/sys_form.php +++ b/includes/sys_form.php @@ -60,6 +60,9 @@ function form_datetime(string $name, string $label, $value) { $dom_id = $name . '-datetime'; if ($value) { + if ($value instanceof DateTime) { + $value = Carbon::instance($value); + } $value = ($value instanceof Carbon) ? $value : Carbon::createFromTimestamp($value, Carbon::now()->timezone); } @@ -261,12 +264,12 @@ function form_textarea($name, $label, $value, $disabled = false) * @param string $class * @return string */ -function form_select($name, $label, $values, $selected, $selectText = '', $class = '') +function form_select($name, $label, $values, $selected, $selectText = '', $class = '', $id = '') { return form_element( $label, - html_select_key('form_' . $name, $name, $values, $selected, $selectText), - 'form_' . $name, + html_select_key('form_' . $id ?? $name, $name, $values, $selected, $selectText), + 'form_' . $id ?? $name, $class ); } @@ -302,10 +305,14 @@ function form_element($label, $input, $for = '', $class = '') * @param string $style * @return string */ -function form($elements, $action = '', $style = '', $btnGroup = false) +function form($elements, $action = '', $style = '', $btnGroup = false, $class = null) { + if ($btnGroup) { + $class .= ' btn-group'; + } + return '' . join($elements) . form_csrf() diff --git a/includes/sys_menu.php b/includes/sys_menu.php index d6dd81e3f..f400ba4f6 100644 --- a/includes/sys_menu.php +++ b/includes/sys_menu.php @@ -44,9 +44,16 @@ function make_navigation(): array 'news' => 'news.title', 'user_shifts' => 'general.shifts', 'locations' => ['location.locations', 'locations.view'], + 'questions' => ['question.menu', 'question.add'], ]; - $menu = make_navigation_group($pages); + foreach ($pages as $menu_page => $options) { + $options = (array) $options; + $menu[$options[0]] = [ + url(str_replace('_', '-', $menu_page)), + $options[1] ?? $menu_page, + ]; + } foreach (config('header_items', []) as $title => $options) { $menu[$title] = $options; @@ -66,11 +73,7 @@ function make_navigation(): array 'admin_shifts' => ['Create shifts', 'admin_shifts'], 'admin/shifttypes' => ['shifttype.shifttypes', 'shifttypes.edit'], 'admin/schedule' => ['schedule.import', 'schedule.import'], - ]; - - $admin_pages = [ - // Other admin stuff - 'admin_groups' => ['Group rights', 'admin_groups'], + 'admin/tags' => ['tag.tags', 'tag.edit'], 'admin/logs' => ['log.log', 'admin_log'], 'admin/config' => ['config.config', 'config.edit'], ]; diff --git a/includes/sys_page.php b/includes/sys_page.php index dc9d597de..9bcd841e4 100644 --- a/includes/sys_page.php +++ b/includes/sys_page.php @@ -49,6 +49,18 @@ function parse_date($pattern, $value) return $datetime->getTimestamp(); } +/** + * Send a JSON response. + * + * @param array $data + */ +function json_output(array $data): void +{ + header('Content-Type: application/json; charset=utf-8'); + echo json_encode($data); + die(); +} + /** * Leitet den Browser an die übergebene URL weiter und hält das Script an. * diff --git a/includes/sys_template.php b/includes/sys_template.php index 2bc0573c6..a38563f70 100644 --- a/includes/sys_template.php +++ b/includes/sys_template.php @@ -1,6 +1,7 @@ '; } -function toolbar_dropdown_item(string $href, string $label, bool $active, string $icon = null): string +function toolbar_dropdown_item(string $href, string $label, bool $active, ?string $icon = null): string { return strtr( '{icon} {label}', @@ -315,7 +316,7 @@ function render_table($columns, $rows, $data = true) return info(__('No data found.'), true); } - $html = ''; + $html = ''; $html .= ''; foreach ($columns as $key => $column) { $html .= '' . $column . ''; @@ -420,13 +421,80 @@ function table_buttons($buttons = [], $additionalClass = '') function user_info_icon(User $user): string { - if (!auth()->can('admin_arrive') || !$user->state->user_info) { + if (!auth()->can('user.info.hint') || !$user->state->user_info) { return ''; } $infoIcon = ' can('user.info.show')) { + if (auth()->can('user.info.view')) { $infoIcon .= 'data-bs-toggle="tooltip" title="' . htmlspecialchars($user->state->user_info) . '"'; } $infoIcon .= '>'; return $infoIcon; } + +function pagination(LengthAwarePaginator $paginator, ?int $selectionSteps = null): string +{ + $paginator->appends(request()->getQueryParams()); + + $items = ''; + foreach ($paginator->getUrlRange(1, $paginator->lastPage()) as $page => $url) { + $active = ''; + + if ($paginator->currentPage() == $page) { + $active = ' active'; + } + + $items .= sprintf( + '%u', + $active, + $url, + $page + ); + } + + if ($selectionSteps) { + $selections = []; + foreach ([$selectionSteps, $selectionSteps * 5, $selectionSteps * 10] as $selection) { + $url = $paginator->appends('c', $selection)->url(1); + $selections[] = sprintf( + '%s', + $url, + $selection, + ); + } + $dropdownValue = request()->get('c', $selectionSteps); + $dropdownValue = $dropdownValue == 'all' || !is_numeric($dropdownValue) ? __('All') : $dropdownValue; + $dropdown = sprintf( + ' + %s + + + %s + + + %s + + %s + + + ', + __('Per page'), + $dropdownValue, + implode(PHP_EOL, $selections), + $paginator->appends('c', 'all')->url(1), + __('All'), + ); + } + + return sprintf(' + + + %s + + %s + + + ', $items, $dropdown); +} diff --git a/includes/view/AngelTypes_view.php b/includes/view/AngelTypes_view.php index ed3847518..843c7f708 100644 --- a/includes/view/AngelTypes_view.php +++ b/includes/view/AngelTypes_view.php @@ -1,6 +1,7 @@ name)) - : form_text('name', __('general.name'), $angeltype->name, false, 255), - $supporter_mode - ? form_info(__('angeltypes.restricted'), $angeltype->restricted ? __('Yes') : __('No')) - : form_checkbox( - 'restricted', - __('angeltypes.restricted') . - ' ', - $angeltype->restricted - ), - $supporter_mode - ? form_info(__('shift.self_signup'), $angeltype->shift_self_signup ? __('Yes') : __('No')) - : form_checkbox( - 'shift_self_signup', - __('shift.self_signup') . - ' ', - $angeltype->shift_self_signup - ), - $requires_driving_license, - $requires_ifsg, - $supporter_mode - ? form_info(__('Show on dashboard'), $angeltype->show_on_dashboard ? __('Yes') : __('No')) - : form_checkbox('show_on_dashboard', __('Show on dashboard'), $angeltype->show_on_dashboard), - $supporter_mode - ? form_info(__('Hide at Registration'), $angeltype->hide_register ? __('Yes') : __('No')) - : form_checkbox('hide_register', __('Hide at Registration'), $angeltype->hide_register), - $supporter_mode - ? form_info( - __('angeltypes.hide_on_shift_view'), - $angeltype->hide_on_shift_view ? __('Yes') : __('No') - ) - : form_checkbox( - 'hide_on_shift_view', - __('angeltypes.hide_on_shift_view') . - ' ', - $angeltype->hide_on_shift_view - ), - form_textarea('description', __('general.description'), $angeltype->description), - form_info('', __('Please use markdown for the description.')), - heading(__('Contact'), 3), - form_info( - '', - __('Primary contact person/desk for user questions.') - ), - form_text('contact_name', __('general.name'), $angeltype->contact_name), - config('enable_dect') ? form_text('contact_dect', __('general.dect'), $angeltype->contact_dect) : '', - form_text('contact_email', __('general.email'), $angeltype->contact_email), + div('row', [ + div('col-md-9', [ + $supporter_mode + ? form_info(__('general.name'), htmlspecialchars($angeltype->name)) + : form_text('name', __('general.name'), $angeltype->name, false, 255), + form_textarea('description', __('general.description'), $angeltype->description), + form_info('', __('Please use markdown for the description.')), + heading(__('Contact'), 3), + form_info( + '', + __('Primary contact person/desk for user questions.') + ), + form_text('contact_name', __('general.name'), $angeltype->contact_name), + config('enable_dect') ? form_text('contact_dect', __('general.dect'), $angeltype->contact_dect) : '', + form_text('contact_email', __('general.email'), $angeltype->contact_email), + ]), + + div('col-md-3', [ + heading(__('State'), 3), + $supporter_mode + ? form_info(__('angeltypes.restricted'), $angeltype->restricted ? __('Yes') : __('No')) + : form_checkbox( + 'restricted', + __('angeltypes.restricted') . + ' ', + $angeltype->restricted + ), + $supporter_mode + ? form_info(__('shift.self_signup'), $angeltype->shift_self_signup ? __('Yes') : __('No')) + : form_checkbox( + 'shift_self_signup', + __('shift.self_signup') . + ' ', + $angeltype->shift_self_signup + ), + $requires_driving_license, + $requires_ifsg, + $supporter_mode + ? form_info(__('Show on dashboard'), $angeltype->show_on_dashboard ? __('Yes') : __('No')) + : form_checkbox('show_on_dashboard', __('Show on dashboard'), $angeltype->show_on_dashboard), + $supporter_mode + ? form_info(__('Hide at Registration'), $angeltype->hide_register ? __('Yes') : __('No')) + : form_checkbox('hide_register', __('Hide at Registration'), $angeltype->hide_register), + $supporter_mode + ? form_info( + __('angeltypes.hide_on_shift_view'), + $angeltype->hide_on_shift_view ? __('Yes') : __('No') + ) + : form_checkbox( + 'hide_on_shift_view', + __('angeltypes.hide_on_shift_view') . + ' ', + $angeltype->hide_on_shift_view + ), + ]), + ]), form_submit('submit', icon('save') . __('form.save')), ]), - ] + ], + true ); } @@ -275,6 +285,15 @@ function AngelType_view_buttons( '', __('form.edit') ); + if (config('app_key') && config('join_qr_code', true)) { + $buttons[] = button( + url('/angeltypes/' . $angeltype->id . '/qr'), + icon('qr-code'), + '', + '', + __('general.qr') + ); + } } if ($admin_angeltypes) { $buttons[] = button( @@ -349,12 +368,8 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange ? ($member->personalData->shirt_size ?: '-') : ''; $got_goodie_button_title = $member->state->got_goodie - ? ($goodie_tshirt - ? __('Remove T-shirt') - : __('Remove goodie')) - : ($goodie_tshirt - ? __('user.got_shirt') - : __('user.got_goodie')); + ? __('Remove goodie') + : __('user.got_goodie'); $goodie_actions[] = ($shirtSize !== '-') ? form( [ form_submit( @@ -381,9 +396,7 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange icon('pencil'), 'btn-secondary btn-sm', false, - $goodie_tshirt - ? __('user.edit.shirt') - : __('user.edit.goodie'), + __('user.edit.goodie'), ); if ($goodie_tshirt) { $member['shirt_size'] = isset($tshirt_sizes[$shirtSize]) ? $tshirt_sizes[$shirtSize] : '-'; @@ -445,7 +458,7 @@ function AngelType_view_members(AngelType $angeltype, $members, $admin_user_ange 'supporter' => 0, ]), icon('person-fill-down'), - 'btn-sm', + 'btn-sm btn-danger', '', __('Remove supporter rights'), ), @@ -549,11 +562,10 @@ function AngelType_view_table_headers(AngelType $angeltype, $supporter, $admin_a && auth()->can('angeltype.goodie.list') && auth()->can('user.goodie.edit') ) { - $headers['goodie_actions'] = __('Goodie actions'); if ($goodie_tshirt) { - $headers['goodie_actions'] = __('T-shirt actions'); $headers['shirt_size'] = __('user.shirt_size'); } + $headers['goodie_actions'] = __('Goodie actions'); } $headers['actions'] = ''; @@ -593,7 +605,7 @@ function AngelType_view( $add = (($admin_angeltypes || $admin_user_angeltypes) ? button( url('/user-angeltypes', ['action' => 'add', 'angeltype_id' => $angeltype->id]), icon('plus-lg'), - '', + 'btn-sm', '', __('general.add') ) : ''); @@ -603,7 +615,7 @@ function AngelType_view( AngelType_view_buttons($angeltype, $user_angeltype, $admin_angeltypes, $supporter, $user_license, $user), msg(), tabs([ - __('Info') => AngelType_view_info( + __('general.info') => AngelType_view_info( $angeltype, $members, $admin_user_angeltypes, @@ -666,9 +678,8 @@ function AngelType_view_info( } $info[] = '' . __('general.description') . ''; - $parsedown = new Parsedown(); if ($angeltype->description != '') { - $info[] = $parsedown->parse(htmlspecialchars($angeltype->description)); + $info[] = (new Markdown())->render($angeltype->description); } if ($angeltype->requires_ifsg_certificate && $required_info_show) { $info[] = info(__('angeltype.ifsg.required.info.preview'), true); @@ -780,7 +791,7 @@ function AngelTypes_list_view($angeltypes, bool $admin_angeltypes) $add = button( url('/angeltypes', ['action' => 'edit']), icon('plus-lg'), - '', + 'btn-sm', '', __('general.add') ); diff --git a/includes/view/Locations_view.php b/includes/view/Locations_view.php index 6e646b343..f88fdb571 100644 --- a/includes/view/Locations_view.php +++ b/includes/view/Locations_view.php @@ -1,5 +1,6 @@ description) { $description = '' . __('general.description') . ''; - $parsedown = new Parsedown(); - $description .= $parsedown->parse(htmlspecialchars($location->description)); + $description .= (new Markdown())->render($location->description); } $neededAngelTypes = ''; diff --git a/includes/view/ShiftCalendarShiftRenderer.php b/includes/view/ShiftCalendarShiftRenderer.php index cc1181c97..7e19ddf5f 100644 --- a/includes/view/ShiftCalendarShiftRenderer.php +++ b/includes/view/ShiftCalendarShiftRenderer.php @@ -271,7 +271,7 @@ private function renderShiftHead(Shift $shift, $class, $needed_angeltypes_count) form_submit( 'delete', icon('trash'), - 'btn-' . $class . ' btn-sm border-light text-white ms-1', + 'btn-sm border-light text-white ms-1', false, 'danger', __('form.delete'), diff --git a/includes/view/ShiftEntry_view.php b/includes/view/ShiftEntry_view.php index acaad4bff..40c0946ba 100644 --- a/includes/view/ShiftEntry_view.php +++ b/includes/view/ShiftEntry_view.php @@ -212,15 +212,12 @@ function ShiftEntry_edit_view( $freeload_form = []; $goodie = GoodieType::from(config('goodie_type')); $goodie_enabled = $goodie !== GoodieType::None; - $goodie_tshirt = $goodie === GoodieType::Tshirt; if ($user_admin_shifts || $angeltype_supporter) { if (!$goodie_enabled) { $freeload_info = __('freeload.freeloaded.info', [config('max_freeloadable_shifts')]); } else { - $freeload_info = __('freeload.freeloaded.info.goodie', [($goodie_tshirt - ? __('T-shirt score') - : __('Goodie score')), + $freeload_info = __('freeload.freeloaded.info.goodie', [__('Goodie score'), config('max_freeloadable_shifts')]); } $freeload_form = [ diff --git a/includes/view/Shifts_view.php b/includes/view/Shifts_view.php index be139cada..11c5f925b 100644 --- a/includes/view/Shifts_view.php +++ b/includes/view/Shifts_view.php @@ -2,6 +2,7 @@ use Engelsystem\Config\GoodieType; use Engelsystem\Helpers\Carbon; +use Engelsystem\Helpers\Markdown; use Engelsystem\Models\AngelType; use Engelsystem\Models\Location; use Engelsystem\Models\Shifts\Shift; @@ -85,18 +86,20 @@ function Shift_editor_info_render(Shift $shift) ? __( 'shift.angeltype_source.shift_type', [ - '' - . htmlspecialchars($shift->schedule->name) - . '', + auth()->can('schedule.import') ? + '' + . htmlspecialchars($shift->schedule->name) + . '' : htmlspecialchars($shift->schedule->name), '' . htmlspecialchars($shift->shiftType->name) . '', ] ) : __('shift.angeltype_source.location', [ - '' - . htmlspecialchars($shift->schedule->name) - . '', + auth()->can('schedule.import') ? + '' + . htmlspecialchars($shift->schedule->name) + . '' : htmlspecialchars($shift->schedule->name), location_name_render($shift->location), ]); } else { @@ -159,9 +162,13 @@ function Shift_view( $nightShiftsConfig = config('night_shifts'); $goodie = GoodieType::from(config('goodie_type')); $goodie_enabled = $goodie !== GoodieType::None; - $goodie_tshirt = $goodie === GoodieType::Tshirt; - $parsedown = new Parsedown(); + $supportsAngelTypes = auth()->user() + ->userAngelTypes() + ->where('supporter', true) + ->pluck('angel_types.id'); + + $mdRenderer = new Markdown(); $angeltypes = []; foreach ($angeltypes_source as $angeltype) { @@ -171,7 +178,7 @@ function Shift_view( $needed_angels = ''; $neededAngels = new Collection($shift->neededAngels); foreach ($neededAngels as $needed_angeltype) { - $needed_angels .= Shift_view_render_needed_angeltype($needed_angeltype, $angeltypes, $shift, $user_shift_admin); + $needed_angels .= Shift_view_render_needed_angeltype($needed_angeltype, $angeltypes, $shift, $user_shift_admin, $supportsAngelTypes); } $shiftEntry = $shift->shiftEntries; @@ -189,31 +196,12 @@ function Shift_view( ->where('angel_type_id', $type) ->whereNull('freeloaded_by') ->count(), - ], $angeltypes, $shift, $user_shift_admin); + ], $angeltypes, $shift, $user_shift_admin, $supportsAngelTypes); } } $content = [msg()]; - if ($shift_signup_state->getState() === ShiftSignupStatus::COLLIDES) { - $content[] = info(__('This shift collides with one of your shifts.'), true); - } - - if ($shift_signup_state->getState() === ShiftSignupStatus::SIGNED_UP) { - $content[] = info(__('You are signed up for this shift.') - . (($shift->start->subHours(config('last_unsubscribe')) < Carbon::now() && $shift->end > Carbon::now()) - ? ' ' . __('shift.sign_out.hint', [config('last_unsubscribe')]) - : ''), true); - } - - $signupAdvanceSeconds = ($shift->shiftType->signup_advance_hours ?: config('signup_advance_hours')) * 3600; - if ($signupAdvanceSeconds && $shift->start->timestamp > time() + $signupAdvanceSeconds) { - $content[] = info(sprintf( - __('This shift is in the far future. It becomes available for signup at %s.'), - date(__('general.datetime'), $shift->start->timestamp - $signupAdvanceSeconds) - ), true); - } - $buttons = []; if ($shift_admin || $shiftTypesEdit || $locationsEdit) { $buttons = [ @@ -252,18 +240,31 @@ function Shift_view( user_link(auth()->user()->id), ' ' . __('profile.my_shifts') ); - $content[] = buttons($buttons); + $nextShift = $shift->nextShift(); + $previousShift = $shift->previousShift(); + + $navigationButtons = ''; + if ($nextShift || $previousShift) { + $navigationButtons = table_buttons([ + $previousShift ? button(url('/shifts', ['action' => 'view', 'shift_id' => $previousShift->id]), __('shift.previous')) : '', + $nextShift ? button(url('/shifts', ['action' => 'view', 'shift_id' => $nextShift->id]), __('shift.next')) : '', + ], 'mb-2'); + } + + $content[] = $navigationButtons ? div('row', [div('col-md-12', [table_buttons($buttons, 'mb-2'), $navigationButtons])]) : buttons($buttons); $content[] = Shift_view_header($shift, $location); + $content[] = div('row', [ div('col-sm-6', [ + Shift_view_alert_render($shift, $shift_signup_state), '' . __('Needed angels') . '', '' . $needed_angels . '', ]), div('col-sm-6', [ '' . __('general.description') . '', - $parsedown->parse(htmlspecialchars($shifttype->description)), - $parsedown->parse(htmlspecialchars($shift->description)), + $mdRenderer->render($shifttype->description), + $mdRenderer->render($shift->description), ]), ]); @@ -276,11 +277,11 @@ function Shift_view( $night_shift_hint = ''; if ($shift->isNightShift() && $goodie_enabled) { $night_shift_hint = ' '; } $link = button(url('/user-shifts'), icon('chevron-left'), 'btn-sm', '', __('general.back')); @@ -293,17 +294,59 @@ function Shift_view( ); } +/** + * Checks whether the user is elligible and able to sign up for this shift and + * generates an appropriate alert if not. + * + * @param Shift $shift + * @param Engelsystem\ShiftSignupState $shift_signup_state + * @return string '' if no alert is necessary, alert div otherwise + */ +function Shift_view_alert_render( + Shift $shift, + ShiftSignupState $shift_signup_state +) { + $alert = ''; + + if ($shift_signup_state->getState() === ShiftSignupStatus::COLLIDES) { + $alert = warning(__('This shift collides with one of your shifts.'), true); + } + + if ($shift_signup_state->getState() === ShiftSignupStatus::SIGNED_UP) { + $alert = info(__('You are signed up for this shift.') + . (($shift->start->subHours(config('last_unsubscribe')) < Carbon::now() && $shift->end > Carbon::now()) + ? ' ' . __('shift.sign_out.hint', [config('last_unsubscribe')]) + : ''), true); + } + + $signupAdvanceSeconds = ($shift->shiftType->signup_advance_hours ?: config('signup_advance_hours')) * 3600; + if ($signupAdvanceSeconds && $shift->start->timestamp > time() + $signupAdvanceSeconds) { + $alert = info(sprintf( + __('This shift is in the far future. It becomes available for signup at %s.'), + date(__('general.datetime'), $shift->start->timestamp - $signupAdvanceSeconds) + ), true); + } + + return $alert; +} + /** * @param array $needed_angeltype * @param AngelType[]|Collection $angeltypes * @param Shift $shift * @param bool $user_shift_admin + * @param Collection|int[] $supportsAngelTypes * @return string */ -function Shift_view_render_needed_angeltype($needed_angeltype, $angeltypes, Shift $shift, $user_shift_admin) -{ +function Shift_view_render_needed_angeltype( + $needed_angeltype, + $angeltypes, + Shift $shift, + $user_shift_admin, + $supportsAngelTypes +) { $angeltype = $angeltypes[$needed_angeltype['angel_type_id']]; - $angeltype_supporter = auth()->user()->isAngelTypeSupporter($angeltype) + $angeltype_supporter = $supportsAngelTypes->contains($needed_angeltype['angel_type_id']) || auth()->can('admin_user_angeltypes'); $needed_angels = ''; @@ -333,7 +376,13 @@ function Shift_view_render_needed_angeltype($needed_angeltype, $angeltypes, Shif $angels = []; foreach ($shift->shiftEntries as $shift_entry) { if ($shift_entry->angel_type_id == $needed_angeltype['angel_type_id']) { - $angels[] = Shift_view_render_shift_entry($shift_entry, $user_shift_admin, $angeltype_supporter, $shift); + $angels[] = Shift_view_render_shift_entry( + $shift_entry, + $user_shift_admin, + $angeltype_supporter, + $shift, + $supportsAngelTypes + ); } } @@ -348,10 +397,16 @@ function Shift_view_render_needed_angeltype($needed_angeltype, $angeltypes, Shif * @param bool $user_shift_admin * @param bool $angeltype_supporter * @param Shift $shift + * @param Collection|int[] $supportsAngelTypes * @return string */ -function Shift_view_render_shift_entry(ShiftEntry $shift_entry, $user_shift_admin, $angeltype_supporter, Shift $shift) -{ +function Shift_view_render_shift_entry( + ShiftEntry $shift_entry, + $user_shift_admin, + $angeltype_supporter, + Shift $shift, + $supportsAngelTypes +) { $entry = User_Nick_render($shift_entry->user); if ($shift_entry->freeloaded_by) { $entry = '' . $entry . ''; @@ -366,13 +421,15 @@ function Shift_view_render_shift_entry(ShiftEntry $shift_entry, $user_shift_admi __('form.edit') ); $angeltype = $shift_entry->angelType; - $disabled = Shift_signout_allowed($shift, $angeltype, $shift_entry->user_id) ? '' : ' btn-disabled'; + $isAngeltypeSupporter = $supportsAngelTypes->contains($angeltype->id); + $signOutAllowed = Shift_signout_allowed($shift, $angeltype, $shift_entry->user_id, $isAngeltypeSupporter); + $disabled = $signOutAllowed ? '' : ' btn-disabled'; $entry .= button_icon( shift_entry_delete_link($shift_entry), 'trash', 'btn-sm btn-danger' . $disabled, __('form.delete'), - !Shift_signout_allowed($shift, $angeltype, $shift_entry->user_id) + !$signOutAllowed ); $entry .= ''; } diff --git a/includes/view/UserAngelTypes_view.php b/includes/view/UserAngelTypes_view.php index a1e093f23..cf69774f5 100644 --- a/includes/view/UserAngelTypes_view.php +++ b/includes/view/UserAngelTypes_view.php @@ -151,7 +151,10 @@ function UserAngelType_add_view(AngelType $angeltype, $users_select, $user_id) $angeltype->restricted ? form_checkbox('auto_confirm_user', __('Confirm user'), true) : '', - form_select('user_id', __('general.user'), $users_select, $user_id), + auth()->can('admin_angel_types') || config('supporters_can_promote') + ? form_checkbox('set_supporter', __('Supporter'), false) + : '', + form_select('user_id', __('general.user'), $users_select, $user_id, '', '', 'user_angel_type_add_user_id'), form_submit('submit', icon('plus-lg') . __('general.add')), ]), ]); diff --git a/includes/view/User_view.php b/includes/view/User_view.php index 9ddafd9ad..79e3875a3 100644 --- a/includes/view/User_view.php +++ b/includes/view/User_view.php @@ -2,6 +2,7 @@ use Carbon\Carbon; use Engelsystem\Config\GoodieType; +use Engelsystem\Helpers\UserVouchers; use Engelsystem\Models\AngelType; use Engelsystem\Models\Group; use Engelsystem\Models\Shifts\Shift; @@ -9,6 +10,7 @@ use Engelsystem\Models\User\PasswordReset; use Engelsystem\Models\User\User; use Engelsystem\Models\Worklog; +use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -35,44 +37,16 @@ function User_delete_view($user) } /** - * View for editing the number of given vouchers - * - * @param User $user - * @return string - */ -function User_edit_vouchers_view($user) -{ - $link = button(user_link($user->id), icon('chevron-left'), 'btn-sm', '', __('general.back')); - return page_with_title( - $link . ' ' . sprintf(__('%s\'s vouchers'), User_Nick_render($user)), - [ - msg(), - info(sprintf( - $user->state->force_active && config('enable_force_active') - ? __('Angel can receive another %d vouchers and is FA.') - : __('Angel can receive another %d vouchers.'), - User_get_eligable_voucher_count($user) - ), true), - form( - [ - form_spinner('vouchers', __('Number of vouchers given out'), $user->state->got_voucher), - form_submit('submit', icon('save') . __('form.save')), - ], - url('/users', ['action' => 'edit_vouchers', 'user_id' => $user->id]) - ), - ] - ); -} - -/** - * @param User[] $users + * @param User[]|LengthAwarePaginator $users * @param string $order_by * @param int $arrived_count * @param int $active_count * @param int $force_active_count + * @param int $force_food_count * @param int $freeloads_count * @param int $goodies_count * @param int $voucher_count + * @param bool $admin_user_privilege * @return string */ function Users_view( @@ -81,15 +55,57 @@ function Users_view( $arrived_count, $active_count, $force_active_count, + $force_food_count, $freeloads_count, $goodies_count, - $voucher_count + $voucher_count, + $admin_user_privilege ) { + $auth = auth(); $goodie = GoodieType::from(config('goodie_type')); $goodie_enabled = $goodie !== GoodieType::None; $goodie_tshirt = $goodie === GoodieType::Tshirt; $usersList = []; foreach ($users as $user) { + $voucher_field = ''; + + if (config('enable_voucher')) { + $voucher_template = << + + {issued} / {eligible} + + {plus_one} + +EOT; + + if ($admin_user_privilege || $auth->can('voucher.edit')) { + $plus_one = << + +1 + +EOT; + + $voucher_template = strtr($voucher_template, [ + '{plus_one}' => $plus_one, + ]); + } + + $voucher_field = strtr( + $voucher_template, + [ + '{issued}' => $user->state->got_voucher, + '{eligible}' => $user->state->got_voucher + UserVouchers::eligibleVoucherCount($user), + '{user}' => $user->id, + '{amount}' => $user->state->got_voucher + 1, + ] + ); + } + $u = []; $u['name'] = User_Nick_render($user) . User_Pronoun_render($user) @@ -99,11 +115,16 @@ function Users_view( $u['dect'] = sprintf('%1$s', htmlspecialchars((string) $user->contact->dect)); $u['arrived'] = icon_bool($user->state->arrived); if (config('enable_voucher')) { - $u['got_voucher'] = $user->state->got_voucher; + $u['got_voucher'] = $voucher_field; } $u['freeloads'] = $user->getAttribute('freeloads'); $u['active'] = icon_bool($user->state->active); - $u['force_active'] = icon_bool($user->state->force_active); + if (config('enable_force_active')) { + $u['force_active'] = icon_bool($user->state->force_active); + } + if (config('enable_force_food')) { + $u['force_food'] = icon_bool($user->state->force_food); + } if ($goodie_enabled) { $u['got_goodie'] = icon_bool($user->state->got_goodie); if ($goodie_tshirt) { @@ -132,9 +153,10 @@ function Users_view( $usersList[] = [ 'name' => '' . __('Sum') . '', 'arrived' => $arrived_count, - 'got_voucher' => $voucher_count, + 'got_voucher' => '' . $voucher_count . '', 'active' => $active_count, 'force_active' => $force_active_count, + 'force_food' => $force_food_count, 'freeloads' => $freeloads_count, 'got_goodie' => $goodies_count, 'actions' => '' . count($usersList) . '', @@ -154,19 +176,20 @@ function Users_view( } $user_table_headers['arrived'] = Users_table_header_link('arrived', __('Arrived'), $order_by); if (config('enable_voucher')) { - $user_table_headers['got_voucher'] = Users_table_header_link('got_voucher', __('Voucher'), $order_by); + $user_table_headers['got_voucher'] = Users_table_header_link('got_voucher', __('voucher.vouchers'), $order_by); } $user_table_headers['freeloads'] = Users_table_header_link('freeloads', __('Freeloads'), $order_by); $user_table_headers['active'] = Users_table_header_link('active', __('user.active'), $order_by); if (config('enable_force_active')) { $user_table_headers['force_active'] = Users_table_header_link('force_active', __('Forced'), $order_by); } + if (config('enable_force_food')) { + $user_table_headers['force_food'] = Users_table_header_link('force_food', __('Food'), $order_by); + } if ($goodie_enabled) { + $user_table_headers['got_goodie'] = Users_table_header_link('got_goodie', __('Goodie'), $order_by); if ($goodie_tshirt) { - $user_table_headers['got_goodie'] = Users_table_header_link('got_goodie', __('T-Shirt'), $order_by); $user_table_headers['shirt_size'] = Users_table_header_link('shirt_size', __('Size'), $order_by); - } else { - $user_table_headers['got_goodie'] = Users_table_header_link('got_goodie', __('Goodie'), $order_by); } } $user_table_headers['arrival_date'] = Users_table_header_link( @@ -186,10 +209,16 @@ function Users_view( unset($user_table_headers[$key]); } - $link = button(url('/register'), icon('plus-lg'), 'add'); + $pagination = ''; + if ($users instanceof LengthAwarePaginator) { + $pagination = pagination($users, config('display_users')); + } + $link = button(url('/register'), icon('plus-lg'), 'btn-sm add'); return page_with_title(__('All users') . ' ' . $link, [ msg(), + $pagination, table($user_table_headers, $usersList), + $pagination, ]); } @@ -251,7 +280,7 @@ function User_shift_state_render($user) . ''; } - return '' + return '' . __('Shift ends %c') . ''; } @@ -306,14 +335,14 @@ function User_view_shiftentries($needed_angel_type) * @param Shift $shift * @param User $user_source * @param bool $its_me + * @param bool $supporter * @return array */ -function User_view_myshift(Shift $shift, $user_source, $its_me) +function User_view_myshift(Shift $shift, $user_source, $its_me, $supporter) { $nightShiftsConfig = config('night_shifts'); $goodie = GoodieType::from(config('goodie_type')); $goodie_enabled = $goodie !== GoodieType::None; - $goodie_tshirt = $goodie === GoodieType::Tshirt; $supporter = auth()->user()->isAngelTypeSupporter(AngelType::findOrFail($shift->angel_type_id)); $shift_info = '' . htmlspecialchars($shift->shiftType->name) . ''; @@ -324,16 +353,11 @@ function User_view_myshift(Shift $shift, $user_source, $its_me) $shift_info .= User_view_shiftentries($needed_angel_type); } + $shift_info = div('table-myshifts-shift-info-limit-height', $shift_info); + $night_shift = ''; if ($shift->isNightShift() && $goodie_enabled) { - $night_shift = ' '; + $night_shift = render_night_shift_hint($nightShiftsConfig); } $myshift = [ 'date' => icon('calendar-event') @@ -373,9 +397,7 @@ function User_view_myshift(Shift $shift, $user_source, $its_me) if (!$goodie_enabled) { $freeload_info = __('freeload.info'); } else { - $freeload_info = __('freeload.info.goodie', [($goodie_tshirt - ? __('T-shirt score') - : __('Goodie score'))]); + $freeload_info = __('freeload.info.goodie', [__('Goodie score')]); } $myshift['hints'] .= ' ' . table_buttons([ @@ -536,13 +561,18 @@ function User_view_worklog(Worklog $worklog, $admin_user_worklog_privilege, $its ]) . ''; } + $night_shift = ''; + if ($worklog->night_shift && $goodie_enabled && $nightShiftsConfig['enabled']) { + $night_shift = render_night_shift_hint($nightShiftsConfig); + } + return [ 'date' => icon('calendar-event') . date(__('general.date'), $worklog->worked_at->timestamp), 'duration' => sprintf('%.2f', $worklog->hours) . ' h', - 'hints' => '', + 'hints' => $night_shift, 'location' => '', 'shift_info' => __('Work log entry'), - 'comment' => htmlspecialchars($worklog->comment) . '' + 'comment' => htmlspecialchars($worklog->description) . '' . sprintf( __('Added by %s at %s'), User_Nick_render($worklog->creator), @@ -589,16 +619,21 @@ function User_view( ) { $goodie = GoodieType::from(config('goodie_type')); $goodie_enabled = $goodie !== GoodieType::None; - $goodie_tshirt = $goodie === GoodieType::Tshirt; $auth = auth(); $nightShiftsConfig = config('night_shifts'); $user_name = htmlspecialchars((string) $user_source->personalData->first_name) . ' ' . htmlspecialchars((string) $user_source->personalData->last_name); $myshifts_table = ''; + $supported_angeltypes = auth()->user() + ->userAngelTypes() + ->where('supporter', true) + ->pluck('angel_types.id'); $user_angeltypes_supporter = false; foreach ($user_source->userAngelTypes as $user_angeltype) { - $user_angeltypes_supporter = $user_angeltypes_supporter - || $auth->user()->isAngelTypeSupporter($user_angeltype); + if ($supported_angeltypes->contains($user_angeltype->id)) { + $user_angeltypes_supporter = true; + break; + } } if ($its_me || $admin_user_privilege || $goodie_admin || $user_angeltypes_supporter) { @@ -609,16 +644,16 @@ function User_view( $goodie_score, $goodie_admin, $user_worklogs, - $admin_user_worklog_privilege + $admin_user_worklog_privilege, ); if (count($my_shifts) > 0) { - $myshifts_table = div('table-responsive', table([ + $myshifts_table = div('', table([ 'date' => __('Day & Time'), 'duration' => __('Duration'), 'hints' => '', 'location' => __('Location'), 'shift_info' => __('Name & Workmates'), - 'comment' => __('worklog.comment'), + 'comment' => __('worklog.description'), 'actions' => __('general.actions'), ], $my_shifts)); } elseif ($user_source->state->force_active && config('enable_force_active')) { @@ -657,7 +692,7 @@ function User_view( table_buttons([ $auth->can('user.goodie.edit') && $goodie_enabled ? button( url('/admin/user/' . $user_source->id . '/goodie'), - icon('gift') . ($goodie_tshirt ? __('T-shirt') : __('Goodie')) + icon('gift') . __('Goodie') ) : '', $admin_user_privilege ? button( url('/admin-user', ['id' => $user_source->id]), @@ -667,15 +702,12 @@ function User_view( form([ form_hidden('action', 'arrived'), form_hidden('user', $user_source->id), - form_submit('submit', icon('house') . __('user.arrive'), '', false), + form_submit('send', icon('house') . __('user.arrive'), '', false), ], url('/admin-arrive'), 'float:left') : '', ($admin_user_privilege || $auth->can('voucher.edit')) && config('enable_voucher') ? button( - url( - '/users', - ['action' => 'edit_vouchers', 'user_id' => $user_source->id] - ), - icon('valentine') . __('Vouchers') + url('/admin/user/' . $user_source->id . '/voucher'), + icon('valentine') . __('voucher.vouchers') ) : '', ( @@ -685,6 +717,11 @@ function User_view( url('/users/' . $user_source->id . '/certificates'), icon('card-checklist') . __('settings.certificates') ) : '', + $auth->can(['admin_log', 'logs.all']) ? + form([ + form_hidden('search_user_id', $user_source->id), + form_submit('submit', icon('journal-text') . __('log.log'), '', false, 'secondary'), + ], url('/admin/logs')) : '', ($admin_user_worklog_privilege && $self_worklog) ? button( url('/admin/user/' . $user_source->id . '/worklog'), icon('clock-history') . __('worklog.add') @@ -748,12 +785,11 @@ function User_view( ($its_me && $nightShiftsConfig['enabled'] && $goodie_enabled) ? info( icon('moon-stars') . __( - 'Night shifts between %d and %d am are multiplied by %d for the %s score.', + 'Night shifts between %d and %d am are multiplied by %d for the goodie score.', [ $nightShiftsConfig['start'], $nightShiftsConfig['end'], $nightShiftsConfig['multiplier'], - ($goodie_tshirt ? __('T-shirt') : __('goodie')), ] ), true, @@ -780,111 +816,83 @@ function User_view( */ function User_view_state($admin_user_privilege, $freeloader, $user_source) { - if ($admin_user_privilege) { - $state = User_view_state_admin($freeloader, $user_source); - } else { - $state = User_view_state_user($user_source); - } - - return div('col-md-2', [ - heading(__('State'), 4), - join('', $state), - ]); -} - -/** - * Render the state section of user view for users. - * - * @param User $user_source - * @return array - */ -function User_view_state_user($user_source) -{ - $state = [ - User_shift_state_render($user_source), - ]; - - if ($user_source->state->arrived) { - $state[] = '' . icon('house') . __('user.arrived') . ''; - } else { - $state[] = '' . __('Not arrived') . ''; - } - - return $state; -} - - -/** - * Render the state section of user view for admins. - * - * @param bool $freeloader - * @param User $user_source - * @return array - */ -function User_view_state_admin($freeloader, $user_source) -{ - $state = []; $goodie = GoodieType::from(config('goodie_type')); $goodie_enabled = $goodie !== GoodieType::None; - $goodie_tshirt = $goodie === GoodieType::Tshirt; $password_reset = PasswordReset::whereUserId($user_source->id) ->where('created_at', '>', $user_source->last_login_at ?: '') ->count(); + $state = []; - if ($freeloader) { + if ($freeloader && $admin_user_privilege) { $state[] = '' . icon('exclamation-circle') . __('Freeloader') . ''; } $state[] = User_shift_state_render($user_source); if ($user_source->state->arrived) { - $state[] = '' . icon('house') - . sprintf( - __('Arrived at %s'), - $user_source->state->arrival_date ? $user_source->state->arrival_date->format(__('general.date')) : '' - ) - . ''; + if ($admin_user_privilege) { + $state[] = '' . icon('house') + . sprintf( + __('Arrived at %s'), + $user_source->state->arrival_date + ? $user_source->state->arrival_date->format(__('general.date')) : '' + ) + . ''; - if ($user_source->state->force_active && config('enable_force_active')) { - $state[] = '' . __('user.force_active') . ''; - } elseif ($user_source->state->active) { - $state[] = '' . __('user.active') . ''; - } - if ($user_source->state->got_goodie && $goodie_enabled) { - $state[] = '' . ($goodie_tshirt ? __('T-shirt') : __('Goodie')) . ''; + if ($user_source->state->force_active && config('enable_force_active')) { + $state[] = '' . __('user.force_active') . ''; + } elseif ($user_source->state->active) { + $state[] = '' . __('user.active') . ''; + } + if ($user_source->state->force_food && config('enable_force_food')) { + $state[] = '' . __('user.force_food') . ''; + } + if ($user_source->state->got_goodie && $goodie_enabled) { + $state[] = '' . __('Goodie') . ''; + } + } else { + $state[] = '' . icon('house') . __('user.arrived') . ''; } } else { - $arrivalDate = $user_source->personalData->planned_arrival_date; - $state[] = '' - . ($arrivalDate ? sprintf( - __('Not arrived (Planned: %s)'), - $arrivalDate->format(__('general.date')) - ) : __('Not arrived')) - . ''; + if ($admin_user_privilege) { + $arrivalDate = $user_source->personalData->planned_arrival_date; + $state[] = '' + . ($arrivalDate ? sprintf( + __('Not arrived (Planned: %s)'), + $arrivalDate->format(__('general.date')) + ) : __('Not arrived')) + . ''; + } else { + $state[] = '' . __('Not arrived') . ''; + } } if (config('enable_voucher')) { $voucherCount = $user_source->state->got_voucher; - $availableCount = $voucherCount + User_get_eligable_voucher_count($user_source); - $availableCount = max($voucherCount, $availableCount); - if ($user_source->state->got_voucher > 0) { - $state[] = '' - . icon('valentine') - . __('Got %s of %s vouchers', [$voucherCount, $availableCount]) - . ''; - } else { - $state[] = '' - . __('Got no vouchers') - . ($availableCount ? ' (' . __('out of %s', [$availableCount]) . ')' : '') - . ''; + $availableCount = $voucherCount + UserVouchers::eligibleVoucherCount($user_source); + $availableVoucher = $availableCount; + if ( + (config('enable_force_active') && $user_source->state->force_active) + || config('enable_force_food') && $user_source->state->force_food + ) { + $availableVoucher = __('user.state.vouchers.force', [$availableCount]); } + $state[] = '' + . icon('valentine') + . __('user.state.vouchers', [$voucherCount, $availableVoucher]) + . ''; } - if ($password_reset) { + if ($password_reset && $admin_user_privilege) { $state[] = __('Password reset in progress'); } - return $state; + return div('col-md-2', [ + heading(__('State'), 4), + join('', $state), + ]); } /** @@ -973,7 +981,7 @@ function User_Nick_render($user, $plain = false) } return render_profile_link( - ' ' . htmlspecialchars($user->displayName) . '', + ' ' . htmlspecialchars($user->displayName) . '', $user->id, ($user->state->arrived ? '' : 'text-muted') ); @@ -1157,3 +1165,14 @@ function render_user_mobile_hint() return null; } + +function render_night_shift_hint(mixed $nightShiftsConfig): string +{ + return ' '; +} diff --git a/package.json b/package.json index 7d30225f3..1e4fbe142 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "engelsystem", "version": "1.1.0", "main": "index.js", - "repository": "https://github.com/engelsystem/engelsystem.git", + "repository": "github:engelsystem/engelsystem", "author": "https://github.com/engelsystem/engelsystem/contributors", "license": "GPL-2.0-or-later", "scripts": { @@ -48,8 +48,5 @@ "webpack": "^5.94.0", "webpack-cli": "^5.0.1", "webpack-manifest-plugin": "^5.0.0" - }, - "resolutions": { - "semver": "7.5.3" } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index b9fd6afe0..3fe5fde95 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -11,5 +11,5 @@ parameters: ignoreErrors: - message: '#.*#' - path: config/config.php + path: config/config.php* reportUnmatched: false diff --git a/resources/api/openapi.yml b/resources/api/openapi.yml index c28421f62..82f76255e 100644 --- a/resources/api/openapi.yml +++ b/resources/api/openapi.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: - version: 0.1.0-beta + version: 0.2.0-beta title: Engelsystem description: > This API is as stable as a **beta** version might be. @@ -40,6 +40,8 @@ tags: description: Shift types - name: user description: Users + - name: worklog + description: Worklogs security: - bearer-auth: [ ] @@ -110,22 +112,37 @@ components: - url UserAngelType: allOf: - - $ref: '#/components/schemas/AngelType' - type: object properties: - confirmed: - type: boolean - example: true - description: > - If the user is confirmed - (either by the angel type not requiring confirmation, being a supporter or being confirmed by one). - supporter: - type: boolean - example: false - description: If the user is a supporter of the angel type. + angeltype: + $ref: '#/components/schemas/Reference' + required: + - angeltype + - $ref: '#/components/schemas/UserAngelTypeRelation' + AngelTypeUser: + allOf: + - type: object + properties: + user: + $ref: '#/components/schemas/Reference' required: - - confirmed - - supporter + - user + - $ref: '#/components/schemas/UserAngelTypeRelation' + UserAngelTypeRelation: + properties: + confirmed: + type: boolean + example: true + description: > + If the user is confirmed + (either by the angel type not requiring confirmation, being a supporter or being confirmed by one). + supporter: + type: boolean + example: false + description: If the user is a supporter of the angel type. + required: + - confirmed + - supporter News: type: object properties: @@ -240,6 +257,11 @@ components: $ref: '#/components/schemas/Reference' shift_type: $ref: '#/components/schemas/Reference' + schedule_guid: + type: string + format: uuid + nullable: true + description: schedule.xml event guid created_at: $ref: '#/components/schemas/DateTimeOptional' updated_at: @@ -261,6 +283,7 @@ components: - ends_at - location - shift_type + - schedule_guid - created_at - updated_at - needed_angel_types @@ -383,6 +406,36 @@ components: - dates - language - arrived + Worklog: + type: object + properties: + id: + type: integer + example: 42 + description: + type: string + example: Main stage build-up + hours: + type: number + format: float + example: 4.23 + description: Hours worked with up to two decimals precision + created_by: + $ref: '#/components/schemas/Reference' + worked_at: + $ref: '#/components/schemas/DateTime' + created_at: + $ref: '#/components/schemas/DateTimeOptional' + updated_at: + $ref: '#/components/schemas/DateTimeOptional' + required: + - id + - description + - hours + - created_by + - worked_at + - created_at + - updated_at DateTime: type: string @@ -503,7 +556,7 @@ paths: - name: id in: path required: true - description: The angel type identifier + description: Angel type identifier example: 42 schema: type: integer @@ -524,6 +577,45 @@ paths: type: array items: $ref: '#/components/schemas/Shift' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + + /angeltypes/{id}/users: + parameters: + - name: id + in: path + required: true + description: Angel type identifier + example: 42 + schema: + type: integer + get: + tags: + - angel type + - user + summary: Get all users of the requested angel type + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/AngelTypeUser' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' /news: get: @@ -574,7 +666,7 @@ paths: - name: id in: path required: true - description: The locations identifier + description: Location identifier example: 42 schema: type: integer @@ -629,7 +721,7 @@ paths: - name: id in: path required: true - description: The shift types identifier + description: Shift type identifier example: 42 schema: type: integer @@ -657,12 +749,34 @@ paths: '404': $ref: '#/components/responses/NotFoundError' + /users: + get: + tags: + - user + summary: Get a list of all users + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Reference' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + /users/{id}: parameters: - name: id in: path required: true - description: The user identifier or `self` + description: User identifier or `self` example: 42 schema: oneOf: @@ -696,7 +810,7 @@ paths: - name: id in: path required: true - description: The user identifier or `self` + description: User identifier or `self` example: 42 schema: oneOf: @@ -731,7 +845,7 @@ paths: - name: id in: path required: true - description: The user identifier or `self` + description: User identifier or `self` example: 42 schema: oneOf: @@ -761,6 +875,41 @@ paths: '404': $ref: '#/components/responses/NotFoundError' + /users/{id}/worklogs: + parameters: + - name: id + in: path + required: true + description: User identifier or `self` + example: 42 + schema: + oneOf: + - type: string + - type: integer + get: + tags: + - worklog + - user + summary: Get all worklogs of the requested user + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Worklog' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + /info: get: tags: @@ -780,8 +929,6 @@ paths: $ref: '#/components/responses/UnauthorizedError' '403': $ref: '#/components/responses/ForbiddenError' - '404': - $ref: '#/components/responses/NotFoundError' /openapi: get: @@ -797,5 +944,3 @@ paths: $ref: '#/components/responses/UnauthorizedError' '403': $ref: '#/components/responses/ForbiddenError' - '404': - $ref: '#/components/responses/NotFoundError' diff --git a/resources/assets/js/countdown.js b/resources/assets/js/countdown.js index 31fb14443..e45a864a2 100644 --- a/resources/assets/js/countdown.js +++ b/resources/assets/js/countdown.js @@ -39,9 +39,14 @@ ready(() => { document.querySelectorAll('[data-countdown-ts]').forEach((element) => { const timestamp = Number(element.dataset.countdownTs); const template = element.textContent; + const templateExpired = String(element.dataset.countdownExpiredTemplate); element.textContent = template.replace('%c', formatFromNow(timestamp)); setInterval(() => { - element.textContent = template.replace('%c', formatFromNow(timestamp)); + if (templateExpired !== 'undefined' && Date.now() / 1000 >= timestamp) { + element.textContent = templateExpired.replace('%c', formatFromNow(timestamp)); + } else { + element.textContent = template.replace('%c', formatFromNow(timestamp)); + } }, 1000); }); }); diff --git a/resources/assets/js/forms.js b/resources/assets/js/forms.js index 0d853d298..54e90d14f 100644 --- a/resources/assets/js/forms.js +++ b/resources/assets/js/forms.js @@ -121,20 +121,6 @@ ready(() => { }); }); -ready(() => { - /** - * Disable every submit button after clicking (to prevent double-clicking) - */ - document.querySelectorAll('form').forEach((formElement) => { - formElement.addEventListener('submit', () => { - document.querySelectorAll('input[type="submit"],button[type="submit"]').forEach((element) => { - element.readOnly = true; - element.classList.add('disabled'); - }); - }); - }); -}); - /** * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled#overview} */ @@ -287,72 +273,75 @@ ready(() => { * The class, title and content of the requesting button gets copied for confirmation * */ -ready(() => { - document.querySelectorAll('[data-confirm_submit_title], [data-confirm_submit_text]').forEach((element) => { - let modalOpen = false; - let oldType = element.type; - if (element.type !== 'submit') { +const confirmationModal = (element) => { + let modalOpen = false; + let oldType = element.type; + if (element.type !== 'submit') { + return; + } + + element.type = 'button'; + element.addEventListener('click', (event) => { + if (modalOpen) { return; } - - element.type = 'button'; - element.addEventListener('click', (event) => { - if (modalOpen) { - return; - } - event.preventDefault(); - - document.getElementById('confirmation-modal')?.remove(); - document.body.insertAdjacentHTML( - 'beforeend', - ` - - - - - ${element.dataset.confirm_submit_title ?? ''} - - - - ${element.dataset.confirm_submit_text ?? ''} - - + event.preventDefault(); + + document.getElementById('confirmation-modal')?.remove(); + document.body.insertAdjacentHTML( + 'beforeend', + ` + + + + + ${element.dataset.confirm_submit_title ?? ''} + + + + ${element.dataset.confirm_submit_text ?? ''} + + - ` - ); - - const modal = document.getElementById('confirmation-modal'); - modal.addEventListener('hide.bs.modal', () => { - modalOpen = false; - }); - - const modalSubmitButton = modal.querySelector('[data-submit]'); - modalSubmitButton.addEventListener('click', () => { - element.type = oldType; - element.click(); - }); + + ` + ); + + const modal = document.getElementById('confirmation-modal'); + let bootstrapModal = new bootstrap.Modal(modal); + modal.addEventListener('hide.bs.modal', () => { + modalOpen = false; + }); - /** - * After the modal has been shown, focus on the "Submit" button in the modal - * so that it can be confirmed with "Enter". - */ - modal.addEventListener('shown.bs.modal', () => { - modalSubmitButton.focus(); - }); + const modalSubmitButton = modal.querySelector('[data-submit]'); + modalSubmitButton.addEventListener('click', () => { + element.type = oldType; + element.click(); + bootstrapModal.hide(); + }); - modalOpen = true; - let bootstrapModal = new bootstrap.Modal(modal); - bootstrapModal.show(); + /** + * After the modal has been shown, focus on the "Submit" button in the modal + * so that it can be confirmed with "Enter". + */ + modal.addEventListener('shown.bs.modal', () => { + modalSubmitButton.focus(); }); + + modalOpen = true; + bootstrapModal.show(); }); +}; + +ready(() => { + document.querySelectorAll('[data-confirm_submit_title], [data-confirm_submit_text]').forEach(confirmationModal); }); /** @@ -423,3 +412,67 @@ ready(() => { }); }); }); + +/** + * focus select at user angel types add action + */ +ready(() => { + document.querySelectorAll('#form_user_angel_type_add_user_id').forEach((element) => { + const innerDiv = element.choices.containerOuter.element; + if (innerDiv) { + innerDiv.focus(); + } + }); +}); + +/** + * Live reload arrive stead instead of submitting form + */ +ready(() => { + document.querySelectorAll('form.arrive_form').forEach((element) => { + element.addEventListener('submit', async (e) => { + e.preventDefault(); + e.stopPropagation(); + + const form = e.target; + const row = form.parentElement.parentElement; + const name = row.querySelector('.column_name a'); + const arrived = row.querySelector('.column_arrived'); + const arrival = row.querySelector('.column_rendered_arrival_date'); + const button = form.querySelector('[name="send"]'); + const data = new FormData(e.target); + data.append('send', '!'); + + const response = await fetch(window.location.href, { + method: 'POST', + headers: { + Accept: 'application/json', + 'X-CSRF-TOKEN': data.get('_token'), + }, + body: data, + }); + + if (!response.ok) { + console.warn('Broken response, submitting form anyways'); + form.submit(); + return; + } + + const body = await response.json(); + arrival.innerText = body.arrival_date; + const newButton = new DOMParser().parseFromString(body.button, 'text/html').body.firstChild; + button.replaceWith(newButton); + + if (body.state === 'arrived') { + name.classList.remove('text-muted'); + arrived.innerHTML = ''; + form.querySelector('[name="action"]').value = 'reset'; + confirmationModal(newButton); + } else { + name.classList.add('text-muted'); + arrived.innerHTML = ''; + form.querySelector('[name="action"]').value = 'arrived'; + } + }); + }); +}); diff --git a/resources/assets/js/table-responsive-sticky-header.js b/resources/assets/js/table-responsive-sticky-header.js new file mode 100644 index 000000000..377553b8c --- /dev/null +++ b/resources/assets/js/table-responsive-sticky-header.js @@ -0,0 +1,24 @@ +import { ready } from './ready'; + +ready(() => { + const navbar = document.querySelector('.navbar'); + + document.querySelectorAll('.table-responsive-sticky-header').forEach((element) => { + const table = element.querySelector('table'); + const header = table.querySelector('thead'); + + table.className += ' table-sticky-header'; + + const update = () => { + const calcHeight = navbar.getBoundingClientRect().height - element.getBoundingClientRect().top; + header.style.top = `${calcHeight}px`; + }; + + update(); + window.addEventListener('resize', update); + window.addEventListener('scroll', update); + + const navbarObserver = new MutationObserver(update); + navbarObserver.observe(navbar, { attributes: true, subtree: true, attributeFilter: ['class'] }); + }); +}); diff --git a/resources/assets/js/utils.js b/resources/assets/js/utils.js new file mode 100644 index 000000000..ac7c17644 --- /dev/null +++ b/resources/assets/js/utils.js @@ -0,0 +1,24 @@ +let csrfToken; + +/** + * Returns the CSRF token. + * + * @throws {Error} - Raises an error if the csrf meta tag cannot be found or is empty + * @returns {string} CSRF token + */ +export const getCSRFToken = () => { + if (!csrfToken) { + const csrfTokenElement = document.querySelector('meta[name="csrf-token"]'); + if (!csrfTokenElement) { + throw new Error('Unable to find csrf-token meta element'); + } + + if (!csrfTokenElement.content) { + throw new Error('Got empty csrf-token meta element'); + } + + csrfToken = csrfTokenElement.content; + } + + return csrfToken; +}; diff --git a/resources/assets/js/vendor.js b/resources/assets/js/vendor.js index ef30ac178..6e940bb8d 100644 --- a/resources/assets/js/vendor.js +++ b/resources/assets/js/vendor.js @@ -4,3 +4,5 @@ import './forms'; import './countdown'; import './dashboard'; import './design'; +import './voucher'; +import './table-responsive-sticky-header'; diff --git a/resources/assets/js/voucher.js b/resources/assets/js/voucher.js new file mode 100644 index 000000000..5f157c15e --- /dev/null +++ b/resources/assets/js/voucher.js @@ -0,0 +1,97 @@ +import { getCSRFToken } from './utils'; +import { ready } from './ready'; + +ready(() => { + // Add plus 1 voucher click handler to all plus 1 voucher buttons + document.querySelectorAll('[data-voucher-amount][data-voucher-user-id]').forEach((element) => { + element.addEventListener('click', handlePlus1VoucherClick); + }); +}); + +/** + * @typedef {Object} EditVoucherResponse + * @property {number} eligible + * @property {number} issued + * @property {number} total - Total number of issued vouchers + */ + +/** + * Send an async request to increase the number of vouchers issued. + * + * @param {number} userId - ID of the user whose voucher amount is to be updated + * @param {number} amount - Voucher amount to set + * + * @returns {Promise} + */ +const sendEditVoucherRequest = async (userId, amount) => { + const csrfToken = getCSRFToken(); + + const data = new FormData(); + data.append('got_voucher', amount); + + const response = await fetch(`/admin/user/${userId}/voucher`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'X-CSRF-TOKEN': csrfToken, + }, + body: data, + }); + + if (!response.ok) { + throw new Error(`Voucher update response not okay`, response); + } + + return await response.json(); +}; + +/** + * @param {MouseEvent} event + */ +const handlePlus1VoucherClick = async (event) => { + event.preventDefault(); + event.stopPropagation(); + + /** + * @type {HTMLButtonElement} + */ + const element = event.target; + const dataset = element.dataset; + + const amount = Number(dataset.voucherAmount); + const userId = Number(dataset.voucherUserId); + + if (Number.isInteger(userId) === false) { + console.error('User ID is not an integer', userId); + return; + } + + if (Number.isInteger(amount) === false) { + console.error('Voucher amount is not an integer', amount); + return; + } + + // Block user from multiple inputs + element.disabled = true; + + try { + const editVoucherResponse = await sendEditVoucherRequest(userId, amount); + + // Update user voucher numbers + element.parentNode.querySelector( + '[data-field="voucher-status"]' + ).textContent = `${editVoucherResponse.issued} / ${editVoucherResponse.eligible}`; + element.dataset.voucherAmount = editVoucherResponse.issued + 1; + + // Update total voucher count + const totalElement = document.getElementById('voucher-count'); + + if (totalElement !== null) { + totalElement.innerText = editVoucherResponse.total; + } + } catch (error) { + console.error('Error during update voucher request', error); + } finally { + element.disabled = false; + } +}; diff --git a/resources/assets/themes/base.scss b/resources/assets/themes/base.scss index 32e76b702..8a8492d7e 100644 --- a/resources/assets/themes/base.scss +++ b/resources/assets/themes/base.scss @@ -148,6 +148,13 @@ table .border-bottom { padding: 0 2px; } +table.table-sticky-header thead { + position: sticky; + top: 57px; + z-index: $zindex-sticky; + background-color: var(--es-table-sticky-bg, var(--bs-body-bg)); +} + .stats { font-size: 20px; height: 150px; @@ -195,7 +202,7 @@ table .border-bottom { // prevent dropdown-menu from overflowing the view .dropdown-menu { - max-height: calc(100vh - 300px); // 300px: menu offset + max-height: 100dvh; overflow: auto; // Show above shifts headers @@ -208,6 +215,17 @@ table .border-bottom { } } +/* make selections scrollable and add a max height of 4.5 entries */ +#collapseShiftsFilterSelect .selection { + overflow-y: auto; + max-height: ($form-check-min-height + $form-check-margin-bottom) * 4.5; +} + +/* add divider between sorting groups of angel types selection */ +#angel_types_selection_hr { + margin: 0; +} + .selection .checkbox { display: block; } @@ -289,6 +307,17 @@ table .border-bottom { text-align: right; } +.column_shift_info:not(:hover) .table-myshifts-shift-info-limit-height { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 6; + overflow: hidden; + + @media (min-width: map-get($grid-breakpoints, 'lg')) { + -webkit-line-clamp: 4; + } +} + h1, h2, h3, @@ -352,6 +381,10 @@ h6, padding-top: 3em; } +.table-sticky-header .ref-id[id] { + padding-top: 7em; +} + // Cards .card > .card-body:first-child { border-top-left-radius: var(--bs-card-border-radius); @@ -422,6 +455,22 @@ code { border-radius: inherit; } +blockquote { + border-left: 0.2em solid $primary; + padding-left: 0.5em; +} + +/* center buttons in titles */ +h1 { + display: flex; + gap: 0.5rem; + align-items: flex-end; + + & .btn { + align-self: center; + } +} + /* Hide the arrow up/down buttons rendered by the browser in the input field */ /* Chrome, Safari, Edge, Opera */ input[type='number']::-webkit-outer-spin-button, diff --git a/resources/assets/themes/dark.scss b/resources/assets/themes/dark.scss index de9ddfdd6..eeaaef8ca 100644 --- a/resources/assets/themes/dark.scss +++ b/resources/assets/themes/dark.scss @@ -8,11 +8,9 @@ $list-group-form-check-input-border-color: #999; $es-choices-highlight-color: #000; -$invert-color-value: 1 !default; - -/* Invert the clock icon color for Chrome */ -input[type='time']::-webkit-calendar-picker-indicator { - filter: invert($invert-color-value); +/* Make user-agent stylesheet follow dark theme */ +body { + color-scheme: dark; } .modal .bg-dark .modal-header .btn-close { diff --git a/resources/assets/themes/theme15.scss b/resources/assets/themes/theme15.scss index 3ff14ea7e..295ad3ca1 100644 --- a/resources/assets/themes/theme15.scss +++ b/resources/assets/themes/theme15.scss @@ -203,3 +203,7 @@ h5 { .card .btn { text-decoration: none; } + +blockquote { + border-left-color: $text-color; +} diff --git a/resources/assets/themes/theme18.scss b/resources/assets/themes/theme18.scss index bb4c1a0a0..de5d2d65c 100644 --- a/resources/assets/themes/theme18.scss +++ b/resources/assets/themes/theme18.scss @@ -5,24 +5,13 @@ @import 'dark'; -:root { - --color-primary: #ff5053; - --color-highlight: #fef2ff; - --color-accent-a: #b2aaff; - --color-accent-b: #6a5fdb; - --color-accent-c: #29114c; - --color-accent-d: #261a66; - --color-accent-e: #190b2f; - --color-background: #0f000a; -} - //== changed Colors $gray-dark: #29114c; $gray-darker: #190b2f; $gray: #6a5fdb; $gray-light: #b2aaff; $gray-lighter: #fef2ff; -$dark: $gray-dark; +$dark: $gray-darker; $primary: #ff5053; $secondary: #b2aaff; @@ -38,7 +27,7 @@ $text-muted: $gray-light; $btn-link-disabled-color: $gray-light; -$dropdown-bg: #212529; +$dropdown-bg: $gray-darker; $dropdown-link-hover-color: #000000; //== changed Forms @@ -89,6 +78,8 @@ $nav-tabs-link-active-border-color: $gray-dark; $nav-tabs-link-active-color: $gray-darker; $nav-pills-link-active-color: $gray-darker; +$popover-bg: $gray-dark; + //== Pagination // //## @@ -148,19 +139,26 @@ h1, // Specials =================================================================== -.bg-success a, -.bg-primary a, -.bg-secondary a, -.bg-warning a, -.bg-info a, -.bg-light a { - color: $gray-darker !important; +.bg-success > a, +a.badge.bg-success, +.bg-primary > a, +a.badge.bg-primary, +.bg-secondary > a, +a.badge.bg-secondary, +.bg-warning > a, +a.badge.bg-warning, +.bg-info > a, +a.badge.bg-info, +.bg-light > a a.badge.bg-light { + color: $gray-darker; + & :hover { + color: $gray-darker; + } } -.bg-body a, .bg-danger a, .bg-dark a { - color: $state-danger-text !important; + color: $state-danger-text; } .bg-primary, @@ -173,9 +171,8 @@ h1, } .bg-body, -.bg-danger, .bg-dark { - color: $state-danger-text; + color: $body-color; } .navbar { @@ -233,3 +230,7 @@ body { :root { scrollbar-color: $secondary $gray-dark; } + +.shift-calendar .lane { + background-color: $body-bg; +} diff --git a/resources/assets/themes/theme19.scss b/resources/assets/themes/theme19.scss new file mode 100644 index 000000000..24346bdc1 --- /dev/null +++ b/resources/assets/themes/theme19.scss @@ -0,0 +1,102 @@ +// eh22-dark + +@import 'dark'; + +$dark: #0c011f; +$gray-darker: #180736; +$gray-dark: #26114b; +$gray: #61468b; +$gray-light: #b2a0cb; +$gray-lighter: #d1c6e0; +$light: #f2f0f5; + +$color-bg: $dark; +$color-shade-bg-1: $gray-darker; +$color-shade-bg-2: $gray-dark; +$color-shade-bg-3: #371f60; +$color-shade-bg-4: #4b3176; +$color-shade-neutral: $gray; +$color-shade-fg-4: #7a60a0; +$color-shade-fg-3: #957eb5; +$color-shade-fg-2: $gray-light; +$color-shade-fg-1: $gray-lighter; +$color-fg: $light; + +$primary: #c6257d; +$secondary: #4badd8; +$success: #54aa18; +$info: #61468b; +$warning: #efb100; +$danger: #bb2626; + +$accent-1: #60a5f9; +$accent-2: #d381f7; +$accent-3: #ff7975; + +:root { + --color-white: #ffffff; + --color-foreground: #f2f0f5; + --color-background: #0c011f; + --color-shade-bg-1: #180736; + --color-shade-bg-2: #26114b; + --color-shade-bg-3: #371f60; + --color-shade-bg-4: #4b3176; + --color-neutral: #61468b; + --color-shade-fg-4: #7a60a0; + --color-shade-fg-3: #957eb5; + --color-shade-fg-2: #b2a0cb; + --color-shade-fg-1: #d1c6e0; + --color-background-image: var(--color-shade-bg-2); + --color-primary: #c6257d; + --color-primary-highlight: #c6257d60; + --color-secondary: #4badd8; + --color-secondary-highlight: #4badd860; + --color-accent-1: #60a5f9; + --color-accent-2: #d381f7; + --color-accent-3: #ff7975; + --color-success: #54aa18; + --color-success-highlight: #54aa1860; + --color-warning: #efb100; + --color-warning-highlight: #efb10060; + --color-error: #bb2626; + --color-error-highlight: #bb262660; + + --filter-glow-primary: drop-shadow(0 0 0.0625em var(--color-white)) drop-shadow(0 0 0.125em var(--color-primary)) + drop-shadow(0 0 0.25em var(--color-primary)); + --filter-glow-secondary: drop-shadow(0 0 0.0625em var(--color-white)) drop-shadow(0 0 0.125em var(--color-secondary)) + drop-shadow(0 0 0.25em var(--color-secondary)); + --filter-glow-error: drop-shadow(0 0 0.0625em var(--color-white)) drop-shadow(0 0 0.125em var(--color-error)) + drop-shadow(0 0 0.25em var(--color-error)); + --filter-glow-success: drop-shadow(0 0 0.0625em var(--color-white)) drop-shadow(0 0 0.125em var(--color-success)) + drop-shadow(0 0 0.25em var(--color-success)); + --filter-glow-warning: drop-shadow(0 0 0.0625em var(--color-white)) drop-shadow(0 0 0.125em var(--color-warning)) + drop-shadow(0 0 0.25em var(--color-warning)); + --color-glow-primary: var(--color-white); + --color-glow-secondary: var(--color-white); + --color-glow-error: var(--color-white); + --color-glow-success: var(--color-white); + --color-glow-warning: var(--color-white); + --transition-glow: filter 150ms cubic-bezier(0, 1.7, 1, -0.3) 50ms, + border-color 150ms cubic-bezier(0, 1.7, 1, -0.3) 50ms; + + @media (prefers-reduced-motion) { + --transition-glow: filter 150ms, border-color 150ms; + } + + --text-glow-primary: drop-shadow(0 0 0.03125em var(--color-white)) drop-shadow(0 0 0.0625em var(--color-primary)) + drop-shadow(0 0 0.125em var(--color-primary)); + --text-glow-secondary: drop-shadow(0 0 0.03125em var(--color-white)) drop-shadow(0 0 0.0625em var(--color-secondary)) + drop-shadow(0 0 0.125em var(--color-secondary)); + --text-glow-error: drop-shadow(0 0 0.03125em var(--color-white)) drop-shadow(0 0 0.0625em var(--color-error)) + drop-shadow(0 0 0.125em var(--color-error)); + --text-glow-success: drop-shadow(0 0 0.03125em var(--color-white)) drop-shadow(0 0 0.0625em var(--color-success)) + drop-shadow(0 0 0.125em var(--color-success)); + --text-glow-warning: drop-shadow(0 0 0.03125em var(--color-white)) drop-shadow(0 0 0.0625em var(--color-warning)) + drop-shadow(0 0 0.125em var(--color-warning)); +} + +@import 'theme19/base'; + +.text-dark { + color: var(--color-shade-bg-3) !important; +} diff --git a/resources/assets/themes/theme19/_base.scss b/resources/assets/themes/theme19/_base.scss new file mode 100644 index 000000000..fbc276b6c --- /dev/null +++ b/resources/assets/themes/theme19/_base.scss @@ -0,0 +1,899 @@ +// eh22-base + +// Variables ================================================================== + +$body-color: $color-fg; +$body-bg: $color-bg; +$border-color: $color-shade-bg-2; + +$table-bg: $color-bg; +$table-bg-accent: $color-shade-bg-1; +$table-bg-hover: $color-shade-bg-2; +$table-bg-active: $color-shade-bg-2; +$table-border-color: $color-shade-neutral; + +$text-color: $color-fg; +$text-muted: $color-shade-fg-2; + +$btn-link-disabled-color: $color-shade-fg-2; + +$dropdown-bg: $color-shade-bg-1; +$dropdown-link-hover-color: $body-bg; + +//== changed Forms + +$input-bg: $color-shade-bg-1; +$input-border-color: $color-shade-bg-2; // TODO +$input-group-addon-bg: $input-bg; +$input-disabled-bg: $body-bg; +$input-disabled-color: $color-shade-neutral; +$input-disabled-border-color: $color-shade-bg-1; + +$form-check-input-border: 1px solid $color-shade-neutral; +$form-switch-color: $color-shade-neutral; + +//== changed Pagination + +$pagination-color: $color-fg; +$pagination-bg: $color-shade-bg-2; +$pagination-border-color: $color-shade-bg-4; +$pagination-disabled-color: $color-shade-fg-4; +$pagination-disabled-bg: $color-bg; +$pagination-disabled-border-color: $color-shade-bg-2; + +//== changed Form states and alerts + +$state-success-text: $body-color; +$state-success-bg: $success; +$state-success-border: darken($state-success-bg, 5%); + +$state-info-text: $body-color; +$state-info-bg: $info; +$state-info-border: darken($state-info-bg, 7%); + +$state-warning-text: $body-color; +$state-warning-bg: $warning; +$state-warning-border: darken($state-warning-bg, 3%); + +$state-danger-text: $body-color; +$state-danger-bg: $danger; +$state-danger-border: darken($state-danger-bg, 3%); + +$headings-small-color: $color-shade-fg-2; + +$alert-bg-scale: 0%; +$alert-border-scale: 0%; +$alert-color-scale: 0%; + +//== Navs + +$nav-tabs-link-active-border-color: $color-shade-bg-2; + +$nav-tabs-link-active-color: $color-shade-bg-1; +$nav-pills-link-active-color: $color-shade-bg-1; + +$popover-bg: $color-shade-bg-2; + +//== Pagination + +$pagination-color: $color-shade-fg-1; +$pagination-border-color: $color-shade-bg-2; + +$pagination-hover-color: $body-bg; +$pagination-hover-border-color: $color-shade-bg-2; + +$pagination-active-color: $body-bg; +$pagination-active-border-color: $color-shade-bg-2; + +$pagination-disabled-color: $color-shade-fg-2; +$pagination-disabled-border-color: $color-shade-bg-2; + +@import '../cyborg_variables'; +@import '../cyborg_styles'; + +//== Typography + +@font-face { + font-family: 'Athiti'; + font-weight: 700; + src: url('athiti/Athiti-Bold.woff2') format('woff2'); +} + +@font-face { + font-family: 'Athiti'; + font-weight: 600; + src: url('athiti/Athiti-SemiBold.woff2') format('woff2'); +} + +@font-face { + font-family: 'Athiti'; + font-weight: 500; + src: url('athiti/Athiti-Medium.woff2') format('woff2'); +} + +@font-face { + font-family: 'Athiti'; + font-weight: 400; + src: url('athiti/Athiti-Regular.woff2') format('woff2'); +} + +@font-face { + font-family: 'Athiti'; + font-weight: 300; + src: url('athiti/Athiti-Light.woff2') format('woff2'); +} + +@font-face { + font-family: 'Athiti'; + font-weight: 200; + src: url('athiti/Athiti-ExtraLight.woff2') format('woff2'); +} + +@font-face { + font-family: 'Departure Mono'; + src: url('departuremono/DepartureMono-Regular.woff2') format('woff2'); +} + +@font-face { + font-family: 'Argon Glow'; + font-weight: 100; + src: url('argonglow/ArgonGlow-Thin.woff2') format('woff2'); +} + +@font-face { + font-family: 'Argon Glow'; + font-weight: 200; + src: url('argonglow/ArgonGlow-ExtraLight.woff2') format('woff2'); +} + +@font-face { + font-family: 'Argon Glow'; + font-weight: 300; + src: url('argonglow/ArgonGlow-Light.woff2') format('woff2'); +} + +@font-face { + font-family: 'Argon Glow'; + font-weight: 400; + src: url('argonglow/ArgonGlow-Regular.woff2') format('woff2'); +} + +@font-face { + font-family: 'Argon Glow'; + font-weight: 500; + src: url('argonglow/ArgonGlow-Medium.woff2') format('woff2'); +} + +@font-face { + font-family: 'Argon Glow'; + font-weight: 600; + src: url('argonglow/ArgonGlow-SemiBold.woff2') format('woff2'); +} + +@font-face { + font-family: 'Argon Glow'; + font-weight: 700; + src: url('argonglow/ArgonGlow-Bold.woff2') format('woff2'); +} + +@font-face { + font-family: 'Argon Glow'; + src: url('argonglow/ArgonGlow-VariableVF.woff2') format('woff2'); + font-weight: 100 900; +} + +body { + font-family: 'Athiti', ui-sans, sans-serif; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: 'Argon Glow', ui-sans, sans-serif; + font-weight: 400; + margin: 1rem 0; + color: var(--color-foreground); +} + +pre, +code, +pre code { + font-family: 'Departure Mono', ui-monospace, monospace; + font-size: 0.8em; +} + +// Component Styling ============================================================ + +a { + color: var(--color-accent-1); + text-decoration: underline; + + &:hover, + &:active, + &:focus { + color: var(--color-accent-3); + text-decoration: none; + } + + &:visited { + color: var(--color-accent-2); + text-decoration: underline; + + &:hover, + &:active, + &:focus { + color: var(--color-accent-3); + text-decoration: none; + } + } +} + +.btn-group > form { + z-index: 1; // fix form-button border being partially hidden behind other buttons +} + +.btn, +a.btn, +button.btn, +.btn-group > .btn, +.btn-group > a.btn, +.btn-group > button.btn { + border: solid 0.1rem var(--color-shade-bg-2); + background-color: var(--color-shade-bg-1); + color: var(--color-foreground); + + &.btn-danger { + border-color: var(--color-error); + background-color: var(--color-error-highlight); + + &:hover, + &:active, + &:focus { + border: solid 0.1rem var(--color-glow-error); + outline: none; + box-shadow: none; + background-color: transparent; + transition: var(--transition-glow); + color: var(--color-glow-error); + filter: var(--filter-glow-error); + } + } + + &.btn-success { + border-color: var(--color-success); + background-color: var(--color-success-highlight); + + &:hover, + &:active, + &:focus { + border: solid 0.1rem var(--color-glow-success); + outline: none; + box-shadow: none; + background-color: transparent; + transition: var(--transition-glow); + color: var(--color-glow-success); + filter: var(--filter-glow-success); + } + } + + &.btn-warning { + border-color: var(--color-warning); + background-color: var(--color-warning-highlight); + + &:hover, + &:active, + &:focus { + border: solid 0.1rem var(--color-glow-warning); + outline: none; + box-shadow: none; + background-color: transparent; + transition: var(--transition-glow); + color: var(--color-glow-warning); + filter: var(--filter-glow-warning); + } + } + + &.btn-dark { + border-color: var(--color-shade-bg-1); + background-color: var(--color-background); + color: var(--color-foreground); + + &:hover, + &:active, + &:focus { + border: solid 0.1rem var(--color-shade-bg-1); + outline: none; + box-shadow: none; + background-color: var(--color-shade-bg-4); + color: var(--color-foreground); + } + } + + &.btn-light { + border-color: var(--color-shade-fg-1); + background-color: var(--color-foreground); + color: var(--color-background); + + &:hover, + &:active, + &:focus { + border: solid 0.1rem var(--color-shade-fg-1); + outline: none; + box-shadow: none; + background-color: var(--color-shade-fg-4); + color: var(--color-background); + } + } + + &.btn-primary { + border-color: var(--color-shade-fg-4); + color: var(--color-foreground); + background-color: var(--color-neutral); + + &:hover, + &:active, + &:focus { + border: solid 0.1rem var(--color-glow-primary); + outline: none; + box-shadow: none; + background-color: transparent; + transition: var(--transition-glow); + color: var(--color-glow-primary); + filter: var(--filter-glow-primary); + } + } + + &.btn-secondary { + border-color: var(--color-shade-bg-2); + color: var(--color-foreground); + background-color: var(--color-shade-bg-1); + } + + &.btn-info { + border-color: var(--color-shade-bg-2); + color: var(--color-foreground); + background-color: var(--color-shade-bg-1); + } + + &.btn-secondary, + &.btn-info { + &:hover, + &:active, + &:focus { + border: solid 0.1rem var(--color-glow-secondary); + outline: none; + box-shadow: none; + background-color: transparent; + transition: var(--transition-glow); + color: var(--color-glow-secondary); + filter: var(--filter-glow-secondary); + } + } +} + +.btn-close, +.modal-header .btn-close, +.modal .bg-dark .modal-header .btn-close { + border: solid 0.1rem var(--color-shade-fg-4); + background-color: var(--color-shade-bg-1); + + &:hover, + &:active, + &:focus { + border: solid 0.1rem var(--color-glow-primary); + outline: none; + box-shadow: none; + background-color: transparent; + transition: var(--transition-glow); + color: var(--color-glow-primary); + filter: var(--filter-glow-primary); + } +} + +.bg-success > a, +a.badge.bg-success, +.bg-primary > a, +a.badge.bg-primary, +.bg-secondary > a, +a.badge.bg-secondary, +.bg-warning > a, +a.badge.bg-warning, +.bg-info > a, +a.badge.bg-info, +.bg-light > a a.badge.bg-light { + color: var(--color-shade-bg-1); + & :hover { + color: var(--color-shade-bg-1); + } +} + +.bg-primary { + --highlight-color: var(--color-primary-highlight); + --header-fg-color: var(--color-foreground); + background-color: var(--color-primary-highlight) !important; + color: var(--color-foreground) !important; +} +.bg-secondary { + --highlight-color: var(--color-secondary-highlight); + --header-fg-color: var(--color-foreground); + background-color: var(--color-secondary-highlight) !important; + color: var(--color-foreground) !important; +} +.bg-success { + --highlight-color: var(--color-success-highlight); + --header-fg-color: var(--color-foreground); + background-color: var(--color-success-highlight) !important; + color: var(--color-foreground) !important; +} +.bg-danger { + --highlight-color: var(--color-error-highlight); + --header-fg-color: var(--color-foreground); + background-color: var(--color-error-highlight) !important; + color: var(--color-foreground) !important; +} +.bg-warning { + --highlight-color: var(--color-warning-highlight); + --header-fg-color: var(--color-foreground); + background-color: var(--color-warning-highlight) !important; + color: var(--color-foreground) !important; +} +.bg-light { + --highlight-color: var(--color-shade-fg-1); + --header-fg-color: var(--color-background); + background-color: var(--color-foreground) !important; + color: var(--color-background) !important; +} +.bg-dark { + --highlight-color: var(--color-shade-bg-1); + --header-fg-color: var(--color-foreground); + background-color: var(--color-shade-bg-1) !important; + color: var(--color-foreground) !important; +} +.bg-info { + --highlight-color: var(--color-shade-bg-2); + --header-fg-color: var(--color-foreground); + background-color: var(--color-shade-bg-2) !important; + color: var(--color-foreground) !important; +} +.bg-body { + --highlight-color: var(--color-background); + --header-fg-color: var(--color-foreground); + background-color: var(--color-background) !important; + color: var(--color-foreground) !important; +} + +div.card { + > .card-header { + background-color: var(--highlight-color) !important; + } + + > .card-header a { + text-decoration: none; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + + > .card-header *, + > .card-header a.text-inherit { + color: var(--header-fg-color) !important; + } + + > .card-header .btn { + &:hover, + &:focus, + &:active { + border-color: var(--highlight-color) !important; + + &.btn-primary { + --header-fg-color: var(--color-glow-primary); + } + &.btn-secondary { + --header-fg-color: var(--color-glow-secondary); + } + &.btn-danger { + --header-fg-color: var(--color-glow-error); + } + &.btn-success { + --header-fg-color: var(--color-glow-success); + } + &.btn-warning { + --header-fg-color: var(--color-glow-warning); + } + } + + &:not(:hover):not(:focus):not(:active) { + --header-fg-color: var(--color-foreground); + border-color: var(--color-shade-bg-4) !important; + color: var(--color-foreground) !important; + background-color: var(--color-shade-bg-2) !important; + } + } + + background-color: var(--color-background) !important; + color: var(--color-foreground) !important; + border: solid 0.1rem var(--highlight-color) !important; +} + +.bg-info { + color: var(--color-foreground); + + > .card-header *, + > .card-header a.text-inherit { + color: var(--color-foreground); + } +} + +.bg-body, +.bg-dark { + color: $body-color; +} + +.navbar { + background: $body-bg; +} + +.navbar .icon-icon_angel { + background-color: var(--color-glow-primary); + text-decoration: none; +} + +nav.navbar { + a.navbar-brand { + color: var(--color-glow-primary); + text-decoration: none; + font-family: 'Argon Glow', ui-sans, sans-serif; + + filter: var(--text-glow-primary); + + &:visited, + &:focus, + &:active, + &:hover { + color: var(--color-glow-primary); + text-decoration: none; + } + } +} + +ul.navbar-nav > li.nav-item > ul.dropdown-menu.show { + background-color: var(--color-background); + display: flex; + flex-direction: column; + padding: 0.5rem; + border-radius: 0.5rem; +} + +ul.nav, +ul.navbar-nav, +ul.navbar-nav > li.nav-item > ul.dropdown-menu { + gap: 0.5rem; + + > li.nav-item, + > li { + border-radius: 0.5rem; + background-color: var(--color-shade-bg-1); + transition: background-color 150ms; + + &.nav-item--userhints { + margin: 0; + } + + &:has(> a.nav-link.active), + &:has(> a.dropdown-item.active) { + background-color: var(--color-shade-bg-2); + + > a.nav-link, + > a.dropdown-item { + background-color: transparent; + border-color: var(--color-shade-bg-4); + } + } + + > a.nav-link, + > a.dropdown-item { + border-radius: 0.5rem; + border: solid 0.1em var(--color-shade-bg-1); + padding: 0.4rem 0.6rem; + color: var(--color-foreground); + text-decoration: none; + font-size: 1.2em; + box-sizing: border-box; + + height: 100%; // fix alignment in full-width mode when + align-content: center; // nav-item content spans across two lines + + transition-property: filter, border-color; + transition-duration: 400ms; + transition-timing-function: ease-in; + + &:visited, + &:focus, + &:active, + &:hover { + background-color: transparent; + outline: none; + box-shadow: none; + color: var(--color-foreground); + text-decoration: none; + } + + span.icon-icon_angel { + background-color: var(--color-foreground); + } + } + + &.bg-danger > a { + height: 100%; // fix height in full-width variant on medium screens + width: 100%; // fix width in drop-down variant on small screens + } + + &:hover, + &:active, + &:focus, + &:focus-within, + &:has(> a.nav-link.active):hover, + &:has(> a.nav-link.active):active, + &:has(> a.nav-link.active):focus, + &:has(> a.nav-link.active):focus-within, + &:has(> a.dropdown-item.active):hover, + &:has(> a.dropdown-item.active):active, + &:has(> a.dropdown-item.active):focus, + &:has(> a.dropdown-item.active):focus-within { + &.bg-danger { + > a.nav-link, + > a.dropdown-item { + border-color: var(--color-glow-primary); + filter: var(--filter-glow-primary); + + &:visited, + &:focus, + &:active, + &:hover { + color: var(--color-glow-primary); + } + } + } + + &:not(.bg-danger) { + > a.nav-link, + > a.dropdown-item { + filter: var(--filter-glow-secondary); + } + } + + > a.nav-link, + > a.dropdown-item { + border: solid 0.1em var(--color-glow-secondary); + outline: none; + box-shadow: none; + background-color: transparent; + transition: var(--transition-glow); + + > span.text-success { + color: var(--color-glow-primary) !important; // fix: don't color "free" green on hover + } + + span.icon-icon_angel { + background-color: var(--color-glow-secondary); + } + + &:visited, + &:focus, + &:active, + &:hover { + color: var(--color-glow-secondary); + outline: none; + box-shadow: none; + text-decoration: none; + } + } + } + } +} + +.table a:not(.btn), +table a:not(.btn) { + color: inherit; +} + +.navbar-toggler { + border: solid 0.1em var(--color-shade-bg-2); + background-color: var(--color-shade-bg-1); + color: var(--color-foreground); + + &:focus, + &:active, + &:hover { + box-shadow: none; + border-color: var(--color-glow-primary); + background-color: transparent; + color: var(--color-glow-primary); + transition: var(--transition-glow); + filter: var(--filter-glow-primary); + } +} + +@media (min-width: 1400px) { + .navbar-collapse { + gap: 0.5rem !important; + } +} + +@media (max-width: 1400px) { + .navbar-collapse > ul.navbar-nav.mb-2.mb-lg-0 { + margin-bottom: 0.5rem !important; + } +} + +.nav-tabs, +.nav-pills, +.pager { + a { + color: var(--color-foreground); + text-decoration: none; + } +} + +$choices-primary-color: var(--color-shade-bg-2); + +div.choices__list--dropdown > div.choices__list > div.choices__item--selectable { + background-color: transparent; + color: var(--color-foreground); + + &.is-selected:not(.is-highlighted) { + background-color: transparent; // again for longest prefix + color: var(--color-glow-primary); + filter: var(--text-glow-primary); + } + + &.is-highlighted { + &:before { + background-color: var(--color-shade-bg-1); + } + + background-color: transparent; // again for longest prefix + color: var(--color-glow-secondary); + filter: var(--text-glow-secondary); + transition: var(--transition-glow); + } +} + +.popover { + --bs-popover-body-padding-y: 0.5rem; + --bs-popover-body-padding-x: 0.5rem; +} + +.alert { + --bs-alert-color: var(--color-foreground); + + &.alert-info, + &.alert-warning, + &.alert-danger, + &.alert-success { + border: none; + border-left: solid 0.5rem var(--bs-alert-bg); + background-color: var(--color-shade-bg-1); + } + + &.alert-info a.text-danger { + --bs-danger-rgb: var(--color-foreground); + } +} + +.list-group-item { + background-color: var(--color-shade-bg-1); + border-color: var(--color-shade-bg-4); +} + +.shift-calendar .lane { + background-color: $body-bg; +} + +.choices:focus, +.choices.is-focused { + .choices__list.choices__list--dropdown { + border-color: var(--color-glow-primary); + } + + .choices__inner { + border-color: var(--color-glow-primary); + } + + border-color: var(--color-glow-primary); + outline: none; + box-shadow: none; + z-index: 1022; + filter: var(--filter-glow-primary); + transition: var(--transition-glow); +} + +.form-control { + border-color: var(--color-shade-fg-4); + + &.choices__inner { + border-color: var(--color-shade-fg-4); + } + + &:focus { + border-color: var(--color-glow-primary); + outline: none; + box-shadow: none; + filter: var(--filter-glow-primary); + transition: var(--transition-glow); + } +} + +.page-link { + border: solid 0.1rem; +} +.page-link:hover, +.page-link:focus, +.page-link:active { + color: var(--color-glow-primary); + border-color: var(--color-glow-primary); + background-color: transparent; + box-shadow: none; + transition: var(--transition-glow); + filter: var(--filter-glow-primary); +} + +.form-check-input { + border: solid 0.1rem var(--color-foreground); + + &:focus { + border-color: var(--color-glow-primary); + outline: none; + box-shadow: none; + filter: var(--filter-glow-primary); + transition: var(--transition-glow); + } + + &:checked { + border-color: var(--color-glow-primary); + filter: var(--filter-glow-primary); + transition: var(--transition-glow); + } +} + +body::before { + content: ''; + position: fixed; + bottom: 0; + right: 0; + width: 100%; + height: 100%; + z-index: -1; + mask-size: cover; + mask-position: bottom right; + mask-repeat: no-repeat; + mask-origin: padding-box; + mask-image: url('background.svg'); + background-color: var(--color-background-image); +} + +// easteregg: flying fairydust on own profile / "my shifts" +// fall back to regular background if reduced motion is preferred +@media (prefers-reduced-motion: no-preference) { + body:has(div.row .bi-calendar-week):has(div.user-info)::before { + content: ''; + position: fixed; + bottom: 0; + right: 0; + width: 100%; + height: 100%; + z-index: -1; + mask-size: contain; + mask-position: center; + mask-repeat: no-repeat; + mask-origin: padding-box; + mask-image: url('fairydust.svg'); + background-color: var(--color-background-image); + } +} diff --git a/resources/assets/themes/theme19/argonglow/ArgonGlow-Bold.woff2 b/resources/assets/themes/theme19/argonglow/ArgonGlow-Bold.woff2 new file mode 100644 index 000000000..caf6ff687 Binary files /dev/null and b/resources/assets/themes/theme19/argonglow/ArgonGlow-Bold.woff2 differ diff --git a/resources/assets/themes/theme19/argonglow/ArgonGlow-ExtraLight.woff2 b/resources/assets/themes/theme19/argonglow/ArgonGlow-ExtraLight.woff2 new file mode 100644 index 000000000..7dc64486e Binary files /dev/null and b/resources/assets/themes/theme19/argonglow/ArgonGlow-ExtraLight.woff2 differ diff --git a/resources/assets/themes/theme19/argonglow/ArgonGlow-Light.woff2 b/resources/assets/themes/theme19/argonglow/ArgonGlow-Light.woff2 new file mode 100644 index 000000000..08a66376a Binary files /dev/null and b/resources/assets/themes/theme19/argonglow/ArgonGlow-Light.woff2 differ diff --git a/resources/assets/themes/theme19/argonglow/ArgonGlow-Medium.woff2 b/resources/assets/themes/theme19/argonglow/ArgonGlow-Medium.woff2 new file mode 100644 index 000000000..83bb0d958 Binary files /dev/null and b/resources/assets/themes/theme19/argonglow/ArgonGlow-Medium.woff2 differ diff --git a/resources/assets/themes/theme19/argonglow/ArgonGlow-Regular.woff2 b/resources/assets/themes/theme19/argonglow/ArgonGlow-Regular.woff2 new file mode 100644 index 000000000..601d4655b Binary files /dev/null and b/resources/assets/themes/theme19/argonglow/ArgonGlow-Regular.woff2 differ diff --git a/resources/assets/themes/theme19/argonglow/ArgonGlow-SemiBold.woff2 b/resources/assets/themes/theme19/argonglow/ArgonGlow-SemiBold.woff2 new file mode 100644 index 000000000..a88f93986 Binary files /dev/null and b/resources/assets/themes/theme19/argonglow/ArgonGlow-SemiBold.woff2 differ diff --git a/resources/assets/themes/theme19/argonglow/ArgonGlow-Thin.woff2 b/resources/assets/themes/theme19/argonglow/ArgonGlow-Thin.woff2 new file mode 100644 index 000000000..dc950fee2 Binary files /dev/null and b/resources/assets/themes/theme19/argonglow/ArgonGlow-Thin.woff2 differ diff --git a/resources/assets/themes/theme19/argonglow/ArgonGlow-VariableVF.woff2 b/resources/assets/themes/theme19/argonglow/ArgonGlow-VariableVF.woff2 new file mode 100644 index 000000000..7d3aaf402 Binary files /dev/null and b/resources/assets/themes/theme19/argonglow/ArgonGlow-VariableVF.woff2 differ diff --git a/resources/assets/themes/theme19/argonglow/OFL b/resources/assets/themes/theme19/argonglow/OFL new file mode 100644 index 000000000..1cdb289f6 --- /dev/null +++ b/resources/assets/themes/theme19/argonglow/OFL @@ -0,0 +1,96 @@ +Copyright (c) 2025, The Argon Glow Project Authors (https://codeberg.org/kritzl/argon-glow), +Copyright (c) 2025, kritzl (kritzl@kritzl.dev), +Copyright (c) 2025, traumweh (traumweh@lyx.sh), +with Reserved Font Name "Argon Glow". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting - in part or in whole - any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/resources/assets/themes/theme19/athiti/Athiti-Bold.woff2 b/resources/assets/themes/theme19/athiti/Athiti-Bold.woff2 new file mode 100644 index 000000000..4cc5810d7 Binary files /dev/null and b/resources/assets/themes/theme19/athiti/Athiti-Bold.woff2 differ diff --git a/resources/assets/themes/theme19/athiti/Athiti-ExtraLight.woff2 b/resources/assets/themes/theme19/athiti/Athiti-ExtraLight.woff2 new file mode 100644 index 000000000..9bb6cea19 Binary files /dev/null and b/resources/assets/themes/theme19/athiti/Athiti-ExtraLight.woff2 differ diff --git a/resources/assets/themes/theme19/athiti/Athiti-Light.woff2 b/resources/assets/themes/theme19/athiti/Athiti-Light.woff2 new file mode 100644 index 000000000..51f9e727a Binary files /dev/null and b/resources/assets/themes/theme19/athiti/Athiti-Light.woff2 differ diff --git a/resources/assets/themes/theme19/athiti/Athiti-Medium.woff2 b/resources/assets/themes/theme19/athiti/Athiti-Medium.woff2 new file mode 100644 index 000000000..bc8b50ac1 Binary files /dev/null and b/resources/assets/themes/theme19/athiti/Athiti-Medium.woff2 differ diff --git a/resources/assets/themes/theme19/athiti/Athiti-Regular.woff2 b/resources/assets/themes/theme19/athiti/Athiti-Regular.woff2 new file mode 100644 index 000000000..c69c1287e Binary files /dev/null and b/resources/assets/themes/theme19/athiti/Athiti-Regular.woff2 differ diff --git a/resources/assets/themes/theme19/athiti/Athiti-SemiBold.woff2 b/resources/assets/themes/theme19/athiti/Athiti-SemiBold.woff2 new file mode 100644 index 000000000..726a075aa Binary files /dev/null and b/resources/assets/themes/theme19/athiti/Athiti-SemiBold.woff2 differ diff --git a/resources/assets/themes/theme19/athiti/OFL b/resources/assets/themes/theme19/athiti/OFL new file mode 100644 index 000000000..911f6efb0 --- /dev/null +++ b/resources/assets/themes/theme19/athiti/OFL @@ -0,0 +1,93 @@ +Copyright (c) 2015, Cadson Demak (info@cadsondemak.com) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/resources/assets/themes/theme19/background.svg b/resources/assets/themes/theme19/background.svg new file mode 100644 index 000000000..387fe69cd --- /dev/null +++ b/resources/assets/themes/theme19/background.svg @@ -0,0 +1,83 @@ + + + + diff --git a/resources/assets/themes/theme19/departuremono/DepartureMono-Regular.woff2 b/resources/assets/themes/theme19/departuremono/DepartureMono-Regular.woff2 new file mode 100644 index 000000000..7d8b33b0a Binary files /dev/null and b/resources/assets/themes/theme19/departuremono/DepartureMono-Regular.woff2 differ diff --git a/resources/assets/themes/theme19/departuremono/LICENSE b/resources/assets/themes/theme19/departuremono/LICENSE new file mode 100644 index 000000000..de5247618 --- /dev/null +++ b/resources/assets/themes/theme19/departuremono/LICENSE @@ -0,0 +1,93 @@ +Copyright 2022–2024 Helena Zhang (helenazhang.com). + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/resources/assets/themes/theme19/fairydust.svg b/resources/assets/themes/theme19/fairydust.svg new file mode 100644 index 000000000..fedaefd60 --- /dev/null +++ b/resources/assets/themes/theme19/fairydust.svg @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/assets/themes/theme20.scss b/resources/assets/themes/theme20.scss new file mode 100644 index 000000000..2fc683de7 --- /dev/null +++ b/resources/assets/themes/theme20.scss @@ -0,0 +1,158 @@ +// eh22-light + +@import 'light'; + +$dark: #0c011f; +$gray-darker: #180736; +$gray-dark: #26114b; +$gray: #61468b; +$gray-light: #b2a0cb; +$gray-lighter: #d1c6e0; +$light: #f2f0f5; + +$color-bg: $light; +$color-shade-bg-1: $gray-lighter; +$color-shade-bg-2: $gray-light; +$color-shade-bg-3: #957eb5; +$color-shade-bg-4: #7a60a0; +$color-shade-neutral: $gray; +$color-shade-fg-4: #4b3176; +$color-shade-fg-3: #371f60; +$color-shade-fg-2: $gray-dark; +$color-shade-fg-1: $gray-darker; +$color-fg: $dark; + +$primary: #9a0a61; +$secondary: #167fac; +$success: #47990f; +$info: #61468b; +$warning: #d08700; +$danger: #b21010; + +$accent-1: #303ec0; +$accent-2: #6c366c; +$accent-3: #932f0a; + +:root { + --color-white: #ffffff; + --color-foreground: #0c011f; + --color-background: #f2f0f5; + --color-shade-bg-1: #d1c6e0; + --color-shade-bg-2: #b2a0cb; + --color-shade-bg-3: #957eb5; + --color-shade-bg-4: #7a60a0; + --color-neutral: #61468b; + --color-shade-fg-4: #4b3176; + --color-shade-fg-3: #371f60; + --color-shade-fg-2: #26114b; + --color-shade-fg-1: #180736; + --color-background-image: var(--color-shade-bg-1); + --color-primary: #9a0a61; + --color-primary-highlight: #9a0a6160; + --color-secondary: #167fac; + --color-secondary-highlight: #167fac60; + --color-accent-1: #303ec0; + --color-accent-2: #6c366c; + --color-accent-3: #932f0a; + --color-success: #47990f; + --color-success-highlight: #47990f60; + --color-warning: #d08700; + --color-warning-highlight: #d0870060; + --color-error: #b21010; + --color-error-highlight: #b2101060; + + --filter-glow-primary: drop-shadow(0 0 0.0625em #3f012d); + --filter-glow-secondary: drop-shadow(0 0 0.0625em #012c3f); + --filter-glow-error: drop-shadow(0 0 0.0625em #3f0101); + --filter-glow-success: drop-shadow(0 0 0.0625em #122b01); + --filter-glow-warning: drop-shadow(0 0 0.0625em #432004); + --color-glow-primary: #6d0449; + --color-glow-secondary: #044c6b; + --color-glow-error: #780404; + --color-glow-success: #2b6206; + --color-glow-warning: #894b00; + --transition-glow: filter 150ms cubic-bezier(0, 2, 1, -0.7) 50ms, border-color 150ms cubic-bezier(0, 2, 1, -0.7) 50ms; + + @media (prefers-reduced-motion) { + --transition-glow: filter 150ms, border-color 150ms; + } + + --text-glow-primary: drop-shadow(0 0 0.03125em #3f012d); + --text-glow-secondary: drop-shadow(0 0 0.03125em #012c3f); + --text-glow-error: drop-shadow(0 0 0.03125em #3f0101); + --text-glow-success: drop-shadow(0 0 0.03125em #122b01); + --text-glow-warning: drop-shadow(0 0 0.03125em #432004); +} + +@import 'theme19/base'; + +.btn, +a.btn, +button.btn, +.btn-group > .btn, +.btn-group > a.btn, +.btn-group > button.btn { + &.btn-primary { + color: var(--color-background); // use background color for better readability + } +} + +.bg-primary { + --header-fg-color: var(--color-background); + color: var(--color-background) !important; +} +.bg-light { + --header-fg-color: var(--color-foreground); + --highlight-color: var(--color-shade-bg-1); + background-color: var(--color-background) !important; + color: var(--color-foreground) !important; +} +.bg-dark { + --header-fg-color: var(--color-background); + --highlight-color: var(--color-shade-fg-1); + background-color: var(--color-foreground) !important; + color: var(--color-background) !important; +} + +.btn, +a.btn, +button.btn, +.btn-group > .btn, +.btn-group > a.btn, +.btn-group > button.btn { + &.btn-dark { + border-color: var(--color-shade-fg-1); + background-color: var(--color-foreground); + color: var(--color-background); + + &:hover, + &:active, + &:focus { + border: solid 0.1rem var(--color-shade-fg-1); + outline: none; + box-shadow: none; + background-color: var(--color-shade-fg-4); + color: var(--color-background); + } + } + + &.btn-light { + border-color: var(--color-shade-bg-1); + background-color: var(--color-background); + color: var(--color-foreground); + + &:hover, + &:active, + &:focus { + border: solid 0.1rem var(--color-shade-bg-1); + outline: none; + box-shadow: none; + background-color: var(--color-shade-bg-4); + color: var(--color-foreground); + } + } +} + +.text-light { + color: var(--color-shade-bg-3) !important; +} diff --git a/resources/lang/de_DE/additional.po b/resources/lang/de_DE/additional.po index 014999e4f..2ee0e1238 100644 --- a/resources/lang/de_DE/additional.po +++ b/resources/lang/de_DE/additional.po @@ -192,6 +192,9 @@ msgstr "FAQ Eintrag erfolgreich gelöscht." msgid "faq.edit.success" msgstr "FAQ Eintrag erfolgreich aktualisiert." +msgid "question.menu" +msgstr "Frag den Himmel" + msgid "question.delete.success" msgstr "Frage erfolgreich gelöscht." @@ -201,6 +204,15 @@ msgstr "Frage erstellt." msgid "question.edit.success" msgstr "Frage erfolgreich bearbeitet." +msgid "tag.edit.duplicate" +msgstr "Ein Tag mit dem Namen existiert bereits!" + +msgid "tag.edit.success" +msgstr "Der Tag wurde erfolgreich aktualisiert." + +msgid "tag.delete.success" +msgstr "Tag erfolgreich gelöscht." + msgid "notification.news.new" msgstr "Neue News: %s" @@ -237,6 +249,9 @@ msgstr "Du wurdest von einem Supporter als %1$s hinzugefügt." msgid "notification.angeltype.added.text" msgstr "Eine Beschreibung findest du unter %2$s" +msgid "angeltype.add.success" +msgstr "Erfolgreich dem Engeltyp beigetreten" + msgid "notification.shift.deleted" msgstr "Deine Schicht wurde gelöscht" @@ -269,6 +284,9 @@ msgstr "Arbeitseinsatz erfolgreich bearbeitet." msgid "worklog.delete.success" msgstr "Arbeitseinsatz erfolgreich gelöscht." +msgid "voucher.save.success" +msgstr "Anzahl der Gutscheine gespeichert." + msgid "location.edit.success" msgstr "Ort erfolgreich bearbeitet." diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po index 8027f6ca1..55a608199 100644 --- a/resources/lang/de_DE/default.po +++ b/resources/lang/de_DE/default.po @@ -54,7 +54,7 @@ msgid "page.419.title" msgstr "Autorisierung ist abgelaufen" msgid "page.419.text" -msgstr "Das angegebene CSRF Token ist ungültig oder abgelaufen" +msgstr "Das angegebene CSRF Token ist ungültig oder abgelaufen. Bitte versuche es erneut." msgid "general.date" msgstr "d.m.Y" @@ -69,7 +69,7 @@ msgid "footer.eventinfo.start_end" msgstr "Event vom %1$s bis %2$s" msgid "footer.issues" -msgstr "Bugs / Features" +msgstr "Bugs/Features" msgid "footer.github" msgstr "Entwicklerplattform" @@ -192,6 +192,9 @@ msgstr "Team %s" msgid "%s (not \"%s\")" msgstr "%s (kein \"%s\")" +msgid "%s (already in shift)" +msgstr "%s (bereits eingetragen)" + msgid "form.view" msgstr "Ansehen" @@ -249,7 +252,7 @@ msgstr "Schichteintrag gelöscht." msgid "This shift was imported from a schedule so some changes will be overwritten with the next import." msgstr "" -"Diese Schicht wurde aus einem Fahrplan importiert. " +"Diese Schicht wurde aus einem Programm importiert. " "Dadurch werden einige Änderungen beim nächsten Import überschrieben." msgid "Please select a location." @@ -282,12 +285,6 @@ msgstr "Titel" msgid "Location:" msgstr "Ort:" -msgid "Start:" -msgstr "Start:" - -msgid "End:" -msgstr "Ende:" - msgid "Needed angels" msgstr "Benötigte Engel" @@ -389,18 +386,9 @@ msgstr "Engel gelöscht." msgid "Delete %s" msgstr "%s löschen" -msgid "Please enter a valid number of vouchers." -msgstr "Bitte gib eine korrekte Anzahl von Gutscheinen ein." - -msgid "Saved the number of vouchers." -msgstr "Anzahl der Gutscheine gespeichert." - msgid "User not found." msgstr "Benutzer nicht gefunden." -msgid "Enough" -msgstr "Genug" - msgid "All users" msgstr "Alle Benutzer" @@ -464,12 +452,6 @@ msgstr "Engel nicht gefunden." msgid "Angel has been marked as not active." msgstr "Engel wurde als nicht aktiv markiert." -msgid "Angel has got a T-shirt." -msgstr "Engel hat ein T-Shirt bekommen." - -msgid "Angel has got no T-shirt." -msgstr "Engel hat kein T-Shirt bekommen." - msgid "Angel has no valid T-shirt size. T-shirt was not set." msgstr "Engel hat keine valide T-Shirt Größe. T-Shirt wurde nicht gespeichert." @@ -482,15 +464,9 @@ msgstr "Setze aktiv" msgid "Remove active" msgstr "Entferne aktiv" -msgid "Remove T-shirt" -msgstr "Entferne T-Shirt" - msgid "Goodie actions" msgstr "Goodie Aktionen" -msgid "T-shirt actions" -msgstr "T-Shirt Aktionen" - msgid "Sum" msgstr "Summe" @@ -512,6 +488,9 @@ msgstr "Nr." msgid "general.shifts" msgstr "Schichten" +msgid "general.info" +msgstr "Info" + msgid "Length" msgstr "Länge" @@ -522,17 +501,17 @@ msgid "Active" msgstr "Aktiv" msgid "Forced" -msgstr "Erzwungen" - -msgid "T-shirt" -msgstr "T-Shirt" +msgstr "Dauerhaft aktiv" -msgid "T-shirt statistic" -msgstr "T-Shirt Statistik" +msgid "Food" +msgstr "Essen" msgid "Given T-shirts" msgstr "Ausgegebene T-Shirts" +msgid "Configured T-shirts" +msgstr "Konfigurierte T-Shirts" + msgid "Arrive angels" msgstr "Ankommende Engel" @@ -581,6 +560,9 @@ msgstr "Engeltyp" msgid "shift.next" msgstr "Nächste Schicht" +msgid "shift.previous" +msgstr "Vorherige Schicht" + msgid "general.shift" msgstr "Schicht" @@ -723,7 +705,10 @@ msgid "No" msgstr "Nein" msgid "Force active" -msgstr "Aktiv erzwingen" +msgstr "Dauerhaft aktiv" + +msgid "Force food" +msgstr "Unbegrenzt Essen" msgid "Edit user" msgstr "User bearbeiten" @@ -740,9 +725,6 @@ msgstr "Gib bitte einen Schwänz-Kommentar ein!" msgid "Shift saved." msgstr "Schicht gespeichert." -msgid "Ask the Heaven" -msgstr "Frag den Himmel" - msgid "The administration has not configured any locations yet." msgstr "Die Administratoren habe noch keine Orte eingerichtet." @@ -810,6 +792,9 @@ msgstr "" "\" target=\"_blank\">JSON Format (Link bitte geheimhalten, sonst musst du den API-Key in " "deinen Einstellungen zurücksetzen)." +msgid "Per page" +msgstr "Pro Seite" + msgid "All" msgstr "Alle" @@ -908,9 +893,6 @@ msgstr "Fahrer" msgid "Has car" msgstr "Hat Auto" -msgid "Info" -msgstr "Info" - msgid "Supporters" msgstr "Supporter" @@ -956,8 +938,8 @@ msgstr "Meine Schicht" msgid "Help needed" msgstr "Hilfe benötigt" -msgid "Other angel type needed / collides with my shifts" -msgstr "Andere Engeltypen benötigt / kollidiert mit meinen Schichten" +msgid "Other angel type needed/collides with my shifts" +msgstr "Andere Engeltypen benötigt/kollidiert mit meinen Schichten" msgid "Shift is full" msgstr "Schicht ist voll" @@ -980,7 +962,7 @@ msgid "ended" msgstr "vorbei" msgid "please arrive for signup" -msgstr "Ankommen zum Eintragen" +msgstr "Komme an um dich einzutragen" msgid "not yet possible" msgstr "noch nicht möglich" @@ -1094,17 +1076,26 @@ msgstr "" msgid "Your password" msgstr "Dein Passwort" -msgid "Angel can receive another %d vouchers." +msgid "voucher.vouchers" +msgstr "Gutscheine" + +msgid "voucher.edit" +msgstr "Gutscheine bearbeiten" + +msgid "voucher.eligible" msgstr "Engel kann noch %d Gutscheine bekommen." -msgid "Angel can receive another %d vouchers and is FA." -msgstr "Engel kann noch %d Gutscheine bekommen und ist FA." +msgid "voucher.eligible.force" +msgstr "Engel kann %d unbegrenzt Gutscheine bekommen." -msgid "Number of vouchers given out" +msgid "voucher.count" msgstr "Anzahl Gutscheine bekommen" -msgid "Voucher" -msgstr "Gutschein" +msgid "user.state.vouchers" +msgstr "%s von %s Gutscheinen bekommen" + +msgid "user.state.vouchers.force" +msgstr "%d unendlich" msgid "Freeloads" msgstr "Schwänzereien" @@ -1130,9 +1121,6 @@ msgstr "Austragen" msgid "Sum:" msgstr "Summe:" -msgid "T-shirt score" -msgstr "T-Shirt Score" - msgid "Work log entry" msgstr "Arbeitseinsatz" @@ -1154,17 +1142,14 @@ msgstr "Du hast genug gemacht." msgid "%s has done enough." msgstr "%s hat genug gemacht." -msgid "Vouchers" -msgstr "Gutscheine" - msgid "iCal Export" msgstr "iCal Export" msgid "JSON Export" msgstr "JSON Export" -msgid "Night shifts between %d and %d am are multiplied by %d for the %s score." -msgstr "Nachtschichten zwischen %d und %d Uhr werden für den %4$s Score mit %3$d multipliziert." +msgid "Night shifts between %d and %d am are multiplied by %d for the goodie score." +msgstr "Nachtschichten zwischen %d und %d Uhr werden für den Goodie Score mit %3$d multipliziert." msgid "" "Go to the shifts table to sign yourself up for some " @@ -1188,12 +1173,6 @@ msgstr "Angekommen am %s" msgid "Not arrived (Planned: %s)" msgstr "Nicht angekommen (Geplant: %s)" -msgid "Got %s of %s vouchers" -msgstr "%s von %s Gutscheinen bekommen" - -msgid "Got no vouchers" -msgstr "Keine Gutscheine bekommen" - msgid "out of %s" msgstr "von %s" @@ -1241,18 +1220,18 @@ msgstr "" "Hier kannst du den Benutzer Eintrag ändern. Unter dem Punkt 'Angekommen' wird der Engel als anwesend markiert, " "ein Ja bei Aktiv bedeutet, dass der Engel aktiv war." -msgid "" -"If the angel is active, it can claim a T-shirt. If T-shirt is set to 'Yes', the angel already got their T-shirt." -msgstr "" -"Ist der Engel Aktiv, hat er damit Anspruch auf ein T-Shirt. Wenn T-Shirt ein 'Ja' enthält, bedeutet dies, " -"dass der Engel bereits sein T-Shirt erhalten hat." - msgid "Here you can reset the password of this angel:" msgstr "Hier kannst du das Passwort für diesen Engel zurücksetzen:" msgid "Here you can define the user groups of the angel:" msgstr "Hier kannst du die Benutzergruppen des Engels definieren:" +msgid "You cannot remove the default group." +msgstr "Du kannst die Standard-Benutzergruppe nicht entfernen." + +msgid "You removed the default group, this has unintended side effects!" +msgstr "Du hast die Standard-Benutzergruppe entfernt, dies führt zu unerwarteten Nebeneffekten!" + msgid "User groups saved." msgstr "Benutzergruppen gespeichert." @@ -1299,10 +1278,10 @@ msgstr "Goodie Statistik" msgid "Remove goodie" msgstr "Goodie entfernen" -msgid "Angel has got a goodie." +msgid "Angel got a goodie." msgstr "Engel hat ein Goodie bekommen." -msgid "Angel has got no goodie." +msgid "Angel got no goodie." msgstr "Engel hat kein Goodie bekommen." msgid "page.404.text" @@ -1337,6 +1316,9 @@ msgstr "Alle löschen" msgid "form.updated" msgstr "Aktualisiert" +msgid "form.generate" +msgstr "Generieren" + msgid "form.cancel" msgstr "Abbrechen" @@ -1350,10 +1332,10 @@ msgid "form.user_select" msgstr "Wähle einen User" msgid "form.tags" -msgstr "Schlagworte" +msgstr "Tags" msgid "form.tags.info" -msgstr "Komma separierte Liste von Schlagworten" +msgstr "Komma separierte Liste von Tags" msgid "schedule.import" msgstr "Programm importieren" @@ -1471,7 +1453,7 @@ msgid "news.edit.message" msgstr "Nachricht" msgid "news.edit.hint" -msgstr "Du kannst Markdown und den [more] Tag benutzen" +msgstr "Du kannst Markdown verwenden sowie den [more] Tag um die Vorschau vom Text zu trennen." msgid "news.delete.title" msgstr "News \"%s\" löschen" @@ -1488,6 +1470,9 @@ msgstr "Du kannst sie dir unter %3$s anschauen." msgid "form.search" msgstr "Suchen" +msgid "form.exact_match" +msgstr "Genaue Übereinstimmung" + msgid "log.log" msgstr "Logs" @@ -1577,12 +1562,9 @@ msgstr "Um gegebenenfalls Voucher für das nächste gleichartige Event zu erhalt msgid "settings.profile.privacy" msgstr "" -"Diese Zustimmung kann während dem Event in den Profil-Einstellungen, sowie, auch nach dem Event, " +"Diese Zustimmung kann während des Events in den Profil-Einstellungen, sowie, auch nach dem Event, " "per E-Mail an %1$s, widerrufen werden." -msgid "settings.profile.shirt_size" -msgstr "T-Shirt-Größe" - msgid "settings.profile.shirt_size.hint" msgstr "Ein straight-cut T-Shirt hat breite Schultern und einen fast quadratischen Körper. " "Ein fitted-cut (tailliertes) T-Shirt hat eine geschwungene Seitennaht, die an der Taille schmaler " @@ -1705,7 +1687,7 @@ msgid "settings.certificates.drive_confirmed.hint" msgstr "Dein Führerschein wurde bestätigt, du kannst deine Angaben nicht mehr selber ändern." msgid "settings.certificates.confirmation.info" -msgstr "Du hast persönlich überprüft, dass die Zertifizierung / Bescheinigung den Anforderungen genügt." +msgstr "Du hast persönlich überprüft, dass die Zertifizierung/Bescheinigung den Anforderungen genügt." msgid "settings.certificates.success" msgstr "Zertifikate wurden erfolgreich aktualisiert." @@ -1796,6 +1778,9 @@ msgstr "Login mit %s" msgid "form.connect" msgstr "Verbinden" +msgid "form.all" +msgstr "Alle" + msgid "form.disconnect" msgstr "Trennen" @@ -1841,23 +1826,41 @@ msgstr "Frage \"%s\" löschen" msgid "question.contact_options" msgstr "Weitere Kontaktmöglichkeiten: " -msgid "user.edit.shirt" -msgstr "T-Shirt bearbeiten" +msgid "tag.tags" +msgstr "Tags" + +msgid "tag.edit" +msgstr "Tag bearbeiten" + +msgid "tag.add" +msgstr "Tag erstellen" + +msgid "tag.delete.title" +msgstr "Tag \"%s\" löschen" msgid "user.edit.goodie" msgstr "Goodie bearbeiten" -msgid "form.shirt" -msgstr "T-Shirt" +msgid "user.goodie_score.enough" +msgstr "Genug" + +msgid "user.goodie_score.value" +msgstr "%s h" + +msgid "user.goodie_score" +msgstr "Goodie Score: %s" msgid "user.shirt_size" -msgstr "T-Shirt größe" +msgstr "T-Shirt-Größe" msgid "user.active" msgstr "Aktiv" msgid "user.force_active" -msgstr "Aktiv (erzwungen)" +msgstr "Dauerhaft aktiv" + +msgid "user.force_food" +msgstr "Unbegrenzt Essen" msgid "user.arrived" msgstr "Angekommen" @@ -1865,9 +1868,6 @@ msgstr "Angekommen" msgid "user.arrive" msgstr "Ankommen" -msgid "user.got_shirt" -msgstr "T-Shirt bekommen" - msgid "user.got_goodie" msgstr "Goodie bekommen" @@ -1898,8 +1898,11 @@ msgstr "Einsatzdatum" msgid "worklog.hours" msgstr "Arbeitsstunden" -msgid "worklog.comment" -msgstr "Kommentar" +msgid "worklog.description" +msgstr "Beschreibung" + +msgid "worklog.night_shift" +msgstr "Arbeitseinsatz ist eine Nachtschicht" msgid "worklog.delete" msgstr "Arbeitseinsatz löschen" @@ -1914,7 +1917,8 @@ msgid "angeltypes.about" msgstr "Teams-/Aufgabenbeschreibung" msgid "angeltypes.about.text" -msgstr "Hier findest Du die Liste der Teams und ihrer Aufgaben. Wenn Du weitere Fragen hast, schaue in den FAQ nach." +msgstr "Hier findest Du die Liste der Teams und ihrer Aufgaben. Wenn Du weitere Fragen hast, " +"schaue in den FAQ nach." msgid "angeltypes.restricted.hint" msgstr "Dieser Engeltyp benötigt eine Einweisung bei einem Einführungstreffen. " @@ -1930,6 +1934,21 @@ msgid "angeltypes.hide_on_shift_view.info" msgstr "Wenn ausgewählt, können nur Admins und Mitglieder des Engeltyps auf der " "Schicht Seite die Filteroption für diesen Engeltyp sehen." +msgid "angeltypes.qr" +msgstr "Engeltyp \"%s\" beitreten QR Code" + +msgid "angeltypes.qr.expires" +msgstr "Läuft ab %c" + +msgid "angeltypes.qr.expired" +msgstr "Abgelaufen" + +msgid "angeltypes.qr.time" +msgstr "Lebensdauer (Minuten)" + +msgid "angeltypes.qr.time.info" +msgstr "Der Link ist für die gesamte angegebene Zeitspanne gültig und kann nicht zurückgezogen werden!" + msgid "location.location" msgstr "Ort" @@ -1940,7 +1959,7 @@ msgid "location.map_url" msgstr "Karte" msgid "location.required_angels" -msgstr "Benötigte Engel (bei Fahrplan import)" +msgstr "Benötigte Engel (bei Programmimport/-kopie)" msgid "location.map_url.info" msgstr "Die Karte wird auf der Ort-Seite als iframe eingebettet." @@ -1967,7 +1986,7 @@ msgid "shifttype.delete.title" msgstr "Schichttyp \"%s\" löschen" msgid "shifttype.required_angels" -msgstr "Benötigte Engel (bei Fahrplan import)" +msgstr "Benötigte Engel (bei Programmimport/-kopie)" msgid "shifttype.edit.signup_advance_hours" msgstr "Selbsteintragen im voraus in Stunden" @@ -1993,6 +2012,11 @@ msgstr "Eventdaten" msgid "registration.what_todo" msgstr "Was möchtest Du machen?" +msgid "registration.jobs" +msgstr "" +"Weitere Informationen wie Du uns helfen kannst findest du in der " +"Teams-/Aufgabenbeschreibung." + msgid "registration.register" msgstr "Registrieren" @@ -2011,6 +2035,9 @@ msgstr "Anzahl" msgid "general.created_at" msgstr "Erstellt am" +msgid "general.url" +msgstr "URL" + msgid "shifts.history" msgstr "Schichten Historie" diff --git a/resources/lang/en_US/additional.po b/resources/lang/en_US/additional.po index 405bdd0ad..e2488ccaa 100644 --- a/resources/lang/en_US/additional.po +++ b/resources/lang/en_US/additional.po @@ -191,6 +191,9 @@ msgstr "FAQ entry successfully deleted." msgid "faq.edit.success" msgstr "FAQ entry successfully updated." +msgid "question.menu" +msgstr "Ask the Heaven" + msgid "question.delete.success" msgstr "Question deleted successfully." @@ -200,6 +203,15 @@ msgstr "Question added successfully." msgid "question.edit.success" msgstr "Question updated successfully." +msgid "tag.edit.duplicate" +msgstr "A tag with the name already exists!" + +msgid "tag.edit.success" +msgstr "Tag updated successfully." + +msgid "tag.delete.success" +msgstr "Tag deleted successfully." + msgid "notification.news.new" msgstr "New news: %s" @@ -236,6 +248,9 @@ msgstr "You have been added as an %1$s by a supporter." msgid "notification.angeltype.added.text" msgstr "You can find a description at %2$s" +msgid "angeltype.add.success" +msgstr "Successfully joined angel type" + msgid "notification.shift.deleted" msgstr "Your Shift was deleted" @@ -268,6 +283,9 @@ msgstr "Work log successfully updated." msgid "worklog.delete.success" msgstr "Work log successfully deleted." +msgid "voucher.save.success" +msgstr "Saved the number of vouchers." + msgid "location.edit.success" msgstr "Location edited successfully." diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po index 9d6606054..76f24abb1 100644 --- a/resources/lang/en_US/default.po +++ b/resources/lang/en_US/default.po @@ -40,6 +40,9 @@ msgstr "Name" msgid "general.shifts" msgstr "Shifts" +msgid "general.info" +msgstr "Info" + msgid "general.description" msgstr "Description" @@ -73,7 +76,7 @@ msgid "page.419.title" msgstr "Authentication expired" msgid "page.419.text" -msgstr "The provided CSRF token is invalid or has expired" +msgstr "The provided CSRF token is invalid or has expired. Please try again." msgid "credits.credit" msgstr "" @@ -118,6 +121,9 @@ msgstr "Delete" msgid "form.delete_all" msgstr "Delete all" +msgid "form.generate" +msgstr "Generate" + msgid "form.updated" msgstr "Updated" @@ -164,7 +170,7 @@ msgid "schedule.import.load.info" msgstr "Import \"%s\" (version \"%s\")" msgid "schedule.name" -msgstr "Programm name" +msgstr "Schedule name" msgid "schedule.url" msgstr "Schedule URL (schedule.xml)" @@ -260,7 +266,7 @@ msgid "news.edit.message" msgstr "Message" msgid "news.edit.hint" -msgstr "You can use Markdown and the [more] tag" +msgstr "You can use Markdown and the [more] tag to separate the preview from the main text." msgid "news.delete.title" msgstr "Delete news \"%s\"" @@ -277,6 +283,9 @@ msgstr "You can view it at %3$s" msgid "form.search" msgstr "Search" +msgid "form.exact_match" +msgstr "Exact match" + msgid "log.log" msgstr "Logs" @@ -368,9 +377,6 @@ msgid "settings.profile.privacy" msgstr "You can withdraw your approval during the event in your profile settings as well " "as after the event via e-mail to %1$s." -msgid "settings.profile.shirt_size" -msgstr "T-shirt size" - msgid "settings.profile.shirt_size.hint" msgstr "A straight-cut shirt has wide shoulders and a body which is almost square. " "A fitted-cut t-shirt has a curved side seam which comes in at the waist " @@ -493,7 +499,7 @@ msgid "settings.certificates.drive_confirmed.hint" msgstr "Your driving license has been confirmed, you can no longer change it by yourself." msgid "settings.certificates.confirmation.info" -msgstr "You personally checked that the certificate / license meets the requirements." +msgstr "You personally checked that the certificate/license meets the requirements." msgid "settings.certificates.success" msgstr "Certificates were updated successfully." @@ -590,6 +596,9 @@ msgstr "Login using %s" msgid "form.connect" msgstr "Connect" +msgid "form.all" +msgstr "All" + msgid "form.disconnect" msgstr "Disconnect" @@ -635,12 +644,30 @@ msgstr "Delete question \"%s\"" msgid "question.contact_options" msgstr "Other contact options: " -msgid "user.edit.shirt" -msgstr "Edit T-shirt" +msgid "tag.tags" +msgstr "Tags" + +msgid "tag.edit" +msgstr "Edit tag" + +msgid "tag.add" +msgstr "Create new tag" + +msgid "tag.delete.title" +msgstr "Delete tag \"%s\"" msgid "user.edit.goodie" msgstr "Edit goodie" +msgid "user.goodie_score.enough" +msgstr "Enough" + +msgid "user.goodie_score.value" +msgstr "%s h" + +msgid "user.goodie_score" +msgstr "Goodie score: %s" + msgid "user.shirt_size" msgstr "T-shirt size" @@ -648,7 +675,10 @@ msgid "user.active" msgstr "Active" msgid "user.force_active" -msgstr "Active (forced)" +msgstr "Always active" + +msgid "user.force_food" +msgstr "Unlimited food" msgid "user.arrived" msgstr "Arrived" @@ -656,9 +686,6 @@ msgstr "Arrived" msgid "user.arrive" msgstr "Arrive" -msgid "user.got_shirt" -msgstr "Got T-shirt" - msgid "user.got_goodie" msgstr "Got goodie" @@ -689,8 +716,11 @@ msgstr "Work date" msgid "worklog.hours" msgstr "Work hours" -msgid "worklog.comment" -msgstr "Comment" +msgid "worklog.description" +msgstr "Description" + +msgid "worklog.night_shift" +msgstr "Work log is a night shift" msgid "worklog.delete" msgstr "Delete work log" @@ -698,6 +728,27 @@ msgstr "Delete work log" msgid "worklog.delete.info" msgstr "Do you really want to delete the work log for %s?" +msgid "voucher.vouchers" +msgstr "Vouchers" + +msgid "voucher.edit" +msgstr "Edit vouchers" + +msgid "voucher.eligible" +msgstr "Angel can receive another %d vouchers." + +msgid "voucher.eligible.force" +msgstr "Angel can receive %d unlimited vouchers." + +msgid "voucher.count" +msgstr "Number of vouchers handed out" + +msgid "user.state.vouchers" +msgstr "Got %s of %s vouchers" + +msgid "user.state.vouchers.force" +msgstr "%d unlimited" + msgid "angeltypes.angeltypes" msgstr "Volunteer types" @@ -705,7 +756,8 @@ msgid "angeltypes.about" msgstr "Teams-/Job description" msgid "angeltypes.about.text" -msgstr "Here you can find the list of teams and their tasks. If you have further questions, have a look at the FAQ." +msgstr "Here you can find the list of teams and their tasks. If you have further questions, " +"have a look at the FAQ." msgid "angeltypes.restricted" msgstr "Requires introduction" @@ -738,8 +790,20 @@ msgid "angeltypes.hide_on_shift_view.info" msgstr "If checked only admins and members of the volunteer type " "can see the filter option for this volunteer type on the shifts page" -msgid "registration.register" -msgstr "Register" +msgid "angeltypes.qr" +msgstr "Join angel type \"%s\" QR code" + +msgid "angeltypes.qr.expires" +msgstr "Expires %c" + +msgid "angeltypes.qr.expired" +msgstr "Expired" + +msgid "angeltypes.qr.time" +msgstr "Lifetime (minutes)" + +msgid "angeltypes.qr.time.info" +msgstr "The link will be valid for the whole time span and can't be revoked!" msgid "location.location" msgstr "Location" @@ -807,6 +871,10 @@ msgstr "Reset done. Volunteer has not arrived." msgid "registration.what_todo" msgstr "What do you want to do?" +msgid "registration.jobs" +msgstr "You can find more information about how to help us in the " +"Teams-/Job description." + msgid "registration.register" msgstr "Register" @@ -866,6 +934,12 @@ msgstr "Volunteer has got no goodie." msgid "Delete angeltype %s" msgstr "Delete volunteer type %s" +msgid "general.created_at" +msgstr "Created at" + +msgid "general.url" +msgstr "URL" + msgid "User is not in angeltype." msgstr "User is not in volunteer type." @@ -961,6 +1035,9 @@ msgstr "%1$s, starting %2$s" msgid "Search angel:" msgstr "Search volunteers:" +msgid "footer.issues" +msgstr "Bugs/Features" + msgid "How much angels should be active?" msgstr "How many volunteers should be active?" @@ -1004,12 +1081,11 @@ msgstr "" msgid "Allow heaven angels to contact you by e-mail." msgstr "Allow volunteer managers to contact you by e-mail." -msgid "" -"Some angel types have to be confirmed later by a supporter at an " -"introduction meeting. You can change your selection in the options section." -msgstr "" -"Some volunteer types have to be confirmed later by a supporter at an " -"introduction meeting. You can change your selection in the options section." +msgid "shift.next" +msgstr "Next shift" + +msgid "shift.previous" +msgstr "Previous shift" msgid "Hello %s, here can you leave messages for other angels" msgstr "Hello %s, here you can leave messages for other volunteers" diff --git a/resources/views/admin/locations/edit.twig b/resources/views/admin/locations/edit.twig index b68c21653..76157b4b0 100644 --- a/resources/views/admin/locations/edit.twig +++ b/resources/views/admin/locations/edit.twig @@ -25,6 +25,7 @@ 'type': 'url', 'value': f.formData('map_url', location ? location.map_url : ''), 'info': __('location.map_url.info'), + 'max_length': 300, }) }} {{ f.textarea('description', __('general.description'), { diff --git a/resources/views/admin/log.twig b/resources/views/admin/log.twig index ad0e8d850..0c65781f2 100644 --- a/resources/views/admin/log.twig +++ b/resources/views/admin/log.twig @@ -15,7 +15,7 @@ {{ csrf() }} - + {{ f.input('search', __('form.search'), { 'value': search, }) }} @@ -28,6 +28,12 @@ 'selected': search_user_id, }) }} + + {{ f.select('level', __('log.level'), levels, { + 'default_option': __('form.all'), + 'selected': level, + }) }} + {% endif %} @@ -41,39 +47,52 @@ {% endif %} - - - {{ __('log.time') }} - {{ __('log.level') }} - {{ __('general.user') }} - {{ __('log.message') }} - - {% for entry in entries %} - {%- set type = 'default' %} - {%- if entry.level in ['notice', 'info'] %} - {%- set type = 'info' %} - {%- endif %} - {%- if entry.level in ['error', 'warning'] %} - {%- set type = 'warning' %} - {%- endif %} - {%- if entry.level in ['emergency', 'alert', 'critical'] %} - {%- set type = 'danger' %} - {%- endif %} - - {%- set td_type = '' %} - {%- if type in ['warning', 'danger'] %} - {%- set td_type = type %} - {%- endif %} - + + - {{ entry.created_at.format(__('general.datetime')) }} - - {{ entry.level|capitalize }} - - {% if entry.user %}{{ m.user(entry.user) }}{% endif %} - {{ entry.message|nl2br }} + {{ __('log.time') }} + {{ __('log.level') }} + {{ __('general.user') }} + {{ __('log.message') }} + - {% endfor %} + + + {% for entry in entries %} + {%- set type = 'default' %} + {%- if entry.level in ['notice', 'info'] %} + {%- set type = 'info' %} + {%- endif %} + {%- if entry.level in ['warning'] %} + {%- set type = 'warning' %} + {%- endif %} + {%- if entry.level in ['error', 'emergency', 'alert', 'critical'] %} + {%- set type = 'danger' %} + {%- endif %} + + {%- set td_type = '' %} + {%- if type in ['warning', 'danger'] %} + {%- set td_type = type %} + {%- endif %} + + + + + {{ entry.created_at.format(__('general.datetime')) }} + + + {{ entry.level|capitalize }} + + {% if entry.user %}{{ m.user(entry.user) }}{% endif %} + {{ entry.message|nl2br }} + + + {{ m.icon('link') }} + + + + {% endfor %} + diff --git a/resources/views/admin/schedule/edit.twig b/resources/views/admin/schedule/edit.twig index b8c5749f4..34a555a66 100644 --- a/resources/views/admin/schedule/edit.twig +++ b/resources/views/admin/schedule/edit.twig @@ -23,32 +23,32 @@ {{ f.input('name', __('schedule.name'), { 'required': true, - 'value': schedule.name, + 'value': f.formData('name', schedule.name), 'max_length': 255, }) }} {{ f.input('url', __('schedule.url'), { 'type': 'url', 'required': true, - 'value': schedule.url + 'value': f.formData('url', schedule.url) }) }} {{ f.select('shift_type', __('schedule.shift-type'), shift_types|default([]), { - 'selected': schedule.shift_type, + 'selected': f.formData('shift_type', schedule.shift_type) ~ '', }) }} {{ f.checkbox('needed_from_shift_type', __('schedule.needed-from-shift-type'), { - 'checked': schedule.needed_from_shift_type, + 'checked': f.formData('needed_from_shift_type', schedule.needed_from_shift_type), }) }} {{ f.input('minutes_before', __('schedule.minutes-before'), { 'type': 'number', 'required': true, - 'value': schedule.id ? schedule.minutes_before : 15 + 'value': f.formData('minutes_before', schedule.id ? schedule.minutes_before : 15) }) }} {{ f.input('minutes_after', __('schedule.minutes-after'), { 'type': 'number', 'required': true, - 'value': schedule.id ? schedule.minutes_after : 15 + 'value': f.formData('minutes_after', schedule.id ? schedule.minutes_after : 15) }) }} {{ f.save(__('form.save')) }} @@ -68,7 +68,7 @@ {{ f.checkbox( 'location_' ~ id, name, - {'checked': schedule.id and id in schedule.activeLocations.pluck('id')} + {'checked': f.formData('location_' ~ id, id in schedule.activeLocations.pluck('id'))} ) }} {% endfor %} diff --git a/resources/views/admin/schedule/index.twig b/resources/views/admin/schedule/index.twig index 85fd35c51..f7f1bae4f 100644 --- a/resources/views/admin/schedule/index.twig +++ b/resources/views/admin/schedule/index.twig @@ -21,7 +21,7 @@ {{ __('schedule.import.text') }} - + diff --git a/resources/views/admin/schedule/load.twig b/resources/views/admin/schedule/load.twig index db0a43a5a..57a673506 100644 --- a/resources/views/admin/schedule/load.twig +++ b/resources/views/admin/schedule/load.twig @@ -35,7 +35,7 @@ {% macro locationsTable(locations) %} - + @@ -55,7 +55,7 @@ {% endmacro %} {% macro shiftsTable(shifts) %} - + diff --git a/resources/views/admin/shifts/history.twig b/resources/views/admin/shifts/history.twig index ace45b592..9757efa80 100644 --- a/resources/views/admin/shifts/history.twig +++ b/resources/views/admin/shifts/history.twig @@ -16,7 +16,7 @@ {% block row_content %} - + diff --git a/resources/views/admin/shifttypes/index.twig b/resources/views/admin/shifttypes/index.twig index fe24b82f3..176374a56 100644 --- a/resources/views/admin/shifttypes/index.twig +++ b/resources/views/admin/shifttypes/index.twig @@ -26,7 +26,7 @@ {% block row_content %} - + diff --git a/resources/views/admin/shifttypes/view.twig b/resources/views/admin/shifttypes/view.twig index 42d6540c3..f885f8b25 100644 --- a/resources/views/admin/shifttypes/view.twig +++ b/resources/views/admin/shifttypes/view.twig @@ -5,30 +5,93 @@ {% block title %}{{ shifttype.name }}{% endblock %} {% block row_content %} - {% if shifttype.signup_advance_hours %} - - {{ __('shifttype.edit.signup_advance_hours') }} {{ f.info(__('shifttype.edit.signup_advance_hours.info')) }}: {{ shifttype.signup_advance_hours }} - - {% endif %} - - {{ __('general.description') }}{{ shifttype.description|md }} - + + + + {{ __('general.info') }} + + + + + {{ __('general.shifts') }} + + + + + + + + {% if shifttype.signup_advance_hours %} + + {{ __('shifttype.edit.signup_advance_hours') }} + {{ f.info(__('shifttype.edit.signup_advance_hours.info')) }}: + {{ shifttype.signup_advance_hours }} + + {% endif %} + + + {{ __('general.description') }}{{ shifttype.description|md }} + - {% if shifttype.neededAngelTypes.isNotEmpty() %} - - {{ __('location.required_angels') }} - - {% for neededAngelType in shifttype.neededAngelTypes %} - {% if neededAngelType.count %} - - - {{ neededAngelType.angelType.name -}} - : {{ neededAngelType.count }} - + {% if shifttype.neededAngelTypes.isNotEmpty() %} + + {{ __('location.required_angels') }} + + {% for neededAngelType in shifttype.neededAngelTypes %} + {% if neededAngelType.count %} + + + {{ neededAngelType.angelType.name -}} + : + {{ neededAngelType.count }} + + {% endif %} + {% endfor %} + + + {% endif %} + + + + + + {% if shifts|length %} + + {{ selected_day|dateWithEventDay }} + {% endif %} - {% endfor %} - + + {% for day in days %} + + + {{ day|dateWithEventDay }} + + + {% endfor %} + + + + {{ renderShifts(shifts) }} + + - {% endif %} + {% endblock %} diff --git a/resources/views/admin/user/edit-goodie.twig b/resources/views/admin/user/edit-goodie.twig index ec6891331..69f08b518 100644 --- a/resources/views/admin/user/edit-goodie.twig +++ b/resources/views/admin/user/edit-goodie.twig @@ -3,18 +3,23 @@ {% import 'macros/form.twig' as f %} {% block title %} - {{ is_tshirt ? __('user.edit.shirt') : __('user.edit.goodie') }} + {{ __('user.edit.goodie') }} {%- endblock %} {% block content %} {{ m.back(url('/admin-active')) }} - {{ block('title') }}: {{ m.user(userdata) }} + {{ block('title') }}: {{ m.user(userdata, {'pronoun': true}) }} {{ m.user_info(userdata) }} {% include 'layouts/parts/messages.twig' %} + + {% set score = goodie_score == '~' ? __('user.goodie_score.enough') : __('user.goodie_score.value', [goodie_score])%} + {{ __('user.goodie_score', [score]) }} + + {{ csrf() }} @@ -46,7 +51,7 @@ 'checked': userdata.state.active, }) }} - {{ f.switch('got_goodie', is_tshirt ? __('user.got_shirt') : __('user.got_goodie'), { + {{ f.switch('got_goodie', __('user.got_goodie'), { 'checked': userdata.state.got_goodie, }) }} diff --git a/resources/views/admin/user/edit-voucher.twig b/resources/views/admin/user/edit-voucher.twig new file mode 100644 index 000000000..7b4be43e4 --- /dev/null +++ b/resources/views/admin/user/edit-voucher.twig @@ -0,0 +1,35 @@ +{% extends "layouts/app.twig" %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ __('voucher.edit') }}{% endblock %} + +{% block content %} + + + {{ m.back(url('/users', {action: 'view', user_id: userdata.id})) }} + {{ block('title') }}: {{ m.user(userdata) }} + + + {% include 'layouts/parts/messages.twig' %} + {{ m.alert( + forceActive or forceFood + ? __('voucher.eligible.force', [eligibleVoucherCount]) + : __('voucher.eligible', [eligibleVoucherCount]) + , 'info', true) }} + + + {{ csrf() }} + + {{ f.input('got_voucher', __('voucher.count'), { + 'type': 'number', + 'value': gotVoucher, + 'required': true, + 'step': '1', + 'min': 0, + }) }} + + {{ f.submit(__('form.save'), {'icon_left': 'save'}) }} + + +{% endblock %} diff --git a/resources/views/admin/user/edit-worklog.twig b/resources/views/admin/user/edit-worklog.twig index 14a9d2275..479a1a2eb 100644 --- a/resources/views/admin/user/edit-worklog.twig +++ b/resources/views/admin/user/edit-worklog.twig @@ -35,11 +35,16 @@ 'step': '0.01', 'min': 0, }) }} - {{ f.input('comment', __('worklog.comment'), { - 'value': comment, + {{ f.input('description', __('worklog.description'), { + 'value': description, 'required': true, 'max_length': 200, }) }} + {% if attribute(config('night_shifts'), 'enabled') %} + {{ f.checkbox('night_shift', __('worklog.night_shift'), { + 'checked': night_shift, + }) }} + {% endif %} {{ f.submit(__('form.save'), {'icon_left': 'save'}) }} diff --git a/resources/views/layouts/app.twig b/resources/views/layouts/app.twig index 025d15361..da3851312 100644 --- a/resources/views/layouts/app.twig +++ b/resources/views/layouts/app.twig @@ -22,6 +22,14 @@ {%- endif %} {% endblock %} + + + + diff --git a/resources/views/layouts/parts/navbar.twig b/resources/views/layouts/parts/navbar.twig index 02ea90293..f3a4c5884 100644 --- a/resources/views/layouts/parts/navbar.twig +++ b/resources/views/layouts/parts/navbar.twig @@ -41,7 +41,7 @@ > - + {% for name,opt in menu() %} {% set url = opt is iterable ? opt[0] : opt %} @@ -82,7 +82,7 @@ {% if is_guest() %} {% include "layouts/parts/language_dropdown.twig" %} - {% if can('register') and config('registration_enabled') %} + {% if can('register') and config('registration_enabled') and config('enable_password') %} {{ _self.toolbar_item( __('general.register'), config('external_registration_url') ?: url('/register'), diff --git a/resources/views/macros/base.twig b/resources/views/macros/base.twig index 386c0b076..15edf3b93 100644 --- a/resources/views/macros/base.twig +++ b/resources/views/macros/base.twig @@ -1,6 +1,6 @@ -{% macro angel() %} +{% macro angel() -%} -{% endmacro %} +{%- endmacro %} {% macro icon(icon, color) %} @@ -26,7 +26,7 @@ - {{ _self.angel() }} {{ user.displayName }} + {{ _self.angel() }} {{ user.displayName }} {%- if opt.pronoun|default(false) and config('enable_pronoun') and user.personalData.pronoun %} ({{ user.personalData.pronoun }}) @@ -34,6 +34,21 @@ {% endmacro %} +{% macro user_info(user, opt) %} + {% if user.state.user_info and can('admin_arrive') %} + + {{ _self.icon('info-circle-fill', 'info') }} + + {% endif %} +{% endmacro %} + {% macro button(label, url, opt) %} - {{ __('angeltypes.about.text') }} + {{ __('angeltypes.about.text', [url('/faq')])|raw }} diff --git a/resources/views/pages/angeltypes/qr.twig b/resources/views/pages/angeltypes/qr.twig new file mode 100644 index 000000000..e9c7fc6d1 --- /dev/null +++ b/resources/views/pages/angeltypes/qr.twig @@ -0,0 +1,59 @@ +{% extends "layouts/app.twig" %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ __('angeltypes.qr', [angel_type.name]) }}{% endblock %} + +{% block content %} + + + + {{ m.back(url('/angeltypes', {'action': 'view', 'angeltype_id': angel_type.id})) }} + {{ block('title') }} + + + + {% if qr_data %} + + + {{ qr(qr_data, 400) }} + {% set expires = session_get('form-data-expires') %} + {% if expires %} + + + {{ __('angeltypes.qr.expires') }} + + + {% endif %} + + + + {% endif %} + {{ f.input('url', __('general.url'), {'value': qr_data, 'readonly': true, 'disabled': not qr_data}) }} + + + {{ csrf() }} + + + {{ f.input('minutes', __('angeltypes.qr.time'), { + 'type': 'number', + 'required': true, + 'value': f.formData('minutes', 5), + 'min': 1, + 'max': qr_max_expiration_minutes, + 'info': __('angeltypes.qr.time.info'), + }) }} + + + + {{ f.submit(__('form.generate'), {'icon_left': 'qr-code', 'class': 'mb-3'}) }} + + + + + +{% endblock %} diff --git a/resources/views/pages/design.twig b/resources/views/pages/design.twig index 054cd0045..c8faa1418 100644 --- a/resources/views/pages/design.twig +++ b/resources/views/pages/design.twig @@ -72,22 +72,26 @@ Tables {{ m.icon('link') }} - - - Header 1 - Header 2 - Header 3 - - - Table content - {{ lipsum }} - {{ m.icon('check-lg') }} - - - Another content - Lorem ipsum - {{ m.icon('x-lg') }} - + + + + Header 1 + Header 2 + Header 3 + + + + + Table content + {{ lipsum }} + {{ m.icon('check-lg') }} + + + Another content + Lorem ipsum + {{ m.icon('x-lg') }} + + @@ -136,6 +140,10 @@ {% endfor %} + + + Markdown {{ m.icon('link') }} + {{ 'In _texts_ you can use **Markdown**! (Can be [GitHub Flavoured](https://commonmark.thephpleague.com/2.x/extensions/github-flavored-markdown/) with [Attributes](https://commonmark.thephpleague.com/2.x/extensions/attributes/)).' | md }} @@ -190,6 +198,7 @@ info hidden: {{ m.icon('eye-slash') }} language: {{ m.icon('translate') }} location: {{ m.icon('pin-map-fill') }} + random / shuffle: {{ m.icon('shuffle') }} remove / substract: {{ m.icon('dash-lg') }} requires introduction: {{ m.icon('mortarboard-fill') }} search: {{ m.icon('search') }} @@ -200,7 +209,7 @@ yes / no: {{ m.iconBool(true) }} / {{ m.iconBool(false) }} - angeltypes: {{ m.icon('person-lines-fill') }} + angel types: {{ m.icon('person-lines-fill') }} arrived: {{ m.icon('house') }} comment: {{ m.icon('chat-left-text') }} email: {{ m.icon('envelope') }} @@ -214,7 +223,7 @@ occupancy: {{ m.icon('person-fill-slash') }} password: {{ m.icon('key-fill') }} phone: {{ m.icon('phone') }} - T-shirt / goodie: {{ m.icon('gift') }} + Goodie: {{ m.icon('gift') }} supporter: {{ m.icon('patch-check') }} user settings: {{ m.icon('person-fill-gear') }} voucher: {{ m.icon('valentine') }} diff --git a/resources/views/pages/faq/edit.twig b/resources/views/pages/faq/edit.twig index 156efb874..7b8de7b2b 100644 --- a/resources/views/pages/faq/edit.twig +++ b/resources/views/pages/faq/edit.twig @@ -80,6 +80,15 @@ {{ faq.text|markdown }} + diff --git a/resources/views/pages/locations/index.twig b/resources/views/pages/locations/index.twig index 54b6a3985..dcdf1bf17 100644 --- a/resources/views/pages/locations/index.twig +++ b/resources/views/pages/locations/index.twig @@ -26,7 +26,7 @@ {% block row_content %} - + @@ -69,7 +69,7 @@ {{ m.edit(url('/admin/locations/edit/' ~ location.id), {'class': 'm-1'}) }} - + {{ csrf() }} {{ f.hidden('id', location.id) }} {{ f.delete(null, { diff --git a/resources/views/pages/messages/index.twig b/resources/views/pages/messages/index.twig index 9a1392a26..354d7126b 100644 --- a/resources/views/pages/messages/index.twig +++ b/resources/views/pages/messages/index.twig @@ -27,7 +27,7 @@ - + {{ __('general.angel') }} diff --git a/resources/views/pages/registration.twig b/resources/views/pages/registration.twig index 7e13e0a4b..d9347c57e 100644 --- a/resources/views/pages/registration.twig +++ b/resources/views/pages/registration.twig @@ -209,7 +209,7 @@ {{ f.select( 'tshirt_size', - __('settings.profile.shirt_size'), + __('user.shirt_size'), tShirtSizes, { 'default_option': __('form.select_placeholder'), @@ -273,6 +273,9 @@ {{ __('registration.what_todo') }} + + {{ __('registration.jobs', [url('/angeltypes/about')])|raw }} + {% for angelType in angelTypes %} diff --git a/resources/views/pages/settings/certificates-admin.twig b/resources/views/pages/settings/certificates-admin.twig index a61c754cc..7a4e23730 100644 --- a/resources/views/pages/settings/certificates-admin.twig +++ b/resources/views/pages/settings/certificates-admin.twig @@ -5,7 +5,7 @@ {% block row_content %} - {% if config('ifsg_enabled') %} + {% if config('ifsg_enabled') and ifsg %} {% endif %} - {% if config('driving_license_enabled') %} + {% if config('driving_license_enabled') and driver_license %} + {{ __('settings.oauth.identity-provider') }} @@ -20,7 +20,7 @@ {% for name,config in providers %} - + {% if config.url|default %} @@ -37,7 +37,7 @@ {{ f.submit(__('form.connect'), {'size' : 'sm', 'icon_left': 'box-arrow-in-right'}) }} - {% else %} + {% elseif config.allow_user_disconnect is not defined or config.allow_user_disconnect %} {{ csrf() }} diff --git a/resources/views/pages/settings/profile.twig b/resources/views/pages/settings/profile.twig index 4926cbd67..cbac45c1d 100644 --- a/resources/views/pages/settings/profile.twig +++ b/resources/views/pages/settings/profile.twig @@ -150,7 +150,7 @@ {% if goodie_tshirt %} - {{ f.select('shirt_size', __('settings.profile.shirt_size'), config('tshirt_sizes'), { + {{ f.select('shirt_size', __('user.shirt_size'), config('tshirt_sizes'), { 'selected': userdata.personalData.shirt_size, 'required': isTShirtSizeRequired, 'required_icon': isTShirtSizeRequired, diff --git a/resources/views/pages/settings/sessions.twig b/resources/views/pages/settings/sessions.twig index 934010261..1b711ca03 100644 --- a/resources/views/pages/settings/sessions.twig +++ b/resources/views/pages/settings/sessions.twig @@ -9,7 +9,7 @@ {{ m.info(__('settings.sessions.info')) }} - + diff --git a/resources/views/pages/tag/edit.twig b/resources/views/pages/tag/edit.twig new file mode 100644 index 000000000..d65449b06 --- /dev/null +++ b/resources/views/pages/tag/edit.twig @@ -0,0 +1,39 @@ +{% extends 'layouts/app.twig' %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ tag and tag.id ? __('tag.edit') : __('tag.add') }}{% endblock %} + +{% block content %} + + + {{ m.back(url('/admin/tags')) }} + {{ block('title') }} + + + {% include 'layouts/parts/messages.twig' %} + + + {{ csrf() }} + + + + {{ f.input('name', __('general.name'), { + 'required': true, + 'required_icon': true, + 'value': tag ? tag.name : '', + 'max_length': 255, + }) }} + + + {{ f.submit(__('form.save'), {'icon_left': 'save'}) }} + + {% if tag and tag.id %} + {{ f.delete(__('form.delete'), {'confirm_title': __('tag.delete.title', [tag.name[:40]|e])})}} + {% endif %} + + + + + +{% endblock %} diff --git a/resources/views/pages/tag/index.twig b/resources/views/pages/tag/index.twig new file mode 100644 index 000000000..492fb6f9c --- /dev/null +++ b/resources/views/pages/tag/index.twig @@ -0,0 +1,54 @@ +{% extends 'layouts/app.twig' %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %} + {{ __('tag.tags') }} +{% endblock %} + +{% block content %} + + + {{ block('title') }} + + {% if not tag|default(false) %} + {{ m.add(url('/admin/tags/edit')) }} + {% endif %} + + + {% include 'layouts/parts/messages.twig' %} + + + + + + + {{ __('general.name') }} + + + + + + {% block row %} + {% for item in items %} + + {{ item.name }} + + + {{ m.edit(url('admin/tags/edit/' ~ item.id)) }} + + + {{ csrf() }} + {{ f.delete(null, {'size': 'sm', 'confirm_title': __('tag.delete.title', [item.name[:40]|e])}) }} + + + + + {% endfor %} + {% endblock %} + + + + + +{% endblock %} diff --git a/src/Application.php b/src/Application.php index ca549a108..b4c8bce5b 100644 --- a/src/Application.php +++ b/src/Application.php @@ -29,7 +29,7 @@ class Application extends Container /** * Application constructor. */ - public function __construct(string $appPath = null) + public function __construct(?string $appPath = null) { if (!is_null($appPath)) { $this->setAppPath($appPath); @@ -71,9 +71,8 @@ public function register(string|ServiceProvider $provider): ServiceProvider /** * Boot service providers * - * @param Config|null $config */ - public function bootstrap(Config $config = null): void + public function bootstrap(?Config $config = null): void { if ($this->isBootstrapped) { return; diff --git a/src/Config/ConfigServiceProvider.php b/src/Config/ConfigServiceProvider.php index 7d2f1f0be..e53227b3a 100644 --- a/src/Config/ConfigServiceProvider.php +++ b/src/Config/ConfigServiceProvider.php @@ -70,7 +70,7 @@ public function register(): void public function boot(): void { if (!$this->eventConfig) { - return; + $this->eventConfig = $this->app->make(EventConfig::class); } /** @var Config $config */ diff --git a/src/Controllers/Admin/FaqController.php b/src/Controllers/Admin/FaqController.php index 642f0885b..f1eec58ba 100644 --- a/src/Controllers/Admin/FaqController.php +++ b/src/Controllers/Admin/FaqController.php @@ -11,6 +11,7 @@ use Engelsystem\Http\Response; use Engelsystem\Models\Faq; use Engelsystem\Models\Tag; +use Illuminate\Support\Collection; use Psr\Log\LoggerInterface; class FaqController extends BaseController @@ -62,23 +63,33 @@ public function save(Request $request): Response $faq->question = $data['question']; $faq->text = $data['text']; + $tags = collect(explode(',', $data['tags'] ?? '')) + ->transform(fn($value) => trim($value)) + ->filter(fn($value) => $value != '') + ->unique(); + if (!is_null($data['preview'])) { + $faq['tags'] = new Collection(); + foreach ($tags as $tagName) { + $tag = new Tag(['name' => $tagName]); + $faq['tags'][] = $tag; + } + return $this->showEdit($faq, $data['tags']); } $faq->save(); $faq->tags()->detach(); - $tags = collect(explode(',', $data['tags'] ?? '')) - ->transform(fn($value) => trim($value)) - ->filter(fn($value) => $value != '') - ->unique(); foreach ($tags as $tagName) { $tag = Tag::whereName($tagName)->firstOrCreate(['name' => $tagName]); $faq->tags()->attach($tag); } - $this->log->info('Updated faq "{question}": {text}', ['question' => $faq->question, 'text' => $faq->text]); + $this->log->info( + 'Saved faq "{question}" ({id}): {text}', + ['question' => $faq->question, 'text' => $faq->text, 'id' => $faq->id] + ); $this->addNotification('faq.edit.success'); @@ -89,14 +100,14 @@ protected function delete(Faq $faq): Response { $faq->delete(); - $this->log->info('Deleted faq "{question}"', ['question' => $faq->question]); + $this->log->info('Deleted faq "{question}" ({id})', ['question' => $faq->question, 'id' => $faq->id]); $this->addNotification('faq.delete.success'); return $this->redirect->to('/faq'); } - protected function showEdit(?Faq $faq, string $tags = null): Response + protected function showEdit(?Faq $faq, ?string $tags = null): Response { return $this->response->withView( 'pages/faq/edit.twig', diff --git a/src/Controllers/Admin/LocationsController.php b/src/Controllers/Admin/LocationsController.php index c0c43cc7b..b4fc2b97a 100644 --- a/src/Controllers/Admin/LocationsController.php +++ b/src/Controllers/Admin/LocationsController.php @@ -85,7 +85,7 @@ public function save(Request $request): Response 'name' => 'required|max:35', 'description' => 'optional', 'dect' => 'optional', - 'map_url' => 'optional|url', + 'map_url' => 'optional|url|max:300', ] + $validation ); diff --git a/src/Controllers/Admin/LogsController.php b/src/Controllers/Admin/LogsController.php index 10f25240c..90e874afe 100644 --- a/src/Controllers/Admin/LogsController.php +++ b/src/Controllers/Admin/LogsController.php @@ -11,6 +11,8 @@ use Engelsystem\Models\LogEntry; use Engelsystem\Models\User\User; use Illuminate\Support\Collection; +use Illuminate\Support\Str; +use Psr\Log\LogLevel; class LogsController extends BaseController { @@ -19,6 +21,17 @@ class LogsController extends BaseController 'admin_log', ]; + protected array $levels = [ + LogLevel::ALERT, + LogLevel::CRITICAL, + LogLevel::DEBUG, + LogLevel::EMERGENCY, + LogLevel::ERROR, + LogLevel::INFO, + LogLevel::NOTICE, + LogLevel::WARNING, + ]; + public function __construct(protected LogEntry $log, protected Response $response, protected Authenticator $auth) { } @@ -27,13 +40,18 @@ public function index(Request $request): Response { $searchUserId = (int) $request->input('search_user_id') ?: null; $search = $request->input('search'); + $level = $request->input('level'); $userId = $this->auth->user()?->id; if ($this->auth->can('logs.all')) { $userId = $searchUserId; } - $entries = $this->log->filter($search, $userId); + if (!in_array($level, $this->levels)) { + $level = null; + } + + $entries = $this->log->filter($search, $userId, $level); /** @var Collection $users */ $users = User::with('personalData') @@ -43,9 +61,21 @@ public function index(Request $request): Response return [$u->id => $u->displayName]; }); + $levels = array_combine($this->levels, $this->levels); + foreach ($levels as $k => $v) { + $levels[$k] = Str::ucfirst($v); + } + return $this->response->withView( 'admin/log.twig', - ['entries' => $entries, 'search' => $search, 'users' => $users, 'search_user_id' => $searchUserId] + [ + 'entries' => $entries, + 'search' => $search, + 'users' => $users, + 'search_user_id' => $searchUserId, + 'level' => $level, + 'levels' => $levels, + ] ); } } diff --git a/src/Controllers/Admin/NewsController.php b/src/Controllers/Admin/NewsController.php index 10a18b3b8..39d959680 100644 --- a/src/Controllers/Admin/NewsController.php +++ b/src/Controllers/Admin/NewsController.php @@ -84,6 +84,7 @@ public function save(Request $request): Response if (!$news->user) { $news->user()->associate($this->auth->user()); } + $contentChanged = $news->title != $data['title'] || $news->text != $data['text']; $news->title = $data['title']; $news->text = $data['text']; $news->is_meeting = !is_null($data['is_meeting']); @@ -103,6 +104,9 @@ public function save(Request $request): Response $this->addNotification('news.edit.duplicate', NotificationType::ERROR); return $this->showEdit($news, $notify); } + if (!$isNewNews & !$contentChanged) { + $news->timestamps = false; + } $news->save(); if ($isNewNews) { @@ -112,8 +116,9 @@ public function save(Request $request): Response } $this->log->info( - 'Updated {pinned}{type} "{news}": {text}', + 'Saved {pinned}{highlighted}{type} "{news}" ({id}): {text}', [ + 'id' => $news->id, 'pinned' => $news->is_pinned ? 'pinned ' : '', 'highlighted' => $news->is_highlighted ? 'highlighted ' : '', 'type' => $news->is_meeting ? 'meeting' : 'news', @@ -132,8 +137,9 @@ protected function delete(News $news): Response $news->delete(); $this->log->info( - 'Deleted {type} "{news}"', + 'Deleted {type} "{news}" ({id})', [ + 'id' => $news->id, 'type' => $news->is_meeting ? 'meeting' : 'news', 'news' => $news->title, ] diff --git a/src/Controllers/Admin/QuestionsController.php b/src/Controllers/Admin/QuestionsController.php index 890c98724..56f77afd2 100644 --- a/src/Controllers/Admin/QuestionsController.php +++ b/src/Controllers/Admin/QuestionsController.php @@ -57,7 +57,15 @@ public function delete(Request $request): Response $question = $this->question->findOrFail($data['id']); $question->delete(); - $this->log->info('Deleted question {question}', ['question' => $question->text]); + $this->log->info( + 'Deleted question "{text}" ({id}) by {user} ({user_id})', + [ + 'text' => $question->text, + 'id' => $question->id, + 'user' => $question->user->name, + 'user_id' => $question->user->id, + ] + ); $this->addNotification('question.delete.success'); return $this->redirect->to('/admin/questions'); @@ -67,7 +75,7 @@ public function edit(Request $request): Response { $questionId = (int) $request->getAttribute('question_id'); - $questions = $this->question->find($questionId); + $questions = $this->question->findOrFail($questionId); return $this->showEdit($questions); } @@ -77,7 +85,7 @@ public function save(Request $request): Response $questionId = (int) $request->getAttribute('question_id'); /** @var Question $question */ - $question = $this->question->findOrNew($questionId); + $question = $this->question->findOrFail($questionId); $data = $this->validate($request, [ 'text' => 'required', @@ -89,7 +97,15 @@ public function save(Request $request): Response if (!is_null($data['delete'])) { $question->delete(); - $this->log->info('Deleted question "{question}"', ['question' => $question->text]); + $this->log->info( + 'Deleted question "{text}" ({id}) by {user} ({user_id})', + [ + 'text' => $question->text, + 'id' => $question->id, + 'user' => $question->user->name, + 'user_id' => $question->user->id, + ] + ); $this->addNotification('question.delete.success'); @@ -108,8 +124,14 @@ public function save(Request $request): Response $question->save(); $this->log->info( - 'Updated questions "{text}": {answer}', - ['text' => $question->text, 'answer' => $question->answer] + 'Saved questions "{text}" ({id}) by {user} ({user_id}): {answer}', + [ + 'text' => $question->text, + 'answer' => $question->answer, + 'id' => $question->id, + 'user' => $question->user->name, + 'user_id' => $question->user->id, + ] ); $this->addNotification('question.edit.success'); diff --git a/src/Controllers/Admin/ScheduleController.php b/src/Controllers/Admin/ScheduleController.php index 940461fd0..530c9f048 100644 --- a/src/Controllers/Admin/ScheduleController.php +++ b/src/Controllers/Admin/ScheduleController.php @@ -569,7 +569,7 @@ protected function eventFromScheduleShift(ScheduleShift $scheduleShift): Event protected function patchSchedule(Schedule $schedule): Schedule { - foreach ($schedule->getRooms() as $room) { + foreach ($schedule->getAllRooms() as $room) { $room->patch('name', Str::substr($room->getName(), 0, 35)); foreach ($room->getEvents() as $event) { diff --git a/src/Controllers/Admin/ShiftTypesController.php b/src/Controllers/Admin/ShiftTypesController.php index 4bc7f46ab..4137d50b9 100644 --- a/src/Controllers/Admin/ShiftTypesController.php +++ b/src/Controllers/Admin/ShiftTypesController.php @@ -69,11 +69,43 @@ public function edit(Request $request): Response public function view(Request $request): Response { $shiftTypeId = (int) $request->getAttribute('shift_type_id'); + /** @var ShiftType $shiftType */ $shiftType = $this->shiftType->findOrFail($shiftTypeId); + $days = $shiftType->shifts() + ->scopes('needsUsers') + ->selectRaw('DATE(start) AS date') + ->orderBy('date') + ->groupBy('date') + ->pluck('date'); + + $day = $request->get('day'); + $day = $days->contains($day) ? $day : $days->first(); + + $shifts = $shiftType->shifts() + ->with([ + 'neededAngelTypes.angelType', + 'schedule', + 'shiftEntries.user.personalData', + 'shiftEntries.user.state', + 'shiftEntries.angelType', + 'shiftType.neededAngelTypes.angelType', + 'location.neededAngelTypes.angelType', + ]) + ->whereDate('start', $day) + ->orderBy('start') + ->get(); + return $this->response->withView( 'admin/shifttypes/view', - ['shifttype' => $shiftType, 'is_view' => true] + [ + 'shifttype' => $shiftType, + 'is_view' => true, + 'shifts_active' => $request->has('shifts') || $request->get('day'), + 'days' => $days, + 'selected_day' => $day, + 'shifts' => $shifts, + ] ); } @@ -136,8 +168,9 @@ public function save(Request $request): Response } $this->log->info( - 'Updated shift type "{name}": {description}, {signup_advance_hours}, {angels}', + 'Saved shift type "{name}" ({id}): {description}, {signup_advance_hours}, {angels}', [ + 'id' => $shiftType->id, 'name' => $shiftType->name, 'description' => $shiftType->description, 'signup_advance_hours' => $shiftType->signup_advance_hours, @@ -165,7 +198,7 @@ public function delete(Request $request): Response } $shiftType->delete(); - $this->log->info('Deleted shift type {name}', ['name' => $shiftType->name]); + $this->log->info('Deleted shift type {name} ({id})', ['name' => $shiftType->name, 'id' => $shiftType->id]); $this->addNotification('shifttype.delete.success'); return $this->redirect->to('/admin/shifttypes'); diff --git a/src/Controllers/Admin/TagController.php b/src/Controllers/Admin/TagController.php new file mode 100644 index 000000000..e1fd690c7 --- /dev/null +++ b/src/Controllers/Admin/TagController.php @@ -0,0 +1,104 @@ + */ + protected array $permissions = [ + 'tag.edit', + ]; + + public function __construct( + protected LoggerInterface $log, + protected Tag $tag, + protected Redirector $redirect, + protected Response $response + ) { + } + + public function list(): Response + { + $items = $this->tag->all(); + return $this->response->withView( + 'pages/tag/index.twig', + ['items' => $items] + ); + } + + public function edit(Request $request): Response + { + $tagId = $request->getAttribute('tag_id'); // optional + $tag = $this->tag->find($tagId); + + return $this->showEdit($tag); + } + + public function save(Request $request): Response + { + $tagId = $request->getAttribute('tag_id'); // optional + + /** @var Tag $tag */ + $tag = $this->tag->findOrNew($tagId); + + if ($request->request->has('delete')) { + return $this->delete($tag); + } + + $data = $this->validate($request, [ + 'name' => 'required|max:255', + 'delete' => 'optional|checked', + ]); + + $tag->name = $data['name']; + + if ( + $this->tag + ->where('name', $tag->name) + ->whereNot('id', $tag->id) + ->exists() + ) { + $this->addNotification('tag.edit.duplicate', NotificationType::ERROR); + + return $this->showEdit($tag); + } + + $tag->save(); + + $this->log->info('Saved tag "{name}" ({id})', ['name' => $tag->name, 'id' => $tag->id]); + $this->addNotification('tag.edit.success'); + + return $this->redirect->to('/admin/tags'); + } + + protected function delete(Tag $tag): Response + { + $tag->delete(); + + $this->log->info('Deleted tag "{name}" ({id})', ['name' => $tag->name, 'id' => $tag->id]); + $this->addNotification('tag.delete.success'); + + return $this->redirect->to('/admin/tags'); + } + + protected function showEdit(?Tag $tag): Response + { + return $this->response->withView( + 'pages/tag/edit.twig', + ['tag' => $tag] + ); + } +} diff --git a/src/Controllers/Admin/UserGoodieController.php b/src/Controllers/Admin/UserGoodieController.php index 38166fb33..67b90bc65 100644 --- a/src/Controllers/Admin/UserGoodieController.php +++ b/src/Controllers/Admin/UserGoodieController.php @@ -4,11 +4,13 @@ namespace Engelsystem\Controllers\Admin; +use Carbon\Carbon; use Engelsystem\Config\Config; use Engelsystem\Config\GoodieType; use Engelsystem\Controllers\BaseController; use Engelsystem\Controllers\HasUserNotifications; use Engelsystem\Helpers\Authenticator; +use Engelsystem\Helpers\Goodie; use Engelsystem\Http\Exceptions\HttpNotFound; use Engelsystem\Http\Redirector; use Engelsystem\Http\Request; @@ -48,13 +50,16 @@ public function editGoodie(Request $request): Response $this->checkActive(); $userId = (int) $request->getAttribute('user_id'); + /** @var User $user */ $user = $this->user->findOrFail($userId); + $goodieScore = $user->state->force_active ? '~' : Goodie::userScore($user); return $this->response->withView( 'admin/user/edit-goodie.twig', [ 'userdata' => $user, 'is_tshirt' => $this->config->get('goodie_type') === GoodieType::Tshirt->value, + 'goodie_score' => $goodieScore, ] ); } @@ -80,7 +85,13 @@ public function saveGoodie(Request $request): Response } if ($this->auth->can('admin_arrive')) { - $user->state->arrived = (bool) $data['arrived']; + if ($user->state->arrived != (bool) $data['arrived']) { + if ((bool) $data['arrived']) { + $user->state->arrival_date = new Carbon(); + } else { + $user->state->arrival_date = null; + } + } } $user->state->active = (bool) $data['active']; @@ -88,7 +99,7 @@ public function saveGoodie(Request $request): Response $user->state->save(); $this->log->info( - 'Updated user goodie state "{user}" ({id}): ' + 'Updated user goodie state {user} ({id}): ' . '{size}, arrived: {arrived}, active: {active}, got goodie: {got_goodie}', [ 'id' => $user->id, diff --git a/src/Controllers/Admin/UserSettingsController.php b/src/Controllers/Admin/UserSettingsController.php index 8de76bd37..6403c4929 100644 --- a/src/Controllers/Admin/UserSettingsController.php +++ b/src/Controllers/Admin/UserSettingsController.php @@ -181,6 +181,8 @@ protected function view(User $user, string $view, array $data = []): Response array_merge([ 'settings_menu' => $this->settingsMenu($user), 'is_admin' => true, + 'ifsg' => $this->isIfsgSupporter() || $this->auth->can('user.ifsg.edit'), + 'driver_license' => $this->isDriverLicenseSupporter() || $this->auth->can('user.drive.edit'), 'admin_user' => $user, ], $data) ); diff --git a/src/Controllers/Admin/UserVoucherController.php b/src/Controllers/Admin/UserVoucherController.php new file mode 100644 index 000000000..9cabb6b27 --- /dev/null +++ b/src/Controllers/Admin/UserVoucherController.php @@ -0,0 +1,107 @@ + */ + protected array $permissions = [ + 'voucher.edit', + ]; + + public function __construct( + protected Authenticator $auth, + protected Config $config, + protected LoggerInterface $log, + protected Worklog $worklog, + protected Redirector $redirect, + protected Response $response, + protected User $user + ) { + } + + private function checkActive(): void + { + if (!config('enable_voucher')) { + throw new HttpNotFound(); + } + } + + public function editVoucher(Request $request): Response + { + $this->checkActive(); + $userId = (int) $request->getAttribute('user_id'); + + /** @var User $user */ + $user = $this->user->findOrFail($userId); + + return $this->response->withView( + 'admin/user/edit-voucher.twig', + [ + 'userdata' => $user, + 'gotVoucher' => $user->state->got_voucher ?? 0, + 'forceActive' => $user->state->force_active && config('enable_force_active'), + 'forceFood' => $user->state->force_food && config('enable_force_food'), + 'eligibleVoucherCount' => UserVouchers::eligibleVoucherCount($user), + ] + ); + } + + public function saveVoucher(Request $request): Response + { + $this->checkActive(); + $userId = (int) $request->getAttribute('user_id'); + /** @var User $user */ + $user = $this->user->findOrFail($userId); + + $data = $this->validate($request, [ + 'got_voucher' => 'int|min:0', + ]); + + $user->state->got_voucher = (int) $data['got_voucher']; + $user->state->save(); + + $this->log->info( + '{name} ({id}) got {got_voucher} vouchers.', + [ + 'name' => $user->name, + 'id' => $user->id, + 'got_voucher' => $user->state->got_voucher, + ] + ); + + if (in_array('application/json', $request->getAcceptableContentTypes())) { + // This was an async request, send a JSON response. + return $this->response + ->withHeader('content-type', 'application/json') + ->withContent(json_encode([ + 'issued' => $user->state->got_voucher, + 'eligible' => $user->state->got_voucher + UserVouchers::eligibleVoucherCount($user), + 'total' => (int) State::query()->sum('got_voucher'), + ])); + } + + $this->addNotification('voucher.save.success'); + + return $this->redirect->to('/users?action=view&user_id=' . $user->id); + // TODO Once User_view.php gets removed, change this to withView + getNotifications + } +} diff --git a/src/Controllers/Admin/UserWorklogController.php b/src/Controllers/Admin/UserWorklogController.php index f66301542..9ae3907b9 100644 --- a/src/Controllers/Admin/UserWorklogController.php +++ b/src/Controllers/Admin/UserWorklogController.php @@ -49,7 +49,14 @@ public function editWorklog(Request $request): Response if ($worklog->user->id != $user->id) { throw new HttpNotFound(); } - return $this->showEditWorklog($user, $worklog->worked_at, $worklog->hours, $worklog->comment, true); + return $this->showEditWorklog( + $user, + $worklog->worked_at, + $worklog->hours, + $worklog->description, + $worklog->night_shift, + true + ); } else { return $this->showEditWorklog($user, Carbon::today()); } @@ -63,7 +70,8 @@ public function saveWorklog(Request $request): Response $data = $this->validate($request, [ 'work_date' => 'required|date:Y-m-d', 'work_hours' => 'float|min:0', - 'comment' => 'required|max:200', + 'description' => 'required|max:200', + 'night_shift' => 'optional|checked', ]); // Search / create worklog @@ -80,17 +88,20 @@ public function saveWorklog(Request $request): Response } $worklog->worked_at = $data['work_date']; $worklog->hours = $data['work_hours']; - $worklog->comment = $data['comment']; + $worklog->description = $data['description']; + $worklog->night_shift = $data['night_shift'] ?: false; $worklog->save(); $this->log->info( - 'Added worklog for {name} ({id}) at {time} spanning {hours}h: {text}', + 'Saved worklog ({wl_id}) for {name} ({id}) at {time} spanning {hours}h{night_shift}: {text}', [ + 'wl_id' => $worklog->id, 'name' => $user->name, 'id' => $user->id, 'time' => $worklog->worked_at, 'hours' => $worklog->hours, - 'text' => $worklog->comment, + 'text' => $worklog->description, + 'night_shift' => $worklog->night_shift ? ' at night' : '', ] ); $this->addNotification(isset($worklogId) ? 'worklog.edit.success' : 'worklog.add.success'); @@ -127,13 +138,15 @@ public function deleteWorklog(Request $request): Response $worklog->delete(); $this->log->info( - 'Deleted worklog for {name} ({id}) at {time} spanning {hours}h: {text}', + 'Deleted worklog ({wl_id}) for {name} ({id}) at {time} spanning {hours}h{night_shift}: {text}', [ + 'wl_id' => $worklog->id, 'name' => $worklog->user->name, 'id' => $worklog->user->id, 'time' => $worklog->worked_at, 'hours' => $worklog->hours, - 'text' => $worklog->comment, + 'text' => $worklog->description, + 'night_shift' => $worklog->night_shift ? ' at night' : '', ] ); $this->addNotification('worklog.delete.success'); @@ -146,7 +159,8 @@ private function showEditWorklog( User $user, Carbon $work_date, float $work_hours = 0, - string $comment = '', + string $description = '', + bool $night_shift = false, bool $is_edit = false ): Response { return $this->response->withView( @@ -155,7 +169,8 @@ private function showEditWorklog( 'userdata' => $user, 'work_date' => $work_date, 'work_hours' => $work_hours, - 'comment' => $comment, + 'description' => $description, + 'night_shift' => $night_shift, 'is_edit' => $is_edit, ] ); diff --git a/src/Controllers/AngelTypesController.php b/src/Controllers/AngelTypesController.php index 24f4e1202..ef4750131 100644 --- a/src/Controllers/AngelTypesController.php +++ b/src/Controllers/AngelTypesController.php @@ -4,13 +4,45 @@ namespace Engelsystem\Controllers; +use Engelsystem\Config\Config; +use Engelsystem\Helpers\Authenticator; +use Engelsystem\Helpers\Carbon; +use Engelsystem\Http\Exceptions\HttpNotFound; +use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Models\AngelType; +use Engelsystem\Models\User\User; +use Engelsystem\Models\UserAngelType; +use Exception; +use Firebase\JWT\BeforeValidException; +use Firebase\JWT\ExpiredException; +use Firebase\JWT\JWT; +use Firebase\JWT\Key; +use Illuminate\Support\Str; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Log\LoggerInterface; class AngelTypesController extends BaseController { - public function __construct(protected Response $response) + use HasUserNotifications; + + public function __construct( + protected Response $response, + protected Config $config, + protected Authenticator $auth, + protected LoggerInterface $log, + ) { + } + + public function hasPermission(ServerRequestInterface $request, string $method): ?bool { + return match ($method) { + 'qrCode' => + $this->auth->user()?->isAngelTypeSupporter($this->getAngelType($request)) + || $this->auth->can('admin_user_angeltypes'), + 'join' => (bool) $this->auth->user(), + default => parent::hasPermission($request, $method), + }; } public function about(): Response @@ -22,4 +54,119 @@ public function about(): Response ['angeltypes' => $angeltypes] ); } + + public function qrCode(Request $request): Response + { + $this->qrJoinEnabled(); + $angelType = $this->getAngelType($request); + $jwtExpirationMin = $this->config->get('jwt_expiration_time'); + $qrData = null; + $data = []; + + if ($request->isMethod('post')) { + $data = $this->validate($request, [ + 'minutes' => 'required|int|min:1|max:' . $jwtExpirationMin, + ]); + $minutes = (int) $data['minutes']; + $time = Carbon::now(); + + $key = $this->config->get('app_key'); + $alg = $this->config->get('jwt_algorithm'); + $jti = Str::random(); + + $iat = $time->timestamp; + $exp = $time->addMinutes($minutes)->timestamp; + $data['expires'] = $time; + + $payload = [ + 'sub' => 'join_angel_type', + 'iat' => $iat, + 'exp' => $exp, + 'jti' => $jti, + 'id' => $angelType->id, + 'by' => $this->auth->user()->id, + ]; + $jwt = JWT::encode($payload, $key, $alg); + $qrData = url('/angeltypes/' . $angelType->id . '/join', ['token' => $jwt]); + } + + return $this->response->withInput($data)->withView( + 'pages/angeltypes/qr', + ['angel_type' => $angelType, 'qr_data' => $qrData, 'qr_max_expiration_minutes' => $jwtExpirationMin], + ); + } + + public function join(Request $request): Response + { + $this->qrJoinEnabled(); + $angelType = $this->getAngelType($request); + + $jwt = $request->get('token', ''); + + $key = $this->config->get('app_key'); + $alg = $this->config->get('jwt_algorithm'); + + try { + $decoded = JWT::decode($jwt, new Key($key, $alg)); + } catch (BeforeValidException | ExpiredException) { + throw new HttpNotFound(); + } catch (Exception $e) { + $this->log->error('JWT Error', ['exception' => $e]); + throw new HttpNotFound(); + } + + $type = $decoded->sub ?? null; + $id = $decoded->id ?? null; + $jti = $decoded->jti ?? null; + if ($type !== 'join_angel_type' || $id !== $angelType->id) { + throw new HttpNotFound(); + } + + /** @var User $confirmingUser */ + $confirmingUser = User::findOrFail($decoded->by ?? null); + /** @var UserAngelType $userAngelType */ + $userAngelType = UserAngelType::firstOrNew([ + 'user_id' => $this->auth->user()->id, + 'angel_type_id' => $angelType->id, + ]); + + if (!$userAngelType->confirmUser) { + $userAngelType->confirmUser()->associate($confirmingUser); + + $this->log->info( + 'Joined angel type {type} ({type_id}) via QR token {token_id} ' + . 'created by {confirming_user} ({confirming_id})', + [ + 'type' => $angelType->name, + 'type_id' => $angelType->id, + 'token_id' => $jti, + 'confirming_user' => $confirmingUser->name, + 'confirming_id' => $confirmingUser->id, + ] + ); + + $userAngelType->save(); + } + + $this->addNotification('angeltype.add.success'); + + return redirect(url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angelType->id])); + } + + protected function qrJoinEnabled(): void + { + if ($this->config->get('app_key') && $this->config->get('join_qr_code', true)) { + return; + } + + throw new HttpNotFound(); + } + + protected function getAngelType(ServerRequestInterface $request): AngelType + { + $angelTypeId = (int) $request->getAttribute('angel_type_id'); + /** @var AngelType $angelType */ + $angelType = AngelType::findOrFail($angelTypeId); + return $angelType; + } } diff --git a/src/Controllers/Api/Resources/AngelTypeResource.php b/src/Controllers/Api/Resources/AngelTypeResource.php index bcfd81f90..1027d1fea 100644 --- a/src/Controllers/Api/Resources/AngelTypeResource.php +++ b/src/Controllers/Api/Resources/AngelTypeResource.php @@ -6,11 +6,12 @@ use Engelsystem\Models\AngelType; use Engelsystem\Models\BaseModel; +use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Support\Collection; class AngelTypeResource extends BasicResource { - protected Collection | BaseModel | AngelType $model; + protected Collection | BaseModel | Pivot | AngelType $model; public function toArray(): array { diff --git a/src/Controllers/Api/Resources/BasicResource.php b/src/Controllers/Api/Resources/BasicResource.php index de0a43d80..0e48e1ac6 100644 --- a/src/Controllers/Api/Resources/BasicResource.php +++ b/src/Controllers/Api/Resources/BasicResource.php @@ -7,13 +7,14 @@ use Engelsystem\Models\BaseModel; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Jsonable; +use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Support\Collection; use Stringable; /** @phpstan-consistent-constructor */ abstract class BasicResource implements Arrayable, Jsonable, Stringable { - public function __construct(protected BaseModel | Collection $model) + public function __construct(protected Collection | BaseModel | Pivot $model) { } diff --git a/src/Controllers/Api/Resources/LocationResource.php b/src/Controllers/Api/Resources/LocationResource.php index cf1028c85..74585cb58 100644 --- a/src/Controllers/Api/Resources/LocationResource.php +++ b/src/Controllers/Api/Resources/LocationResource.php @@ -6,11 +6,12 @@ use Engelsystem\Models\BaseModel; use Engelsystem\Models\Location; +use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Support\Collection; class LocationResource extends BasicResource { - protected Collection | BaseModel | Location $model; + protected Collection | BaseModel | Pivot | Location $model; public function toArray(): array { diff --git a/src/Controllers/Api/Resources/NewsResource.php b/src/Controllers/Api/Resources/NewsResource.php index 63a31f170..dbd042102 100644 --- a/src/Controllers/Api/Resources/NewsResource.php +++ b/src/Controllers/Api/Resources/NewsResource.php @@ -6,11 +6,12 @@ use Engelsystem\Models\BaseModel; use Engelsystem\Models\News; +use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Support\Collection; class NewsResource extends BasicResource { - protected Collection | BaseModel | News $model; + protected Collection | BaseModel | Pivot | News $model; public function toArray(): array { diff --git a/src/Controllers/Api/Resources/ShiftResource.php b/src/Controllers/Api/Resources/ShiftResource.php index 4d8aafb97..be1f752c5 100644 --- a/src/Controllers/Api/Resources/ShiftResource.php +++ b/src/Controllers/Api/Resources/ShiftResource.php @@ -7,11 +7,12 @@ use Engelsystem\Models\BaseModel; use Engelsystem\Models\Shifts\Shift; use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Support\Collection; class ShiftResource extends BasicResource { - protected Collection | BaseModel | Shift $model; + protected Collection | BaseModel | Pivot | Shift $model; public function toArray(array | Arrayable $location = []): array { @@ -23,6 +24,7 @@ public function toArray(array | Arrayable $location = []): array 'ends_at' => $this->model->end, 'location' => LocationResource::toIdentifierArray($location), 'shift_type' => ShiftTypeResource::toIdentifierArray($this->model->shiftType), + 'schedule_guid' => $this->model->scheduleShift?->guid, 'created_at' => $this->model->created_at, 'updated_at' => $this->model->updated_at, 'url' => url('/shifts', ['action' => 'view', 'shift_id' => $this->model->id]), diff --git a/src/Controllers/Api/Resources/ShiftTypeResource.php b/src/Controllers/Api/Resources/ShiftTypeResource.php index cd1b26ed0..a23406067 100644 --- a/src/Controllers/Api/Resources/ShiftTypeResource.php +++ b/src/Controllers/Api/Resources/ShiftTypeResource.php @@ -6,11 +6,12 @@ use Engelsystem\Models\BaseModel; use Engelsystem\Models\Shifts\ShiftType; +use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Support\Collection; class ShiftTypeResource extends BasicResource { - protected Collection | BaseModel | ShiftType $model; + protected Collection | BaseModel | Pivot | ShiftType $model; public function toArray(): array { diff --git a/src/Controllers/Api/Resources/UserAngelTypeReferenceResource.php b/src/Controllers/Api/Resources/UserAngelTypeReferenceResource.php new file mode 100644 index 000000000..54ece78cb --- /dev/null +++ b/src/Controllers/Api/Resources/UserAngelTypeReferenceResource.php @@ -0,0 +1,30 @@ +model->pivotRelated; + /** @var AngelType $angelType */ + $angelType = $this->model->pivotParent; + /** @var UserAngelType $userAngelType */ + $userAngelType = $this->model; + + return [ + 'user' => UserResource::toIdentifierArray($user), + 'confirmed' => !$angelType->restricted + || $userAngelType->supporter + || $userAngelType->confirm_user_id, + 'supporter' => $userAngelType->supporter, + ]; + } +} diff --git a/src/Controllers/Api/Resources/UserAngelTypeResource.php b/src/Controllers/Api/Resources/UserAngelTypeResource.php index 70da33322..d050a80b6 100644 --- a/src/Controllers/Api/Resources/UserAngelTypeResource.php +++ b/src/Controllers/Api/Resources/UserAngelTypeResource.php @@ -4,16 +4,23 @@ namespace Engelsystem\Controllers\Api\Resources; +use Engelsystem\Models\AngelType; + class UserAngelTypeResource extends AngelTypeResource { public function toArray(): array { + /** @var AngelType $angelType */ + $angelType = $this->model; + /** @var AngelType $angelType */ + $userAngelType = $this->model->pivot; + return [ - ...parent::toArray(), - 'confirmed' => !$this->model->restricted - || $this->model->pivot->supporter - || $this->model->pivot->confirm_user_id, - 'supporter' => $this->model->pivot->supporter, + 'angeltype' => AngelTypeResource::toIdentifierArray($angelType), + 'confirmed' => !$angelType->restricted + || $userAngelType->supporter + || $userAngelType->confirm_user_id, + 'supporter' => $userAngelType->supporter, ]; } } diff --git a/src/Controllers/Api/Resources/UserResource.php b/src/Controllers/Api/Resources/UserResource.php index 2793cc8c9..02e06ccc4 100644 --- a/src/Controllers/Api/Resources/UserResource.php +++ b/src/Controllers/Api/Resources/UserResource.php @@ -6,11 +6,12 @@ use Engelsystem\Models\BaseModel; use Engelsystem\Models\User\User; +use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Support\Collection; class UserResource extends BasicResource { - protected Collection | BaseModel | User $model; + protected Collection | BaseModel | Pivot | User $model; public function toArray(): array { diff --git a/src/Controllers/Api/Resources/WorklogResource.php b/src/Controllers/Api/Resources/WorklogResource.php new file mode 100644 index 000000000..ce02eaec0 --- /dev/null +++ b/src/Controllers/Api/Resources/WorklogResource.php @@ -0,0 +1,28 @@ + $this->model->id, + 'description' => $this->model->description, + 'hours' => $this->model->hours, + 'created_by' => UserResource::toIdentifierArray($this->model->creator), + 'worked_at' => $this->model->worked_at, + 'created_at' => $this->model->created_at, + 'updated_at' => $this->model->updated_at, + ]; + } +} diff --git a/src/Controllers/Api/ShiftsController.php b/src/Controllers/Api/ShiftsController.php index dbb69697b..5e294716b 100644 --- a/src/Controllers/Api/ShiftsController.php +++ b/src/Controllers/Api/ShiftsController.php @@ -158,6 +158,7 @@ protected function shiftEntriesResponse(BuilderContract $shifts): Response 'shiftEntries.user.contact', 'shiftEntries.user.personalData', 'shiftType', + 'scheduleShift', 'schedule.shiftType.neededAngelTypes.angelType', ]) ->orderBy('start') diff --git a/src/Controllers/Api/UsersController.php b/src/Controllers/Api/UsersController.php index 5173a1280..0508efd8e 100644 --- a/src/Controllers/Api/UsersController.php +++ b/src/Controllers/Api/UsersController.php @@ -4,15 +4,37 @@ namespace Engelsystem\Controllers\Api; +use Engelsystem\Controllers\Api\Resources\UserAngelTypeReferenceResource; use Engelsystem\Controllers\Api\Resources\UserDetailResource; use Engelsystem\Controllers\Api\Resources\UserResource; +use Engelsystem\Controllers\Api\Resources\WorklogResource; use Engelsystem\Http\Request; use Engelsystem\Http\Response; +use Engelsystem\Models\AngelType; +use Engelsystem\Models\BaseModel; +use Engelsystem\Models\User\User; +use Engelsystem\Models\UserAngelType; +use Illuminate\Database\Eloquent\Collection; class UsersController extends ApiController { use UsesAuth; + public function index(): Response + { + $models = User::query() + ->orderBy('name') + ->get(); + + $models = $models->map(function (BaseModel $model) { + return UserResource::toIdentifierArray($model); + }); + + $data = ['data' => $models]; + return $this->response + ->withContent(json_encode($data)); + } + public function user(Request $request): Response { $id = $request->getAttribute('user_id'); @@ -23,4 +45,49 @@ public function user(Request $request): Response return $this->response ->withContent(json_encode($data)); } + + public function entriesByAngeltype(Request $request): Response + { + $id = (int) $request->getAttribute('angeltype_id'); + /** @var AngelType $angelType */ + $angelType = AngelType::findOrFail($id); + + /** @var User[]|Collection $models */ + $models = $angelType->userAngelTypes() + ->orderBy('name') + ->get(); + + /** @var UserAngelType[]|Collection $models */ + $models = $models->map(function (User $model) { + // Patch to use the existing user model instead of a partially populated one + $model->pivot->setRelatedModel($model); + return $model->pivot; + }); + + /** @var UserAngelTypeReferenceResource[]|Collection $models */ + $models = UserAngelTypeReferenceResource::collection($models); + + $data = ['data' => $models]; + return $this->response + ->withContent(json_encode($data)); + } + + public function worklogs(Request $request): Response + { + $id = (int) $request->getAttribute('user_id'); + /** @var User $user */ + $user = User::findOrFail($id); + + $models = $user->worklogs(); + + $models = $models + ->orderBy('worked_at') + ->get(); + + $models = WorklogResource::collection($models); + + $data = ['data' => $models]; + return $this->response + ->withContent(json_encode($data)); + } } diff --git a/src/Controllers/BaseController.php b/src/Controllers/BaseController.php index 7575bf805..03e994036 100644 --- a/src/Controllers/BaseController.php +++ b/src/Controllers/BaseController.php @@ -5,6 +5,7 @@ namespace Engelsystem\Controllers; use Engelsystem\Http\Validation\ValidatesRequest; +use Psr\Http\Message\ServerRequestInterface; abstract class BaseController { @@ -14,7 +15,7 @@ abstract class BaseController protected array $permissions = []; /** - * Returns the list of permissions + * Returns the list of permissions for instance / methods * * @return string[]|string[][] */ @@ -22,4 +23,14 @@ public function getPermissions(): array { return $this->permissions; } + + /** + * Check if the request should be permitted + * + * $this->getPermissions will be interpreted on null return + */ + public function hasPermission(ServerRequestInterface $request, string $method): ?bool + { + return null; + } } diff --git a/src/Controllers/HasUserNotifications.php b/src/Controllers/HasUserNotifications.php index 5789158fc..d3c60c8cc 100644 --- a/src/Controllers/HasUserNotifications.php +++ b/src/Controllers/HasUserNotifications.php @@ -21,7 +21,7 @@ protected function addNotification(string|array $value, NotificationType $type = * @param NotificationType[]|null $types * @return array> */ - protected function getNotifications(array $types = null): array + protected function getNotifications(?array $types = null): array { $return = []; $types = $types ?: [ diff --git a/src/Controllers/Metrics/Controller.php b/src/Controllers/Metrics/Controller.php index d8883129f..d0d015b40 100644 --- a/src/Controllers/Metrics/Controller.php +++ b/src/Controllers/Metrics/Controller.php @@ -104,6 +104,7 @@ public function metrics(): Response ], 'users_info' => ['type' => 'gauge', $this->stats->usersInfo()], 'users_force_active' => ['type' => 'gauge', $this->stats->forceActiveUsers()], + 'users_force_food' => ['type' => 'gauge', $this->stats->forceFoodUsers()], 'users_pronouns' => ['type' => 'gauge', $this->stats->usersPronouns()], 'licenses' => [ 'type' => 'gauge', @@ -234,41 +235,17 @@ public function metrics(): Response ->withContent($this->engine->get('/metrics', $data)); } - public function stats(): Response - { - $this->checkAuth(true); - - $data = [ - 'user_count' => $this->stats->usersState() + $this->stats->usersState(null, false), - 'arrived_user_count' => $this->stats->usersState(), - 'done_work_hours' => round($this->stats->workSeconds(true) / 60 / 60), - 'users_in_action' => $this->stats->currentlyWorkingUsers(), - ]; - - return $this->response - ->withHeader('Content-Type', 'application/json') - ->withContent(json_encode($data)); - } - /** * Ensure that the request is authorized */ - protected function checkAuth(bool $isJson = false): void + protected function checkAuth(): void { $apiKey = $this->config->get('api_key'); if (empty($apiKey) || $this->request->get('api_key') == $apiKey) { return; } - $message = 'The api_key is invalid'; - $headers = []; - - if ($isJson) { - $message = json_encode(['error' => $message]); - $headers['Content-Type'] = 'application/json'; - } - - throw new HttpForbidden($message, $headers); + throw new HttpForbidden('The api_key is invalid'); } /** diff --git a/src/Controllers/Metrics/Stats.php b/src/Controllers/Metrics/Stats.php index 14dad0f3d..f49e5d814 100644 --- a/src/Controllers/Metrics/Stats.php +++ b/src/Controllers/Metrics/Stats.php @@ -41,9 +41,8 @@ public function __construct(protected Database $db) /** * The number of users that arrived/not arrived and/or did some work * - * @param bool|null $working */ - public function usersState(bool $working = null, bool $arrived = true): int + public function usersState(?bool $working = null, bool $arrived = true): int { $query = State::whereArrived($arrived); @@ -85,6 +84,11 @@ public function forceActiveUsers(): int return State::whereForceActive(true)->count(); } + public function forceFoodUsers(): int + { + return State::whereForceFood(true)->count(); + } + public function usersPronouns(): int { return PersonalData::query()->where('pronoun', '!=', '')->count(); @@ -104,9 +108,8 @@ public function email(string $type): int /** * The number of currently working users * - * @param bool|null $freeloaded */ - public function currentlyWorkingUsers(bool $freeloaded = null): int + public function currentlyWorkingUsers(?bool $freeloaded = null): int { $query = User::query() ->join('shift_entries', 'shift_entries.user_id', '=', 'users.id') @@ -203,12 +206,10 @@ public function licenses(string $license, bool $confirmed = false): int } /** - * @param bool|null $done - * @param bool|null $freeloaded * * @codeCoverageIgnore because it is only used in functions that use TIMESTAMPDIFF */ - protected function workSecondsQuery(bool $done = null, bool $freeloaded = null): QueryBuilder + protected function workSecondsQuery(?bool $done = null, ?bool $freeloaded = null): QueryBuilder { $query = $this ->getQuery('shift_entries') @@ -216,8 +217,8 @@ protected function workSecondsQuery(bool $done = null, bool $freeloaded = null): if (!is_null($freeloaded)) { $freeloaded - ? $query->whereNull('freeloaded_by') - : $query->whereNotNull('freeloaded_by'); + ? $query->whereNotNull('freeloaded_by') + : $query->whereNull('freeloaded_by'); } if (!is_null($done)) { @@ -230,12 +231,10 @@ protected function workSecondsQuery(bool $done = null, bool $freeloaded = null): /** * The amount of worked seconds * - * @param bool|null $done - * @param bool|null $freeloaded * * @codeCoverageIgnore as TIMESTAMPDIFF is not implemented in SQLite */ - public function workSeconds(bool $done = null, bool $freeloaded = null): int + public function workSeconds(?bool $done = null, ?bool $freeloaded = null): int { $query = $this->workSecondsQuery($done, $freeloaded); @@ -245,12 +244,10 @@ public function workSeconds(bool $done = null, bool $freeloaded = null): int /** * The number of worked shifts * - * @param bool|null $done - * @param bool|null $freeloaded * * @codeCoverageIgnore as TIMESTAMPDIFF is not implemented in SQLite */ - public function workBuckets(array $buckets, bool $done = null, bool $freeloaded = null): array + public function workBuckets(array $buckets, ?bool $done = null, ?bool $freeloaded = null): array { return $this->getBuckets( $buckets, @@ -352,10 +349,7 @@ public function shifts(): int return Shift::query()->count(); } - /** - * @param bool|null $meeting - */ - public function announcements(bool $meeting = null): int + public function announcements(?bool $meeting = null): int { $query = is_null($meeting) ? News::query() : News::whereIsMeeting($meeting); @@ -368,10 +362,7 @@ public function comments(): int ->count(); } - /** - * @param bool|null $answered - */ - public function questions(bool $answered = null): int + public function questions(?bool $answered = null): int { $query = Question::query(); if (!is_null($answered)) { @@ -433,10 +424,7 @@ public function databaseWrite(): float return microtime(true) - $start; } - /** - * @param string|null $level - */ - public function logEntries(string $level = null): int + public function logEntries(?string $level = null): int { $query = is_null($level) ? LogEntry::query() : LogEntry::whereLevel($level); diff --git a/src/Controllers/OAuthController.php b/src/Controllers/OAuthController.php index 3eee8277a..37cb3c9de 100644 --- a/src/Controllers/OAuthController.php +++ b/src/Controllers/OAuthController.php @@ -13,6 +13,7 @@ use Engelsystem\Http\Response; use Engelsystem\Http\UrlGenerator; use Engelsystem\Models\OAuth; +use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use League\OAuth2\Client\Provider\AbstractProvider; @@ -140,9 +141,20 @@ public function index(Request $request): Response 'refresh_token' => $accessToken->getRefreshToken(), 'expires_at' => $expirationTime, ]); - $oauth->user() - ->associate($user) - ->save(); + + try { + $oauth->user() + ->associate($user) + ->save(); + // @codeCoverageIgnoreStart + } catch (UniqueConstraintViolationException) { + $this->log->error( + 'Duplicate OAuth user {user} using {provider}: Database does not support unique with mixed case! ', + ['provider' => $providerName, 'user' => $resourceId] + ); + throw new HttpNotFound('oauth.provider-error'); + // @codeCoverageIgnoreEnd + } $this->log->info( 'Connected OAuth user {user} using {provider}', @@ -194,6 +206,12 @@ public function disconnect(Request $request): Response { $providerName = $request->getAttribute('provider'); + $this->requireProvider($providerName); + + if (!($this->config->get('oauth')[$providerName]['allow_user_disconnect'] ?? true)) { + throw new HttpNotFound(); + } + $this->oauth ->whereUserId($this->auth->user()->id) ->where('provider', $providerName) @@ -260,7 +278,6 @@ protected function handleArrive( return; } - $userState->arrived = true; $userState->arrival_date = new Carbon(); $userState->save(); diff --git a/src/Controllers/PasswordResetController.php b/src/Controllers/PasswordResetController.php index ec601acce..7d081ec91 100644 --- a/src/Controllers/PasswordResetController.php +++ b/src/Controllers/PasswordResetController.php @@ -83,7 +83,7 @@ public function postResetPassword(Request $request): Response $reset = $this->requireToken($request); $data = $this->validate($request, [ - 'password' => 'required|min:' . config('password_min_length'), + 'password' => 'required|length:' . config('password_min_length'), 'password_confirmation' => 'required', ]); diff --git a/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php index 4d6262db0..750ad312b 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -112,8 +112,8 @@ public function saveProfile(Request $request): Response if ( $goodie_tshirt - && (isset(config('tshirt_sizes')[$data['shirt_size'] ?? '']) || is_null($data['shirt_size'])) && !$user->state->got_goodie + && (isset(config('tshirt_sizes')[$data['shirt_size'] ?? '']) || is_null($data['shirt_size'])) ) { $user->personalData->shirt_size = $data['shirt_size']; } @@ -146,7 +146,7 @@ public function savePassword(Request $request): Response $minLength = config('password_min_length'); $data = $this->validate($request, [ 'password' => empty($user->password) ? 'optional' : 'required', - 'new_password' => 'required|min:' . $minLength, + 'new_password' => 'required|length:' . $minLength, 'new_password2' => 'required', ]); @@ -423,8 +423,9 @@ public function settingsMenu(): array protected function checkOauthHidden(): bool { - foreach (config('oauth') as $config) { - if (empty($config['hidden'])) { + $userServices = $this->auth->user()->oauth; + foreach (config('oauth') as $name => $config) { + if (empty($config['hidden']) || $userServices->contains('provider', $name)) { return false; } } diff --git a/src/Database/DatabaseServiceProvider.php b/src/Database/DatabaseServiceProvider.php index b1b6713bb..c5d60adf9 100644 --- a/src/Database/DatabaseServiceProvider.php +++ b/src/Database/DatabaseServiceProvider.php @@ -5,10 +5,12 @@ namespace Engelsystem\Database; use Carbon\Carbon; +use Engelsystem\Config\Config; use Engelsystem\Container\ServiceProvider; use Exception; use Illuminate\Database\Capsule\Manager as CapsuleManager; use Illuminate\Database\Connection as DatabaseConnection; +use PDO; use PDOException; use Throwable; @@ -16,7 +18,9 @@ class DatabaseServiceProvider extends ServiceProvider { public function register(): void { + /** @var Config $config */ $config = $this->app->get('config'); + /** @var CapsuleManager $capsule */ $capsule = $this->app->make(CapsuleManager::class); $now = Carbon::now($config->get('timezone')); @@ -44,6 +48,7 @@ public function register(): void $this->exitOnError($e); } + $this->app->instance(PDO::class, $pdo); $this->app->instance(CapsuleManager::class, $capsule); $this->app->instance(Db::class, $capsule); Db::setDbManager($capsule); diff --git a/src/Events/Listener/OAuth2.php b/src/Events/Listener/OAuth2.php index 9f145e596..b44f98cf8 100644 --- a/src/Events/Listener/OAuth2.php +++ b/src/Events/Listener/OAuth2.php @@ -68,7 +68,7 @@ protected function syncTeams(string $providerName, User $user, array $ssoTeam): if (!$userAngeltype) { $this->log->info( - 'SSO {provider}: Added to angeltype {angeltype}, confirmed: {confirmed}, supporter: {supporter}', + 'SSO {provider}: Added to angel type {angeltype}, confirmed: {confirmed}, supporter: {supporter}', [ 'provider' => $providerName, 'angeltype' => $angelType->name, diff --git a/src/Events/Listener/Shifts.php b/src/Events/Listener/Shifts.php index c9a8a40f3..bc7e628fd 100644 --- a/src/Events/Listener/Shifts.php +++ b/src/Events/Listener/Shifts.php @@ -35,7 +35,7 @@ public function deletingCreateWorklogs(Shift $shift): void $worklog->hours = (($shift->end->timestamp - $shift->start->timestamp) / 60 / 60) * $shift->getNightShiftMultiplier(); - $worklog->comment = Str::substr(sprintf( + $worklog->description = Str::substr(sprintf( __('%s (%s as %s) in %s, %s - %s'), $shift->shiftType->name, $shift->title, @@ -48,7 +48,7 @@ public function deletingCreateWorklogs(Shift $shift): void $this->log->info( 'Created worklog entry from shift for {user} ({uid}): {worklog})', - ['user' => $worklog->user->name, 'uid' => $worklog->user->id, 'worklog' => $worklog->comment] + ['user' => $worklog->user->name, 'uid' => $worklog->user->id, 'worklog' => $worklog->description] ); } } diff --git a/src/Exceptions/Handler.php b/src/Exceptions/Handler.php index 4fc992fa1..5a4dea963 100644 --- a/src/Exceptions/Handler.php +++ b/src/Exceptions/Handler.php @@ -41,13 +41,21 @@ public function register(): void set_exception_handler([$this, 'exceptionHandler']); } - public function errorHandler(int $number, string $message, string $file, int $line): void + public function errorHandler(int $errorNumber, string $message, string $file, int $line): bool { - $exception = new ErrorException($message, 0, $number, $file, $line); - $this->exceptionHandler($exception); + $handleLevel = $this->environment == Environment::DEVELOPMENT + ? E_ALL + : E_RECOVERABLE_ERROR | E_COMPILE_ERROR; + // If error level should be intercepted or ignored (like warnings in prod) + $shouldHandle = (bool) ($errorNumber & $handleLevel); + + $exception = new ErrorException($message, 0, $errorNumber, $file, $line); + $this->exceptionHandler($exception, $shouldHandle); + + return $shouldHandle; } - public function exceptionHandler(Throwable $e, bool $return = false): string + public function exceptionHandler(Throwable $e, bool $showError = true): string { if (!$this->request instanceof Request) { $this->request = new Request(); @@ -58,7 +66,7 @@ public function exceptionHandler(Throwable $e, bool $return = false): string ob_start(); $handler->render($this->request, $e); - if ($return) { + if (!$showError) { $output = ob_get_contents(); ob_end_clean(); return $output; @@ -101,7 +109,7 @@ public function setEnvironment(Environment $environment): void /** * @return HandlerInterface|HandlerInterface[] */ - public function getHandler(Environment $environment = null): HandlerInterface|array + public function getHandler(?Environment $environment = null): HandlerInterface|array { if (!is_null($environment)) { return $this->handler[$environment->value]; diff --git a/src/Exceptions/Handlers/Legacy.php b/src/Exceptions/Handlers/Legacy.php index c121872da..5788c36b7 100644 --- a/src/Exceptions/Handlers/Legacy.php +++ b/src/Exceptions/Handlers/Legacy.php @@ -5,7 +5,9 @@ namespace Engelsystem\Exceptions\Handlers; use Engelsystem\Http\Request; +use ErrorException; use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; use Throwable; class Legacy implements HandlerInterface @@ -25,7 +27,8 @@ public function report(Throwable $e): void { $previous = $e->getPrevious(); error_log(sprintf( - 'Exception: Code: %s, Message: %s, File: %s:%u, Previous: %s, Trace: %s', + '%s: Code: %s, Message: %s, File: %s:%u, Previous: %s, Trace: %s', + get_class($e), $e->getCode(), $e->getMessage(), $this->stripBasePath($e->getFile()), @@ -38,8 +41,14 @@ public function report(Throwable $e): void return; } + $errorLevels = E_ERROR | E_RECOVERABLE_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR; + $logAsError = !$e instanceof ErrorException || $e->getSeverity() & $errorLevels; try { - $this->log->critical('', ['exception' => $e]); + $this->log->log( + $logAsError ? LogLevel::CRITICAL : LogLevel::WARNING, + '', + ['exception' => $e], + ); } catch (Throwable) { } } diff --git a/src/Factories/User.php b/src/Factories/User.php index f262ed19c..33ca9c0eb 100644 --- a/src/Factories/User.php +++ b/src/Factories/User.php @@ -275,7 +275,6 @@ private function createUser(array $data, array $rawData): EngelsystemUser $state = new State([]); if ($this->config->get('autoarrive')) { - $state->arrived = true; $state->arrival_date = CarbonImmutable::now(); } @@ -301,6 +300,15 @@ private function createUser(array $data, array $rawData): EngelsystemUser $this->session->remove('oauth2_access_token'); $this->session->remove('oauth2_refresh_token'); $this->session->remove('oauth2_expires_at'); + + $this->logger->info( + '{user} connected OAuth user {oauth_user} using {provider}', + [ + 'provider' => $oauth->provider, + 'user' => sprintf('%s (%u)', $user->displayName, $user->id), + 'oauth_user' => $oauth->identifier, + ] + ); } $defaultGroup = Group::find($this->authenticator->getDefaultRole()); diff --git a/src/Helpers/Cache.php b/src/Helpers/Cache.php new file mode 100644 index 000000000..f3052d42e --- /dev/null +++ b/src/Helpers/Cache.php @@ -0,0 +1,57 @@ +cacheFilePath($key); + + // Check for file existence, forget old ones + $exists = file_exists($cacheFile); + if ($exists && filemtime($cacheFile) < time() - $seconds) { + $this->forget($key); + $exists = false; + } + + // Handle callback to get default value + if (!$exists) { + if (!is_callable($default)) { + return $default; + } + + file_put_contents($cacheFile, serialize($default())); + } + + // Get data from cache + return unserialize(file_get_contents($cacheFile)); + } + + public function forget(string $key): void + { + $cacheFile = $this->cacheFilePath($key); + if (!file_exists($cacheFile)) { + return; + } + + unlink($cacheFile); + } + + protected function cacheFilePath(string $key): string + { + return $this->path . '/' . $key . '.cache'; + } +} diff --git a/src/Helpers/CacheServiceProvider.php b/src/Helpers/CacheServiceProvider.php new file mode 100644 index 000000000..e03eaba8c --- /dev/null +++ b/src/Helpers/CacheServiceProvider.php @@ -0,0 +1,18 @@ +app->bind('cache', Cache::class); + $this->app->when(Cache::class) + ->needs('$path') + ->give(fn() => $this->app->get('path.cache')); + } +} diff --git a/src/Helpers/Carbon.php b/src/Helpers/Carbon.php index b559c926e..be344dba7 100644 --- a/src/Helpers/Carbon.php +++ b/src/Helpers/Carbon.php @@ -31,16 +31,4 @@ public static function createFromDatetime(string $value): ?\Carbon\Carbon return null; } - - /** - * Parses HTML datetime-local and ISO date/time strings. - * - * @return int|null Timestamp if parseable, else null - * @see self::DATETIME_FORMATS - */ - public static function createTimestampFromDatetime(string $value): ?int - { - $carbon = self::createFromDateTime($value); - return $carbon?->timestamp; - } } diff --git a/src/Helpers/DayOfEvent.php b/src/Helpers/DayOfEvent.php index a997fd224..596ff256c 100644 --- a/src/Helpers/DayOfEvent.php +++ b/src/Helpers/DayOfEvent.php @@ -11,7 +11,7 @@ class DayOfEvent * If `event_has_day0` is set to true in config, the first day of the event will be 0, else 1. * Returns null if "event_start" is not set. */ - public static function get(Carbon $date = null): int | null + public static function get(?Carbon $date = null): int | null { if (!config('enable_day_of_event')) { return null; diff --git a/src/Helpers/Goodie.php b/src/Helpers/Goodie.php new file mode 100644 index 000000000..c776a3364 --- /dev/null +++ b/src/Helpers/Goodie.php @@ -0,0 +1,149 @@ +getConnection(); + + if (!$connection->getQueryGrammar() instanceof MySqlGrammar) { + return $connection->raw('0'); + } + + // @codeCoverageIgnoreStart + // as sqlite does not support TIMESTAMPDIFF + + if (!$nightShifts['enabled']) { + return $connection->raw( + /** @lang MySQL */ + 'COALESCE(SUM(TIMESTAMPDIFF(MINUTE, shifts.start, shifts.end) * 60), 0)' + ); + } + + /* @see \Engelsystem\Models\Shifts\Shift::isNightShift to keep them in sync */ + $query = + /** @lang MySQL */ + ' + COALESCE(SUM( + /* Shift length */ + TIMESTAMPDIFF(MINUTE, shifts.start, shifts.end) * 60 + /* Is night shift */ + * ( + CASE WHEN + /* Starts during night */ + HOUR(shifts.start) >= %1$d AND HOUR(shifts.start) < %2$d + /* Ends during night */ + OR ( + HOUR(shifts.end) > %1$d + || HOUR(shifts.end) = %1$d AND MINUTE(shifts.end) > 0 + ) AND HOUR(shifts.end) <= %2$d + /* Starts before and ends after night */ + OR HOUR(shifts.start) <= %1$d AND HOUR(shifts.end) >= %2$d + /* Use multiplier */ + THEN + /* Handle freeloading */ + CASE WHEN `shift_entries`.`freeloaded_by` IS NULL + THEN %3$d + ELSE -%3$d + END + ELSE + /* Handle freeloading */ + CASE WHEN `shift_entries`.`freeloaded_by` IS NULL + THEN 1 + ELSE -2 + END + END + ) + ), 0) + '; + + $query = sprintf($query, $nightShifts['start'], $nightShifts['end'], $nightShifts['multiplier']); + + return $connection->raw($query); + // @codeCoverageIgnoreEnd + } + + public static function worklogScoreQuery(): Expression + { + $nightShifts = config('night_shifts'); + + /** @var Database $db */ + $db = app(Database::class); + $connection = $db->getConnection(); + + if (!$nightShifts['enabled']) { + return $connection->raw( + /** @lang MySQL */ + 'COALESCE(SUM(`hours`), 0)' + ); + } + + return $connection->raw(sprintf( + /** @lang MySQL */ + 'COALESCE(SUM(IF(`night_shift`, `hours` * %d, `hours`)), 0)', + $nightShifts['multiplier'] + )); + } + + /** + * Returns the goodie score (number of hours counted for goodie score) + * Includes only ended shifts + */ + public static function userScore(User $user): float + { + /** @var Database $db */ + $db = app(Database::class); + $con = $db->getConnection(); + + $state = $con + ->query() + ->from('users') + ->selectRaw(sprintf( + /** @lang MySQL */ + 'ROUND((%s) / 3600, 2) AS `goodie_score`', + self::shiftScoreQuery()->getValue($con->getQueryGrammar()) + )) + ->where('users.id', $user->id) + ->join('shift_entries', 'users.id', 'shift_entries.user_id') + ->join('shifts', 'shift_entries.shift_id', 'shifts.id') + ->where('shifts.end', '<', Carbon::now()) + ->groupBy('users.id') + ->first(); + + $shiftHours = 0; + if ($state) { + // @codeCoverageIgnoreStart + $shiftHours = (float) $state->goodie_score; + // @codeCoverageIgnoreEnd + } + + $worklogHours = $user->worklogs() + ->where('worked_at', '<=', Carbon::Now()) + ->selectRaw(sprintf( + /** @lang MySQL */ + '%s as `total_hours`', + self::worklogScoreQuery()->getValue($con->getQueryGrammar()) + )) + ->value('total_hours'); + + return $shiftHours + $worklogHours; + } +} diff --git a/src/Helpers/Markdown.php b/src/Helpers/Markdown.php new file mode 100644 index 000000000..f00b92130 --- /dev/null +++ b/src/Helpers/Markdown.php @@ -0,0 +1,53 @@ +getRenderer($allowHtml); + $content = $renderer->convert($text) + ->getContent(); + return rtrim($content, PHP_EOL); + } + + protected function getRenderer(bool $allowHtml): MarkdownConverter + { + $config = [ + 'html_input' => $allowHtml ? HtmlFilter::ALLOW : HtmlFilter::ESCAPE, + 'allow_unsafe_links' => false, + 'max_nesting_level' => 42, + 'max_delimiters_per_line' => 42, + 'default_attributes' => [ + Table::class => [ + 'class' => ['table', 'table-striped', 'table-sticky-header', 'data'], + ], + ], + 'table' => [ + 'alignment_attributes' => [ + 'left' => ['class' => 'text-start'], + 'center' => ['class' => 'text-center'], + 'right' => ['class' => 'text-end'], + ], + ], + ]; + + $environment = new Environment($config); + $environment->addExtension(new CommonMarkCoreExtension()); + $environment->addExtension(new GithubFlavoredMarkdownExtension()); + $environment->addExtension(new DefaultAttributesExtension()); + + return new MarkdownConverter($environment); + } +} diff --git a/src/Helpers/Schedule/Event.php b/src/Helpers/Schedule/Event.php index 9ea738827..ee7b50a9b 100644 --- a/src/Helpers/Schedule/Event.php +++ b/src/Helpers/Schedule/Event.php @@ -48,6 +48,7 @@ public function __construct( protected ?string $url = null, protected ?string $videoDownloadUrl = null, protected ?string $feedbackUrl = null, + protected ?string $originUrl = null, ) { $this->endDate = $this->date ->copy() @@ -172,6 +173,11 @@ public function getFeedbackUrl(): ?string return $this->feedbackUrl; } + public function getOriginUrl(): ?string + { + return $this->originUrl; + } + public function getVideoDownloadUrl(): ?string { return $this->videoDownloadUrl; diff --git a/src/Helpers/Schedule/Schedule.php b/src/Helpers/Schedule/Schedule.php index b8dedeca1..67d1de65e 100644 --- a/src/Helpers/Schedule/Schedule.php +++ b/src/Helpers/Schedule/Schedule.php @@ -39,20 +39,36 @@ public function getDays(): array /** * @return Room[] + * + * Returns a list of all rooms for all days */ - public function getRooms(): array + public function getAllRooms(): array { $rooms = []; foreach ($this->days as $day) { foreach ($day->getRooms() as $room) { - $name = $room->getName(); - $rooms[$name] = $room; + $rooms[] = $room; } } return $rooms; } + /** + * @return Room[] + * + * Returns a list of rooms unique by name and thus the instance of the last day they are used + */ + public function getRooms(): array + { + $rooms = []; + foreach ($this->getAllRooms() as $room) { + $name = $room->getName(); + $rooms[$name] = $room; + } + + return $rooms; + } public function getStartDateTime(): ?Carbon { diff --git a/src/Helpers/Schedule/XmlParser.php b/src/Helpers/Schedule/XmlParser.php index 5fedeb4f0..ac8d9e23c 100644 --- a/src/Helpers/Schedule/XmlParser.php +++ b/src/Helpers/Schedule/XmlParser.php @@ -180,6 +180,7 @@ protected function parseEvents(array $eventElements, Room $room, array $tracks): $this->getFirstXpathContent('url', $event) ?: null, $this->getFirstXpathContent('video_download_url', $event) ?: null, $this->getFirstXpathContent('feedback_url', $event) ?: null, + $this->getFirstXpathContent('origin_url', $event) ?: null, ); } diff --git a/src/Helpers/ShiftsRenderer.php b/src/Helpers/ShiftsRenderer.php new file mode 100644 index 000000000..41ba4f189 --- /dev/null +++ b/src/Helpers/ShiftsRenderer.php @@ -0,0 +1,83 @@ +id] = $shift->shiftEntries; + + if (!$shift->schedule) { + $angelTypes = $shift->neededAngelTypes; + } else { + if ($shift->schedule->needed_from_shift_type) { + $angelTypes = $shift->shiftType->neededAngelTypes; + } else { + $angelTypes = $shift->location->neededAngelTypes; + } + } + + $neededAngelTypes[$shift->id] = []; + foreach ($angelTypes as $nAngelType) { + $data = $nAngelType->toArray(); + $data['id'] = $nAngelType->angelType->id; + $data['name'] = $nAngelType->angelType->name; + $data['restricted'] = $nAngelType->angelType->restricted; + $data['shift_self_signup'] = $nAngelType->angelType->shift_self_signup; + $neededAngelTypes[$shift->id][] = $data; + } + } + + return $this->renderShiftCalendar($shifts, $neededAngelTypes, $shiftEntries); + } + + /** + * @param Collection|Shift[] $shifts + * @param array[] $neededAngelTypes + * @param ShiftEntry[][] $shiftEntries + * @codeCoverageIgnore + */ + protected function renderShiftCalendar( + array | Collection $shifts, + array $neededAngelTypes, + array $shiftEntries + ): string { + if (!$shifts instanceof Collection) { + $shifts = collect($shifts); + } + + /** @var Carbon $start */ + $start = $shifts->min('start'); + /** @var Carbon $end */ + $end = $shifts->max('end'); + + $shiftsFilter = new ShiftsFilter(); + $shiftsFilter->setStartTime($start ? $start->timestamp : 0); + $shiftsFilter->setEndTime($end ? $end->timestamp : 0); + + $renderer = new ShiftCalendarRenderer($shifts, $neededAngelTypes, $shiftEntries, $shiftsFilter); + + return $renderer->render(); + } +} diff --git a/src/Helpers/Translation/Translator.php b/src/Helpers/Translation/Translator.php index cd27a1a2d..920bc1df2 100644 --- a/src/Helpers/Translation/Translator.php +++ b/src/Helpers/Translation/Translator.php @@ -24,7 +24,7 @@ public function __construct( protected string $fallbackLocale, callable $getTranslatorCallback, protected array $locales = [], - callable $localeChangeCallback = null + ?callable $localeChangeCallback = null ) { $this->localeChangeCallback = $localeChangeCallback; $this->getTranslatorCallback = $getTranslatorCallback; diff --git a/src/Helpers/UserVouchers.php b/src/Helpers/UserVouchers.php new file mode 100644 index 000000000..fec2c3be4 --- /dev/null +++ b/src/Helpers/UserVouchers.php @@ -0,0 +1,61 @@ +setTime(0, 0) + : null; + + $shiftEntries = $user->shiftEntries() + ->join('shifts', 'shift_entries.shift_id', '=', 'shifts.id') + ->where('shifts.end', '<', Carbon::now()) + ->where('shifts.start', '>=', $start ?: 0) + ->whereNull('freeloaded_by') + ->get() + ->load('shift'); + $worklogs = $user->worklogs() + ->where('worked_at', '>=', $start ?: 0) + ->where('worked_at', '<=', Carbon::now()) + ->get(); + $shiftsCount = + $shiftEntries->count() + + $worklogs->count(); + + $shiftsTime = 0; + foreach ($shiftEntries as $shiftEntry) { + $shiftsTime += $shiftEntry->shift->start->diffInHours($shiftEntry->shift->end); + } + foreach ($worklogs as $worklog) { + $shiftsTime += $worklog->hours; + } + + $vouchers = $voucherSettings['initial_vouchers']; + if ($voucherSettings['shifts_per_voucher']) { + $vouchers += $shiftsCount / $voucherSettings['shifts_per_voucher']; + } + if ($voucherSettings['hours_per_voucher']) { + $vouchers += $shiftsTime / $voucherSettings['hours_per_voucher']; + } + + $vouchers -= $user->state->got_voucher; + $vouchers = floor($vouchers); + if ($vouchers <= 0) { + return 0; + } + + return (int) $vouchers; + } +} diff --git a/src/Helpers/Uuid.php b/src/Helpers/Uuid.php index 4fd94aafe..79507209f 100644 --- a/src/Helpers/Uuid.php +++ b/src/Helpers/Uuid.php @@ -31,7 +31,7 @@ public static function uuid(): string * Generate a dependent v4 UUID * @var string|int|float|Stringable $value any value that can be converted to string */ - public static function uuidBy(mixed $value, string $name = null): string + public static function uuidBy(mixed $value, ?string $name = null): string { if (!is_null($name)) { if (!preg_match('/^[\da-f]+$/i', $name)) { diff --git a/src/Http/Exceptions/HttpAuthExpired.php b/src/Http/Exceptions/HttpAuthExpired.php index a06a94841..f9005f222 100644 --- a/src/Http/Exceptions/HttpAuthExpired.php +++ b/src/Http/Exceptions/HttpAuthExpired.php @@ -8,14 +8,11 @@ class HttpAuthExpired extends HttpException { - /** - * @param Throwable|null $previous - */ public function __construct( string $message = 'Authentication Expired', array $headers = [], int $code = 0, - Throwable $previous = null + ?Throwable $previous = null ) { // The 419 code is used as "Page Expired" to differentiate from a 401 (not authorized) parent::__construct(419, $message, $headers, $code, $previous); diff --git a/src/Http/Exceptions/HttpException.php b/src/Http/Exceptions/HttpException.php index 74b93f089..488d37724 100644 --- a/src/Http/Exceptions/HttpException.php +++ b/src/Http/Exceptions/HttpException.php @@ -9,15 +9,12 @@ class HttpException extends RuntimeException { - /** - * @param Throwable|null $previous - */ public function __construct( protected int $statusCode, string $message = '', protected array $headers = [], int $code = 0, - Throwable $previous = null + ?Throwable $previous = null ) { parent::__construct($message, $code, $previous); diff --git a/src/Http/Exceptions/HttpForbidden.php b/src/Http/Exceptions/HttpForbidden.php index a76eb1478..d85320110 100644 --- a/src/Http/Exceptions/HttpForbidden.php +++ b/src/Http/Exceptions/HttpForbidden.php @@ -8,14 +8,11 @@ class HttpForbidden extends HttpException { - /** - * @param Throwable|null $previous - */ public function __construct( string $message = '', array $headers = [], int $code = 0, - Throwable $previous = null + ?Throwable $previous = null ) { parent::__construct(403, $message, $headers, $code, $previous); } diff --git a/src/Http/Exceptions/HttpNotFound.php b/src/Http/Exceptions/HttpNotFound.php index 0e7938f2c..a848aaca9 100644 --- a/src/Http/Exceptions/HttpNotFound.php +++ b/src/Http/Exceptions/HttpNotFound.php @@ -8,14 +8,11 @@ class HttpNotFound extends HttpException { - /** - * @param Throwable|null $previous - */ public function __construct( string $message = '', array $headers = [], int $code = 0, - Throwable $previous = null + ?Throwable $previous = null ) { parent::__construct(404, $message, $headers, $code, $previous); } diff --git a/src/Http/Exceptions/ValidationException.php b/src/Http/Exceptions/ValidationException.php index ea96bedb1..cd624a490 100644 --- a/src/Http/Exceptions/ValidationException.php +++ b/src/Http/Exceptions/ValidationException.php @@ -10,14 +10,11 @@ class ValidationException extends RuntimeException { - /** - * @param Throwable|null $previous - */ public function __construct( protected Validator $validator, string $message = '', int $code = 0, - Throwable $previous = null + ?Throwable $previous = null ) { parent::__construct($message, $code, $previous); } diff --git a/src/Http/PaginationServiceProvider.php b/src/Http/PaginationServiceProvider.php new file mode 100644 index 000000000..8bd353192 --- /dev/null +++ b/src/Http/PaginationServiceProvider.php @@ -0,0 +1,16 @@ +app); + } +} diff --git a/src/Http/Request.php b/src/Http/Request.php index a858e9c80..cf28cbf50 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -26,6 +26,8 @@ public function postData(string $key, mixed $default = null): mixed /** * Get input data + * + * @deprecated */ public function input(string $key, mixed $default = null): mixed { @@ -34,6 +36,8 @@ public function input(string $key, mixed $default = null): mixed /** * Checks if the input exists + * + * @deprecated */ public function has(string $key): bool { @@ -42,6 +46,28 @@ public function has(string $key): bool return !is_null($value); } + /** + * Get input from any request part (attributes, GET or POST) + * + * @deprecated + */ + public function get(string $key, mixed $default = null): mixed + { + if ($this !== $result = $this->attributes->get($key, $this)) { + return $result; + } + + if ($this->query->has($key)) { + return $this->query->all()[$key]; + } + + if ($this->request->has($key)) { + return $this->request->all()[$key]; + } + + return $default; + } + /** * Checks if the POST data exists */ diff --git a/src/Http/SessionHandlers/DatabaseHandler.php b/src/Http/SessionHandlers/DatabaseHandler.php index 5851fa8be..9d885999d 100644 --- a/src/Http/SessionHandlers/DatabaseHandler.php +++ b/src/Http/SessionHandlers/DatabaseHandler.php @@ -33,7 +33,7 @@ public function write(string $id, string $data): bool $session->id = $id; $session->payload = $data; $session->last_activity = Carbon::now(); - $session->user_id = auth()->user()?->id; + $session->user_id = auth()->user()?->id ?? null; $session->save(); // The save return can't be used directly as it won't change if the second call is in the same second diff --git a/src/Http/Validation/Rules/After.php b/src/Http/Validation/Rules/After.php index afd329763..f378e1f10 100644 --- a/src/Http/Validation/Rules/After.php +++ b/src/Http/Validation/Rules/After.php @@ -4,14 +4,14 @@ namespace Engelsystem\Http\Validation\Rules; -use Respect\Validation\Rules\AbstractRule; +use Respect\Validation\Rules\AbstractComparison; -class After extends AbstractRule +class After extends AbstractComparison { use ComparesDateTime; - protected function compare(mixed $input): bool + protected function compare(mixed $left, mixed $right): bool { - return $this->orEqual ? $input >= $this->compareTo : $input > $this->compareTo; + return $this->orEqual ? $left >= $right : $left > $right; } } diff --git a/src/Http/Validation/Rules/Before.php b/src/Http/Validation/Rules/Before.php index 926bdcc3a..77ee3b09b 100644 --- a/src/Http/Validation/Rules/Before.php +++ b/src/Http/Validation/Rules/Before.php @@ -4,14 +4,14 @@ namespace Engelsystem\Http\Validation\Rules; -use Respect\Validation\Rules\AbstractRule; +use Respect\Validation\Rules\AbstractComparison; -class Before extends AbstractRule +class Before extends AbstractComparison { use ComparesDateTime; - protected function compare(mixed $input): bool + protected function compare(mixed $left, mixed $right): bool { - return $this->orEqual ? $input <= $this->compareTo : $input < $this->compareTo; + return $this->orEqual ? $left <= $right : $left < $right; } } diff --git a/src/Http/Validation/Rules/Between.php b/src/Http/Validation/Rules/Between.php index 8f502f03c..731d37be1 100644 --- a/src/Http/Validation/Rules/Between.php +++ b/src/Http/Validation/Rules/Between.php @@ -5,8 +5,14 @@ namespace Engelsystem\Http\Validation\Rules; use Respect\Validation\Rules\Between as RespectBetween; +use Respect\Validation\Rules\Core\Envelope; -class Between extends RespectBetween +class Between extends Envelope { use StringInputLength; + + public function __construct(mixed $minValue, mixed $maxValue) + { + parent::__construct(new RespectBetween($minValue, $maxValue)); + } } diff --git a/src/Http/Validation/Rules/Checked.php b/src/Http/Validation/Rules/Checked.php index f4da9352b..0b220753a 100644 --- a/src/Http/Validation/Rules/Checked.php +++ b/src/Http/Validation/Rules/Checked.php @@ -4,13 +4,13 @@ namespace Engelsystem\Http\Validation\Rules; -use Respect\Validation\Rules\AbstractRule; +use Respect\Validation\Rules\Core\Simple; -class Checked extends AbstractRule +class Checked extends Simple { use Truthy; - public function validate(mixed $input): bool + public function isValid(mixed $input): bool { return $this->truthy($input); } diff --git a/src/Http/Validation/Rules/ComparesDateTime.php b/src/Http/Validation/Rules/ComparesDateTime.php index 13f678d1e..9f8624a8e 100644 --- a/src/Http/Validation/Rules/ComparesDateTime.php +++ b/src/Http/Validation/Rules/ComparesDateTime.php @@ -23,7 +23,7 @@ public function validate(mixed $input): bool { $input = $this->toDateTime($input); - return $this->compare($input); + return $this->compare($input, $this->compareTo); } protected function toDateTime(mixed $value): mixed diff --git a/src/Http/Validation/Rules/DateTime.php b/src/Http/Validation/Rules/DateTime.php index fba556845..87f21bc9a 100644 --- a/src/Http/Validation/Rules/DateTime.php +++ b/src/Http/Validation/Rules/DateTime.php @@ -4,9 +4,10 @@ namespace Engelsystem\Http\Validation\Rules; -use Respect\Validation\Rules\Date; +use Respect\Validation\Rules\Core\Envelope; +use Respect\Validation\Rules\DateTime as RespectDateTime; -class DateTime extends Date +class DateTime extends Envelope { public function __construct(?string $format = null) { @@ -14,6 +15,6 @@ public function __construct(?string $format = null) $format = 'Y-m-d\TH:i'; } - parent::__construct($format); + parent::__construct(new RespectDateTime($format)); } } diff --git a/src/Http/Validation/Rules/Email.php b/src/Http/Validation/Rules/Email.php deleted file mode 100644 index a14d0e92f..000000000 --- a/src/Http/Validation/Rules/Email.php +++ /dev/null @@ -1,14 +0,0 @@ -errors = []; $this->data = []; + $this->configureValidationFactory(); + $validData = []; foreach ($rules as $fieldName => $rulesList) { - $v = new RespectValidator(); - $v->with('\\Engelsystem\\Http\\Validation\\Rules', true); - $value = $data[$fieldName] ?? null; $rulesList = is_array($rulesList) ? $rulesList : explode('|', $rulesList); // Configure the check to be run for every rule foreach ($rulesList as $parameters) { + $v = new RespectValidator(); + $parameters = is_array($parameters) ? $parameters : explode(':', $parameters); $rule = array_shift($parameters); $rule = Str::camel($rule); @@ -68,8 +71,6 @@ public function validate(array $data, array $rules): bool } else { $this->errors[$fieldName][] = implode('.', ['validation', $fieldName, $this->mapBack($rule)]); } - - $v->removeRules(); } } @@ -112,4 +113,16 @@ public function addErrors(array $errors): self return $this; } + + protected function configureValidationFactory(): void + { + $f = (new Factory()) + ->withRuleNamespace('\\Engelsystem\\Http\\Validation\\Rules'); + + // Hacking around, alternative is to reimplement it... + $property = new ReflectionProperty($f, 'rulesNamespaces'); + $property->setValue($f, array_reverse($property->getValue($f))); + + Factory::setDefaultInstance($f); + } } diff --git a/src/Logger/Logger.php b/src/Logger/Logger.php index f697a2a89..c33350704 100644 --- a/src/Logger/Logger.php +++ b/src/Logger/Logger.php @@ -68,7 +68,8 @@ protected function interpolate(string $message, array $context = []): string protected function formatException(Throwable $e): string { return sprintf( - implode(PHP_EOL, ['', 'Exception: %s', 'File: %s:%u', 'Code: %s', 'Trace:', '%s']), + implode(PHP_EOL, ['', '%s: %s', 'File: %s:%u', 'Code: %s', 'Trace:', '%s']), + get_class($e), $e->getMessage(), $e->getFile(), $e->getLine(), diff --git a/src/Mail/EngelsystemMailer.php b/src/Mail/EngelsystemMailer.php index bea73cc9c..8680a9329 100644 --- a/src/Mail/EngelsystemMailer.php +++ b/src/Mail/EngelsystemMailer.php @@ -12,26 +12,15 @@ class EngelsystemMailer extends Mailer { - protected ?Renderer $view = null; - - protected ?Translator $translation = null; - protected ?string $subjectPrefix = null; - /** - * @param Renderer|null $view - * @param Translator|null $translation - */ public function __construct( LoggerInterface $log, MailerInterface $mailer, - Renderer $view = null, - Translator $translation = null + protected ?Renderer $view = null, + protected ?Translator $translation = null ) { parent::__construct($log, $mailer); - - $this->translation = $translation; - $this->view = $view; } /** diff --git a/src/Middleware/ApiRouteHandler.php b/src/Middleware/ApiRouteHandler.php index f2a42a16b..4df9b36b2 100644 --- a/src/Middleware/ApiRouteHandler.php +++ b/src/Middleware/ApiRouteHandler.php @@ -29,7 +29,6 @@ public function __construct( '/ical', '/metrics', '/shifts-json-export', - '/stats', ] ) { } @@ -70,7 +69,7 @@ protected function processApi(ServerRequestInterface $request, RequestHandlerInt } catch (Throwable $e) { /** @var Handler $handler */ $handler = app('error.handler'); - $handler->exceptionHandler($e, true); + $handler->exceptionHandler($e, false); $response = new Response('', 500); $response->setContent($response->getReasonPhrase()); } diff --git a/src/Middleware/CallableHandler.php b/src/Middleware/CallableHandler.php index 3923a66c5..f1c799fba 100644 --- a/src/Middleware/CallableHandler.php +++ b/src/Middleware/CallableHandler.php @@ -20,15 +20,12 @@ class CallableHandler implements MiddlewareInterface, RequestHandlerInterface /** @var callable */ protected $callable; - protected ?Container $container = null; - /** * @param callable $callable The callable that should be wrapped */ - public function __construct(callable $callable, Container $container = null) + public function __construct(callable $callable, protected ?Container $container = null) { $this->callable = $callable; - $this->container = $container; } /** diff --git a/src/Middleware/ExceptionHandler.php b/src/Middleware/ExceptionHandler.php index 967b0ff65..440624960 100644 --- a/src/Middleware/ExceptionHandler.php +++ b/src/Middleware/ExceptionHandler.php @@ -32,7 +32,7 @@ public function process( } catch (Throwable $e) { /** @var ExceptionsHandler $handler */ $handler = $this->container->get('error.handler'); - $content = $handler->exceptionHandler($e, true); + $content = $handler->exceptionHandler($e, false); return response($content, 500); } diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php index 807ca1d3f..1c4909e56 100644 --- a/src/Middleware/LegacyMiddleware.php +++ b/src/Middleware/LegacyMiddleware.php @@ -63,6 +63,10 @@ public function process( list($title, $content) = $this->loadPage($page); } + if ($content instanceof ResponseInterface) { + return $content; + } + if (empty($title) and empty($content)) { /** @var Translator $translator */ $translator = $this->container->get('translator'); diff --git a/src/Middleware/RequestHandler.php b/src/Middleware/RequestHandler.php index 91592aa2b..212bcc764 100644 --- a/src/Middleware/RequestHandler.php +++ b/src/Middleware/RequestHandler.php @@ -39,8 +39,12 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface if ($requestHandler instanceof CallableHandler) { $callable = $requestHandler->getCallable(); - if (is_array($callable) && $callable[0] instanceof BaseController) { - $this->checkPermissions($callable[0], $callable[1]); + if ( + is_array($callable) + && $callable[0] instanceof BaseController + && !$this->checkPermissions($request, $callable[0], $callable[1]) + ) { + throw new HttpForbidden(); } } @@ -87,8 +91,16 @@ class_exists($handler[0]) /** * Check required page permissions */ - protected function checkPermissions(BaseController $controller, string $method): bool - { + protected function checkPermissions( + ServerRequestInterface $request, + BaseController $controller, + string $method + ): bool { + $hasPermission = $controller->hasPermission($request, $method); + if (!is_null($hasPermission)) { + return $hasPermission; + } + /** @var Authenticator $auth */ $auth = $this->container->get('auth'); $permissions = $controller->getPermissions(); @@ -110,7 +122,7 @@ protected function checkPermissions(BaseController $controller, string $method): ? !$auth->canAny(explode('||', $value)) : !$auth->can($permission) ) { - throw new HttpForbidden(); + return false; } } } diff --git a/src/Middleware/SendResponseHandler.php b/src/Middleware/SendResponseHandler.php index 9931afed7..2cfac4bb3 100644 --- a/src/Middleware/SendResponseHandler.php +++ b/src/Middleware/SendResponseHandler.php @@ -56,7 +56,7 @@ protected function headersSent(): bool * * @codeCoverageIgnore */ - protected function sendHeader(string $content, bool $replace = true, int $code = null): void + protected function sendHeader(string $content, bool $replace = true, ?int $code = null): void { if (is_null($code)) { header($content, $replace); diff --git a/src/Models/AngelType.php b/src/Models/AngelType.php index bae063dad..50927760d 100644 --- a/src/Models/AngelType.php +++ b/src/Models/AngelType.php @@ -57,7 +57,7 @@ class AngelType extends BaseModel 'contact_name' => '', 'contact_dect' => '', 'contact_email' => '', - 'restricted' => false, + 'restricted' => true, 'requires_driver_license' => false, 'requires_ifsg_certificate' => false, 'shift_self_signup' => true, diff --git a/src/Models/LogEntry.php b/src/Models/LogEntry.php index 7ee78730b..4a8d1bbe5 100644 --- a/src/Models/LogEntry.php +++ b/src/Models/LogEntry.php @@ -60,8 +60,11 @@ class LogEntry extends BaseModel /** * @return Builder[]|Collection|SupportCollection|LogEntry[] */ - public static function filter(?string $keyword = null, ?int $userId = null): array|Collection|SupportCollection - { + public static function filter( + ?string $keyword = null, + ?int $userId = null, + ?string $level = null + ): array | Collection | SupportCollection { $query = self::with(['user', 'user.personalData', 'user.state']) ->orderByDesc('created_at') ->orderByDesc('id') @@ -75,12 +78,12 @@ public static function filter(?string $keyword = null, ?int $userId = null): arr }); } + if (!empty($level)) { + $query->where('level', '=', $level); + } + if (!empty($keyword)) { - $query - ->where(function (Builder $query) use ($keyword): void { - $query->where('level', '=', $keyword) - ->orWhere('message', 'LIKE', '%' . $keyword . '%'); - }); + $query->where('message', 'LIKE', '%' . $keyword . '%'); } return $query->get(); diff --git a/src/Models/Shifts/ScheduleShift.php b/src/Models/Shifts/ScheduleShift.php index 39a7759ee..72c42093c 100644 --- a/src/Models/Shifts/ScheduleShift.php +++ b/src/Models/Shifts/ScheduleShift.php @@ -5,6 +5,7 @@ namespace Engelsystem\Models\Shifts; use Engelsystem\Models\BaseModel; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Query\Builder as QueryBuilder; @@ -22,6 +23,8 @@ */ class ScheduleShift extends BaseModel { + use HasFactory; + /** @var string The primary key for the model */ protected $primaryKey = 'shift_id'; // phpcs:ignore diff --git a/src/Models/Shifts/Shift.php b/src/Models/Shifts/Shift.php index cbe96b02b..c47237fe5 100644 --- a/src/Models/Shifts/Shift.php +++ b/src/Models/Shifts/Shift.php @@ -8,12 +8,16 @@ use Engelsystem\Models\BaseModel; use Engelsystem\Models\Location; use Engelsystem\Models\User\User; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOneThrough; use Illuminate\Database\Query\Builder as QueryBuilder; +use Illuminate\Database\Query\Grammars\SQLiteGrammar; +use Illuminate\Database\Query\JoinClause; /** * @property int $id @@ -32,6 +36,7 @@ * * @property-read Collection|NeededAngelType[] $neededAngelTypes * @property-read Schedule $schedule + * @property-read ScheduleShift $scheduleShift * @property-read Collection|ShiftEntry[] $shiftEntries * @property-read ShiftType $shiftType * @property-read Location $location @@ -101,6 +106,11 @@ public function schedule(): HasOneThrough return $this->hasOneThrough(Schedule::class, ScheduleShift::class, null, 'id', null, 'schedule_id'); } + public function scheduleShift(): HasOne + { + return $this->hasOne(ScheduleShift::class); + } + public function shiftEntries(): HasMany { return $this->hasMany(ShiftEntry::class); @@ -126,6 +136,91 @@ public function updatedBy(): BelongsTo return $this->belongsTo(User::class, 'updated_by'); } + public function scopeNeedsUsers(Builder $query): void + { + $query + ->addSelect([ + // This is "hidden" behind an attribute to not "poison" the SELECT default with fields from added joins + 'needs_users' => Shift::from('shifts as s2') + ->leftJoin('schedule_shift', 'schedule_shift.shift_id', 's2.id') + ->leftJoin('schedules', 'schedules.id', 'schedule_shift.schedule_id') + ->leftJoin('needed_angel_types', function (JoinClause $join): void { + // Directly + $join->on('needed_angel_types.shift_id', 's2.id') + // Via schedule location + ->orOn('needed_angel_types.location_id', 's2.location_id') + // Via schedule shift type + ->orOn('needed_angel_types.shift_type_id', 'schedules.shift_type'); + }) + ->whereColumn('s2.id', 'shifts.id') + ->where(function (Builder $query): void { + $query + ->where(function (Builder $query): void { + $query + // Direct requirement + ->whereColumn('needed_angel_types.shift_id', 's2.id') + // Or has schedule & via location + ->orWhere(function (Builder $query): void { + $query + ->where('schedules.needed_from_shift_type', false) + ->whereColumn('needed_angel_types.location_id', 's2.location_id'); + }) + // Or has schedule & via type + ->orWhere(function (Builder $query): void { + $query + ->where('schedules.needed_from_shift_type', true) + ->whereColumn('needed_angel_types.shift_type_id', 's2.shift_type_id'); + }); + }); + }) + ->selectRaw('COUNT(*) > 0'), + ]); + + if ($query->getConnection()->getQueryGrammar() instanceof SQLiteGrammar) { + // SQLite does not support HAVING for non-aggregate queries + $query->where('needs_users', '>', 0); + } else { + // @codeCoverageIgnoreStart + // needs_users is defined on select and thus only available after select + $query->having('needs_users', '>', 0); + // @codeCoverageIgnoreEnd + } + } + + /** + * get next shift with same shift type and location + */ + public function nextShift(): Shift|null + { + $query = Shift::query(); + if (Shift::whereTitle($this->title)->where('start', '>', $this->start)->exists()) { + $query = $query->where('title', $this->title); + } + return $query + ->where('shift_type_id', $this->shiftType->id) + ->where('location_id', $this->location->id) + ->where('start', '>', $this->start) + ->orderBy('start') + ->first(); + } + + /** + * get previous shift with same shift type and location + */ + public function previousShift(): Shift|null + { + $query = Shift::query(); + if (Shift::whereTitle($this->title)->where('end', '<', $this->end)->exists()) { + $query = $query->where('title', $this->title); + } + return $query + ->where('shift_type_id', $this->shiftType->id) + ->where('location_id', $this->location->id) + ->where('end', '<', $this->end) + ->orderBy('end', 'desc') + ->first(); + } + /** * Check if the shift is a night shift */ @@ -133,7 +228,7 @@ public function isNightShift(): bool { $config = config('night_shifts'); - /** @see User_get_shifts_sum_query to keep it in sync */ + /** @see \Engelsystem\Helpers\Goodie::shiftScoreQuery to keep them in sync */ return $config['enabled'] && ( // Starts during night $this->start->hour >= $config['start'] && $this->start->hour < $config['end'] diff --git a/src/Models/User/State.php b/src/Models/User/State.php index c43e9fc63..067c70ea9 100644 --- a/src/Models/User/State.php +++ b/src/Models/User/State.php @@ -5,15 +5,17 @@ namespace Engelsystem\Models\User; use Carbon\Carbon; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Query\Builder as QueryBuilder; /** - * @property bool $arrived + * @property-read bool $arrived * @property Carbon|null $arrival_date * @property string|null $user_info * @property bool $active * @property bool $force_active + * @property bool $force_food * @property bool $got_goodie * @property int $got_voucher * @@ -22,6 +24,7 @@ * @method static QueryBuilder|State[] whereUserInfo($value) * @method static QueryBuilder|State[] whereActive($value) * @method static QueryBuilder|State[] whereForceActive($value) + * @method static QueryBuilder|State[] whereForceFood($value) * @method static QueryBuilder|State[] whereGotGoodie($value) * @method static QueryBuilder|State[] whereGotVoucher($value) */ @@ -34,11 +37,11 @@ class State extends HasUserModel /** @var array Default attributes */ protected $attributes = [ // phpcs:ignore - 'arrived' => false, 'arrival_date' => null, 'user_info' => null, 'active' => false, 'force_active' => false, + 'force_food' => false, 'got_goodie' => false, 'got_voucher' => 0, ]; @@ -46,10 +49,10 @@ class State extends HasUserModel /** @var array */ protected $casts = [ // phpcs:ignore 'user_id' => 'integer', - 'arrived' => 'boolean', 'arrival_date' => 'datetime', 'active' => 'boolean', 'force_active' => 'boolean', + 'force_food' => 'boolean', 'got_goodie' => 'boolean', 'got_voucher' => 'integer', ]; @@ -61,12 +64,31 @@ class State extends HasUserModel */ protected $fillable = [ // phpcs:ignore 'user_id', - 'arrived', 'arrival_date', 'user_info', 'active', 'force_active', + 'force_food', 'got_goodie', 'got_voucher', ]; + + /** + * Accessor: for arrived property + * Derived from arrival_date being not null + */ + public function getArrivedAttribute(): bool + { + return $this->arrival_date !== null; + } + + /** + * provide WhereArrived query scope + */ + public static function scopeWhereArrived(Builder $query, bool $value): Builder + { + return $value + ? $query->whereNotNull('arrival_date') + : $query->whereNull('arrival_date'); + } } diff --git a/src/Models/Worklog.php b/src/Models/Worklog.php index f17343d74..174206775 100644 --- a/src/Models/Worklog.php +++ b/src/Models/Worklog.php @@ -15,8 +15,9 @@ * @property int $id * @property int $creator_id * @property float $hours - * @property string $comment + * @property string $description * @property Carbon $worked_at + * @property bool $night_shift * @property Carbon|null $created_at * @property Carbon|null $updated_at * @@ -26,7 +27,7 @@ * @method static QueryBuilder|Worklog[] whereCreatorId($value) * @method static QueryBuilder|Worklog[] whereWorkedAt($value) * @method static QueryBuilder|Worklog[] whereHours($value) - * @method static QueryBuilder|Worklog[] whereComment($value) + * @method static QueryBuilder|Worklog[] whereDescription($value) */ class Worklog extends BaseModel { @@ -38,10 +39,11 @@ class Worklog extends BaseModel /** @var array */ protected $casts = [ // phpcs:ignore - 'user_id' => 'integer', - 'creator_id' => 'integer', - 'hours' => 'float', - 'worked_at' => 'datetime', + 'user_id' => 'integer', + 'creator_id' => 'integer', + 'hours' => 'float', + 'worked_at' => 'datetime', + 'night_shift' => 'boolean', ]; /** @@ -53,8 +55,9 @@ class Worklog extends BaseModel 'user_id', 'creator_id', 'hours', - 'comment', + 'description', 'worked_at', + 'night_shift', ]; public function creator(): BelongsTo diff --git a/src/Renderer/Twig/Extensions/Legacy.php b/src/Renderer/Twig/Extensions/Legacy.php index ea23fdd01..ae628cb20 100644 --- a/src/Renderer/Twig/Extensions/Legacy.php +++ b/src/Renderer/Twig/Extensions/Legacy.php @@ -4,8 +4,11 @@ namespace Engelsystem\Renderer\Twig\Extensions; +use Engelsystem\Helpers\ShiftsRenderer; use Engelsystem\Http\Request; +use Illuminate\Database\Eloquent\Collection; use Twig\Extension\AbstractExtension as TwigExtension; +use Twig\TwigFilter; use Twig\TwigFunction; class Legacy extends TwigExtension @@ -25,10 +28,21 @@ public function getFunctions(): array new TwigFunction('menuUserShiftState', 'User_shift_state_render', $isSafeHtml), new TwigFunction('menuUserHints', 'header_render_hints', $isSafeHtml), new TwigFunction('menuLanguages', 'make_language_select', $isSafeHtml), + new TwigFunction('renderShifts', [$this, 'renderShifts'], $isSafeHtml), new TwigFunction('page', [$this, 'getPage']), ]; } + /** + * @return TwigFilter[] + */ + public function getFilters(): array + { + return [ + new TwigFilter('dateWithEventDay', 'dateWithEventDay'), + ]; + } + public function getPage(): string { if ($this->request->has('p')) { @@ -37,4 +51,11 @@ public function getPage(): string return $this->request->path(); } + + public function renderShifts(array | Collection $shifts): string + { + /** @var ShiftsRenderer $renderer */ + $renderer = app()->make(ShiftsRenderer::class); + return $renderer->render($shifts); + } } diff --git a/src/Renderer/Twig/Extensions/Markdown.php b/src/Renderer/Twig/Extensions/Markdown.php index 9e06c9ef5..d6f6d9b3d 100644 --- a/src/Renderer/Twig/Extensions/Markdown.php +++ b/src/Renderer/Twig/Extensions/Markdown.php @@ -4,13 +4,13 @@ namespace Engelsystem\Renderer\Twig\Extensions; -use Parsedown; +use Engelsystem\Helpers\Markdown as MarkdownRenderer; use Twig\Extension\AbstractExtension as TwigExtension; use Twig\TwigFilter; class Markdown extends TwigExtension { - public function __construct(protected Parsedown $renderer) + public function __construct(protected MarkdownRenderer $renderer) { } @@ -24,8 +24,9 @@ public function getFilters(): array ]; } - public function render(string $text, bool $escapeHtml = true): string + public function render(mixed $text, bool $escapeHtml = true): string { - return $this->renderer->setSafeMode($escapeHtml)->text($text); + return $this->renderer + ->render((string) $text, !$escapeHtml); } } diff --git a/src/Renderer/Twig/Extensions/Notification.php b/src/Renderer/Twig/Extensions/Notification.php index fefe8220c..476c103b1 100644 --- a/src/Renderer/Twig/Extensions/Notification.php +++ b/src/Renderer/Twig/Extensions/Notification.php @@ -32,7 +32,7 @@ public function getFunctions(): array /** * @return Collection|Collection[] */ - public function notifications(string $type = null): Collection + public function notifications(?string $type = null): Collection { $types = $type ? [NotificationType::from($type)] : null; diff --git a/src/Renderer/Twig/Extensions/Qr.php b/src/Renderer/Twig/Extensions/Qr.php new file mode 100644 index 000000000..9af87fa99 --- /dev/null +++ b/src/Renderer/Twig/Extensions/Qr.php @@ -0,0 +1,36 @@ + ['html']]), + ]; + } + + public function getQr(string $content, int $size = 200): string + { + $renderer = new ImageRenderer( + new RendererStyle($size), + new SvgImageBackEnd(), + ); + $writer = new Writer($renderer); + + return $writer->writeString($content); + } +} diff --git a/src/Renderer/TwigServiceProvider.php b/src/Renderer/TwigServiceProvider.php index 22aae06a7..ddbef9a59 100644 --- a/src/Renderer/TwigServiceProvider.php +++ b/src/Renderer/TwigServiceProvider.php @@ -15,6 +15,7 @@ use Engelsystem\Renderer\Twig\Extensions\Legacy; use Engelsystem\Renderer\Twig\Extensions\Markdown; use Engelsystem\Renderer\Twig\Extensions\Notification; +use Engelsystem\Renderer\Twig\Extensions\Qr; use Engelsystem\Renderer\Twig\Extensions\Session; use Engelsystem\Renderer\Twig\Extensions\StringExtension; use Engelsystem\Renderer\Twig\Extensions\Translation; @@ -42,6 +43,7 @@ class TwigServiceProvider extends ServiceProvider 'string' => StringExtension::class, 'legacy' => Legacy::class, 'markdown' => Markdown::class, + 'qr' => Qr::class, 'translation' => Translation::class, 'url' => Url::class, 'uuid' => Uuid::class, diff --git a/src/helpers.php b/src/helpers.php index 1f166c6ea..65decd755 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -6,6 +6,7 @@ use Engelsystem\Config\Config; use Engelsystem\Events\EventDispatcher; use Engelsystem\Helpers\Authenticator; +use Engelsystem\Helpers\Cache; use Engelsystem\Helpers\Translation\Translator; use Engelsystem\Http\Redirector; use Engelsystem\Http\Request; @@ -19,7 +20,7 @@ * @return mixed|Application * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.UselessAnnotation */ -function app(string $id = null): mixed +function app(?string $id = null): mixed { if (is_null($id)) { return Application::getInstance(); @@ -46,12 +47,24 @@ function back(int $status = 302, array $headers = []): Response return $redirect->back($status, $headers); } +function cache(string|null $key = null, mixed $default = null, int $seconds = 60 * 60): mixed +{ + /** @var Cache $cache */ + $cache = app('cache'); + + if (empty($key)) { + return $cache; + } + + return $cache->get($key, $default, $seconds); +} + /** * Get or set config values * @return mixed|Config * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.UselessAnnotation */ -function config(string|array $key = null, mixed $default = null): mixed +function config(string|array|null $key = null, mixed $default = null): mixed { /** @var Config $config */ $config = app('config'); @@ -112,7 +125,7 @@ function redirect(string $path, int $status = 302, array $headers = []): Respons * @return mixed|Request * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.UselessAnnotation */ -function request(string $key = null, mixed $default = null): mixed +function request(?string $key = null, mixed $default = null): mixed { /** @var Request $request */ $request = app('request'); @@ -143,7 +156,7 @@ function response(mixed $content = '', int $status = 200, array $headers = []): * @return mixed|SessionInterface * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.UselessAnnotation */ -function session(string $key = null, mixed $default = null): mixed +function session(?string $key = null, mixed $default = null): mixed { /** @var SessionInterface $session */ $session = app('session'); @@ -158,7 +171,7 @@ function session(string $key = null, mixed $default = null): mixed /** * Translate the given message */ -function trans(string $key = null, array $replace = []): string|Translator +function trans(?string $key = null, array $replace = []): string|Translator { /** @var Translator $translator */ $translator = app('translator'); @@ -192,7 +205,7 @@ function _e(string $key, string $keyPlural, int $number, array $replace = []): s return $translator->translatePlural($key, $keyPlural, $number, $replace); } -function url(string $path = null, array $parameters = []): UrlGeneratorInterface|string +function url(?string $path = null, array $parameters = []): UrlGeneratorInterface|string { /** @var UrlGeneratorInterface $urlGenerator */ $urlGenerator = app('http.urlGenerator'); @@ -204,7 +217,7 @@ function url(string $path = null, array $parameters = []): UrlGeneratorInterface return $urlGenerator->to($path, $parameters); } -function view(string $template = null, array $data = []): Renderer|string +function view(?string $template = null, array $data = []): Renderer|string { /** @var Renderer $renderer */ $renderer = app('renderer'); diff --git a/tests/Feature/ApplicationFeatureTest.php b/tests/Feature/ApplicationFeatureTest.php index 3560e6ec2..06cd2af36 100644 --- a/tests/Feature/ApplicationFeatureTest.php +++ b/tests/Feature/ApplicationFeatureTest.php @@ -4,10 +4,13 @@ namespace Engelsystem\Test\Feature; +use Engelsystem\Application; use PHPUnit\Framework\TestCase; abstract class ApplicationFeatureTest extends TestCase { + protected Application $app; + public static function setUpBeforeClass(): void { $_SERVER['HTTP_HOST'] = 'foo.bar'; @@ -27,4 +30,11 @@ protected function tearDown(): void ini_set('date.timezone', 'UTC'); date_default_timezone_set('UTC'); } + + protected function setUp(): void + { + parent::setUp(); + + $this->app = app(); + } } diff --git a/tests/Feature/Controllers/RegistrationControllerTest.php b/tests/Feature/Controllers/RegistrationControllerTest.php index 052970d13..067c87ed1 100644 --- a/tests/Feature/Controllers/RegistrationControllerTest.php +++ b/tests/Feature/Controllers/RegistrationControllerTest.php @@ -213,6 +213,7 @@ private function createAngelTypes(): array { $angelType1 = AngelType::create([ 'name' => 'Test angel type 1', + 'restricted' => false, ]); $angelType2 = AngelType::create([ @@ -228,6 +229,7 @@ private function createAngelTypes(): array $angelType4 = AngelType::create([ 'name' => 'Test angel type 4', 'hide_register' => true, + 'restricted' => false, ]); $this->modelsToBeDeleted[] = $angelType1; diff --git a/tests/Feature/Helpers/GoodieTest.php b/tests/Feature/Helpers/GoodieTest.php new file mode 100644 index 000000000..09ebb4c62 --- /dev/null +++ b/tests/Feature/Helpers/GoodieTest.php @@ -0,0 +1,109 @@ + 'gn8', 'email' => 'g@n.8', 'password' => '', 'api_key' => '']); + $user->save(); + $this->createdModels[] = $user; + $workLog = new Worklog([ + 'user_id' => $user->id, + 'hours' => 3.87, + 'creator_id' => $user->id, + 'description' => '', + 'worked_at' => Carbon::now()->subHour(), + ]); + $workLog->save(); + $this->createdModels[] = $workLog; + $shiftType = new ShiftType([ + 'name' => 'Type', + 'description' => '', + ]); + $shiftType->save(); + $this->createdModels[] = $shiftType; + $location = new Location([ + 'name' => 'Local', + ]); + $location->save(); + $this->createdModels[] = $location; + $shift = new Shift([ + 'title' => 'Shift', + 'start' => Carbon::create('2020-03-02 1:00'), + 'end' => Carbon::create('2020-03-02 4:00'), + 'shift_type_id' => $shiftType->id, + 'location_id' => $location->id, + 'created_by' => $user->id, + ]); + $shift->save(); + $this->createdModels[] = $shift; + $angelType = new AngelType([ + 'name' => 'AngelType', + ]); + $angelType->save(); + $this->createdModels[] = $angelType; + $shiftEntry = new ShiftEntry([ + 'user_id' => $user->id, + 'shift_id' => $shift->id, + 'angel_type_id' => $angelType->id, + ]); + $shiftEntry->save(); + $this->createdModels[] = $shiftEntry; + + $result = Goodie::userScore($user); + + $this->assertEquals(9.87, round($result, 2)); + } + + private function deleteModels(): void + { + foreach ($this->createdModels as $model) { + $model->delete(); + } + } + + protected function setUp(): void + { + parent::setUp(); + + $this->createdModels = []; + config([ + 'night_shifts' => [ + 'enabled' => true, + 'start' => 2, + 'end' => 6, + 'multiplier' => 2, + ], + ]); + } + + public function tearDown(): void + { + parent::tearDown(); + $this->deleteModels(); + } +} diff --git a/tests/Unit/Config/ConfigServiceProviderTest.php b/tests/Unit/Config/ConfigServiceProviderTest.php index 7bb2e9231..d89d742b7 100644 --- a/tests/Unit/Config/ConfigServiceProviderTest.php +++ b/tests/Unit/Config/ConfigServiceProviderTest.php @@ -105,7 +105,7 @@ public function testRegisterException(): void */ public function testBoot(): void { - $app = $this->getApp(['get']); + $app = $this->getApp(['get', 'make']); /** @var EventConfig|MockObject $eventConfig */ $eventConfig = $this->createMock(EventConfig::class); @@ -140,6 +140,7 @@ public function testBoot(): void $this->setExpects($eloquentBuilder, 'get', [['name', 'value']], $configs); $this->setExpects($app, 'get', ['config'], $config, $this->exactly(3)); + $this->setExpects($app, 'make', [EventConfig::class], $eventConfig, $this->exactly(1)); $serviceProvider = new ConfigServiceProvider($app); $serviceProvider->boot(); @@ -147,7 +148,6 @@ public function testBoot(): void $serviceProvider = new ConfigServiceProvider($app, $eventConfig); $serviceProvider->boot(); $serviceProvider->boot(); - $serviceProvider->boot(); $this->assertArraySubset( [ diff --git a/tests/Unit/Controllers/Admin/FaqControllerTest.php b/tests/Unit/Controllers/Admin/FaqControllerTest.php index 927d6fdf4..6eb8dec72 100644 --- a/tests/Unit/Controllers/Admin/FaqControllerTest.php +++ b/tests/Unit/Controllers/Admin/FaqControllerTest.php @@ -78,7 +78,7 @@ public function testSaveCreateEdit(): void $controller->save($this->request); - $this->assertTrue($this->log->hasInfoThatContains('Updated')); + $this->assertTrue($this->log->hasInfoThatContains('Saved')); $faq = (new Faq())->find(2); $this->assertEquals('Foo?', $faq->question); @@ -111,6 +111,7 @@ public function testSavePreview(): void $this->assertEquals('New question', $faq->question); $this->assertEquals('New text', $faq->text); $this->assertEquals('Foo, Bar', $data['tags']); + $this->assertEquals(collect([new Tag(['name' => 'Foo']), new Tag(['name' => 'Bar'])]), $faq->tags); return $this->response; }); diff --git a/tests/Unit/Controllers/Admin/LogsControllerTest.php b/tests/Unit/Controllers/Admin/LogsControllerTest.php index 9cc7f9ade..7be59f34e 100644 --- a/tests/Unit/Controllers/Admin/LogsControllerTest.php +++ b/tests/Unit/Controllers/Admin/LogsControllerTest.php @@ -37,6 +37,16 @@ public function testIndex(): void $this->setExpects($auth, 'can', ['logs.all'], true, 2); $response = $this->createMock(Response::class); + $levels = [ + LogLevel::ALERT => 'Alert', + LogLevel::CRITICAL => 'Critical', + LogLevel::DEBUG => 'Debug', + LogLevel::EMERGENCY => 'Emergency', + LogLevel::ERROR => 'Error', + LogLevel::INFO => 'Info', + LogLevel::NOTICE => 'Notice', + LogLevel::WARNING => 'Warning', + ]; $response->expects($this->exactly(2)) ->method('withView') ->withConsecutive( @@ -45,12 +55,16 @@ public function testIndex(): void 'search' => null, 'users' => new Collection(), 'search_user_id' => null, + 'level' => null, + 'levels' => $levels, ]], ['admin/log.twig', [ 'entries' => new Collection([$error]), 'search' => 'error', 'users' => new Collection(), 'search_user_id' => null, + 'level' => null, + 'levels' => $levels, ]] ) ->willReturn($response); diff --git a/tests/Unit/Controllers/Admin/NewsControllerTest.php b/tests/Unit/Controllers/Admin/NewsControllerTest.php index 060965860..ce04ffc1b 100644 --- a/tests/Unit/Controllers/Admin/NewsControllerTest.php +++ b/tests/Unit/Controllers/Admin/NewsControllerTest.php @@ -117,12 +117,11 @@ public function saveCreateEditProvider(): array * @covers \Engelsystem\Controllers\Admin\NewsController::save * @dataProvider saveCreateEditProvider * - * @param int|null $id */ public function testSaveCreateEdit( string $text, bool $isMeeting, - int $id = null, + ?int $id = null, bool $sendNotification = false ): void { $this->request->attributes->set('news_id', $id); @@ -160,15 +159,49 @@ public function testSaveCreateEdit( $controller->save($this->request); - $this->assertTrue($this->log->hasInfoThatContains('Updated')); + $this->assertTrue($this->log->hasInfoThatContains('Saved')); $this->assertHasNotification('news.edit.success'); - $news = (new News())->find($id ?: 2); + /** @var News $news */ + $news = News::find($id ?: 2); $this->assertEquals($text, $news->text); $this->assertEquals($isMeeting, (bool) $news->is_meeting); } + /** + * @covers \Engelsystem\Controllers\Admin\NewsController::save + */ + public function testSaveNoContentChange(): void + { + /** @var News $news */ + $news = News::factory()->create([ + 'is_pinned' => false, + ]); + $this->request->attributes->set('news_id', $news->id); + $body = [ + 'title' => $news->title, + 'text' => $news->text, + 'is_pinned' => '1', + ]; + $this->addUser(); + $this->request = $this->request->withParsedBody($body); + + /** @var NewsController $controller */ + $controller = $this->app->make(NewsController::class); + $controller->setValidator(new Validator()); + + $controller->save($this->request); + + /** @var News $updatedNews */ + $updatedNews = News::find($news->id); + $this->assertEquals($news->title, $updatedNews->title); + $this->assertEquals($news->text, $updatedNews->text); + $this->assertEquals($news->created_at, $updatedNews->created_at); + $this->assertEquals($news->updated_at, $updatedNews->updated_at); + $this->assertTrue($updatedNews->is_pinned); + } + /** * @covers \Engelsystem\Controllers\Admin\NewsController::save */ diff --git a/tests/Unit/Controllers/Admin/QuestionsControllerTest.php b/tests/Unit/Controllers/Admin/QuestionsControllerTest.php index 480c47126..e0e64bbfc 100644 --- a/tests/Unit/Controllers/Admin/QuestionsControllerTest.php +++ b/tests/Unit/Controllers/Admin/QuestionsControllerTest.php @@ -121,11 +121,39 @@ public function testEdit(): void $controller->edit($this->request); } + /** + * @covers \Engelsystem\Controllers\Admin\QuestionsController::edit + */ + public function testEditNotFound(): void + { + $this->request->attributes->set('question_id', 42); + $this->expectException(ModelNotFoundException::class); + + /** @var QuestionsController $controller */ + $controller = $this->app->get(QuestionsController::class); + + $controller->edit($this->request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\QuestionsController::save + */ + public function testSaveNotFound(): void + { + $this->expectException(ModelNotFoundException::class); + + /** @var QuestionsController $controller */ + $controller = $this->app->make(QuestionsController::class); + $controller->setValidator(new Validator()); + $controller->save($this->request); + } + /** * @covers \Engelsystem\Controllers\Admin\QuestionsController::save */ public function testSaveCreateInvalid(): void { + $this->request->attributes->set('question_id', 1); $this->expectException(ValidationException::class); /** @var QuestionsController $controller */ @@ -157,7 +185,7 @@ public function testSaveCreateEdit(): void $controller->save($this->request); - $this->assertTrue($this->log->hasInfoThatContains('Updated')); + $this->assertTrue($this->log->hasInfoThatContains('Saved')); $this->assertHasNotification('question.edit.success'); $question = Question::find(2); diff --git a/tests/Unit/Controllers/Admin/ShiftTypesControllerTest.php b/tests/Unit/Controllers/Admin/ShiftTypesControllerTest.php index f733e9524..0b54c8bda 100644 --- a/tests/Unit/Controllers/Admin/ShiftTypesControllerTest.php +++ b/tests/Unit/Controllers/Admin/ShiftTypesControllerTest.php @@ -12,6 +12,7 @@ use Engelsystem\Http\Request; use Engelsystem\Http\Validation\Validator; use Engelsystem\Models\AngelType; +use Engelsystem\Models\Shifts\NeededAngelType; use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\Shifts\ShiftEntry; use Engelsystem\Models\Shifts\ShiftType; @@ -52,14 +53,34 @@ public function testView(): void { /** @var ShiftTypesController $controller */ $controller = $this->app->make(ShiftTypesController::class); + /** @var ShiftType $shiftType */ $shiftType = ShiftType::factory()->create(); + /** @var AngelType $angelType */ + $angelType = AngelType::factory()->create(); + /** @var Shift $shift */ + $shift = Shift::factory()->create(['shift_type_id' => $shiftType->id]); + NeededAngelType::factory()->create(['angel_type_id' => $angelType->id, 'shift_id' => $shift->id]); $this->response->expects($this->once()) ->method('withView') - ->willReturnCallback(function (string $view, array $data) use ($shiftType) { + ->willReturnCallback(function (string $view, array $data) use ($shift) { $this->assertEquals('admin/shifttypes/view', $view); + $this->assertArrayHasKey('shifttype', $data); - $this->assertEquals($shiftType->id, $data['shifttype']['id']); + $this->assertEquals($shift->shiftType->id, $data['shifttype']['id']); + + $this->assertArrayHasKey('days', $data); + $this->assertArrayHasKey('selected_day', $data); + $day = $shift->start->format('Y-m-d'); + $this->assertEquals([$day], $data['days']->toArray()); + $this->assertEquals($shift->start->format('Y-m-d'), $data['selected_day']); + + $this->assertArrayHasKey('shifts_active', $data); + $this->assertFalse($data['shifts_active']); + + $this->assertArrayHasKey('shifts', $data); + $this->assertEquals($shift->id, $data['shifts']->first()->id); + return $this->response; }); @@ -136,7 +157,7 @@ public function testSave(): void $controller->save($this->request); - $this->assertTrue($this->log->hasInfoThatContains('Updated shift type')); + $this->assertTrue($this->log->hasInfoThatContains('Saved shift type')); $this->assertHasNotification('shifttype.edit.success'); $this->assertCount(1, ShiftType::whereName('Test shift type')->get()); $this->assertCount(1, ShiftType::whereDescription('Something')->get()); diff --git a/tests/Unit/Controllers/Admin/TagControllerTest.php b/tests/Unit/Controllers/Admin/TagControllerTest.php new file mode 100644 index 000000000..6a6148771 --- /dev/null +++ b/tests/Unit/Controllers/Admin/TagControllerTest.php @@ -0,0 +1,168 @@ +app->make(TagController::class); + + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function ($view, $data) { + $this->assertEquals('pages/tag/index.twig', $view); + + $this->assertNotEmpty($data['items']); + + return $this->response; + }); + + $controller->list(); + } + + /** + * @covers \Engelsystem\Controllers\Admin\TagController::edit + * @covers \Engelsystem\Controllers\Admin\TagController::showEdit + */ + public function testEdit(): void + { + $this->request->attributes->set('tag_id', 1); + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function ($view, $data) { + $this->assertEquals('pages/tag/edit.twig', $view); + + $this->assertNotEmpty($data['tag']); + + return $this->response; + }); + + /** @var TagController $controller */ + $controller = $this->app->make(TagController::class); + + $controller->edit($this->request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\TagController::save + */ + public function testSaveCreateInvalid(): void + { + /** @var TagController $controller */ + $this->expectException(ValidationException::class); + + $controller = $this->app->make(TagController::class); + $controller->setValidator(new Validator()); + $controller->save($this->request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\TagController::save + */ + public function testSaveDuplicate(): void + { + $body = ['name' => 'Lorem']; + + $this->request = $this->request->withParsedBody($body); + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function ($view, $data) { + $this->assertEquals('pages/tag/edit.twig', $view); + + $this->assertNotEmpty($data['tag']); + + return $this->response; + }); + + /** @var TagController $controller */ + $controller = $this->app->make(TagController::class); + $controller->setValidator(new Validator()); + + $controller->save($this->request); + + $this->assertHasNotification('tag.edit.duplicate', NotificationType::ERROR); + $this->assertCount(1, Tag::all()); + } + + /** + * @covers \Engelsystem\Controllers\Admin\TagController::save + */ + public function testSaveCreateEdit(): void + { + $body = ['name' => 'Foo?']; + + $this->request = $this->request->withParsedBody($body); + $this->response->expects($this->once()) + ->method('redirectTo') + ->with('http://localhost/admin/tags') + ->willReturn($this->response); + + /** @var TagController $controller */ + $controller = $this->app->make(TagController::class); + $controller->setValidator(new Validator()); + + $controller->save($this->request); + + $this->assertTrue($this->log->hasInfoThatContains('Saved')); + + /** @var Tag $tag */ + $tag = (new Tag())->find(2); + $this->assertEquals('Foo?', $tag->name); + $this->assertHasNotification('tag.edit.success'); + $this->assertCount(2, Tag::all()); + $this->assertTrue(Tag::whereName('Lorem')->get()->isNotEmpty()); + } + + /** + * @covers \Engelsystem\Controllers\Admin\TagController::save + * @covers \Engelsystem\Controllers\Admin\TagController::delete + */ + public function testSaveDelete(): void + { + $this->request->attributes->set('tag_id', 1); + $this->request = $this->request->withParsedBody([ + 'delete' => '1', + ]); + $this->response->expects($this->once()) + ->method('redirectTo') + ->with('http://localhost/admin/tags') + ->willReturn($this->response); + + /** @var TagController $controller */ + $controller = $this->app->make(TagController::class); + $controller->setValidator(new Validator()); + + $controller->save($this->request); + + $this->assertTrue($this->log->hasInfoThatContains('Deleted')); + + $this->assertHasNotification('tag.delete.success'); + } + + /** + * Setup environment + */ + public function setUp(): void + { + parent::setUp(); + + (new Tag([ + 'name' => 'Lorem', + ]))->save(); + } +} diff --git a/tests/Unit/Controllers/Admin/UserGoodieControllerTest.php b/tests/Unit/Controllers/Admin/UserGoodieControllerTest.php index 32b9b6221..fe650bba1 100644 --- a/tests/Unit/Controllers/Admin/UserGoodieControllerTest.php +++ b/tests/Unit/Controllers/Admin/UserGoodieControllerTest.php @@ -4,6 +4,7 @@ namespace Engelsystem\Test\Unit\Controllers\Admin; +use Carbon\Carbon; use Engelsystem\Config\GoodieType; use Engelsystem\Controllers\Admin\UserGoodieController; use Engelsystem\Helpers\Authenticator; @@ -30,6 +31,7 @@ class UserGoodieControllerTest extends ControllerTest public function testIndex(): void { $this->config->set('goodie_type', GoodieType::Tshirt->value); + $this->config->set('night_shifts', ['enabled' => false]); $request = $this->request->withAttribute('user_id', 1); /** @var Authenticator|MockObject $auth */ $auth = $this->createMock(Authenticator::class); @@ -124,11 +126,11 @@ public function testSaveGoodie(): void ->create(); $auth - ->expects($this->exactly(5)) + ->expects($this->exactly(6)) ->method('can') ->with('admin_arrive') - ->willReturnOnConsecutiveCalls(true, true, false, false, true); - $this->setExpects($redirector, 'back', null, $this->response, $this->exactly(5)); + ->willReturnOnConsecutiveCalls(true, true, false, false, true, true); + $this->setExpects($redirector, 'back', null, $this->response, $this->exactly(6)); $controller = new UserGoodieController( $auth, @@ -190,7 +192,7 @@ public function testSaveGoodie(): void 'arrived' => '1', ]); - $user->state->arrived = false; + $user->state->arrival_date = null; $user->state->save(); $this->assertFalse($user->state->arrived); $controller->saveGoodie($request); @@ -218,6 +220,19 @@ public function testSaveGoodie(): void $controller->saveGoodie($request); $user = User::find(1); $this->assertEquals('XS', $user->personalData->shirt_size); + + // remove arrived + $user->state->arrival_date = Carbon::now(); + $user->state->save(); + $request = $request + ->withParsedBody([ + 'shirt_size' => 'XS', + 'arrived' => '', + ]); + + $controller->saveGoodie($request); + $user = User::find(1); + $this->assertFalse($user->state->arrived); } /** diff --git a/tests/Unit/Controllers/Admin/UserVoucherControllerTest.php b/tests/Unit/Controllers/Admin/UserVoucherControllerTest.php new file mode 100644 index 000000000..1fb31d0d0 --- /dev/null +++ b/tests/Unit/Controllers/Admin/UserVoucherControllerTest.php @@ -0,0 +1,191 @@ +config->set('enable_voucher', false); + $request = $this->request; + $this->expectException(HttpNotFound::class); + $this->controller->editVoucher($request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\UserVoucherController::editVoucher + */ + public function testShowEditVoucherWithUnknownUserIdThrows(): void + { + $request = $this->request->withAttribute('user_id', 1234); + $this->expectException(ModelNotFoundException::class); + $this->controller->editVoucher($request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\UserVoucherController::editVoucher + * @covers \Engelsystem\Controllers\Admin\UserVoucherController::__construct + * + * @uses \Engelsystem\Helpers\UserVouchers::eligibleVoucherCount + */ + public function testShowEditVoucher(): void + { + $request = $this->request->withAttribute('user_id', $this->user->id); + $this->response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function (string $view, array $data) { + $this->assertEquals('admin/user/edit-voucher.twig', $view); + $this->assertEquals($this->user->id, $data['userdata']->id); + $this->assertEquals($this->user->state->got_voucher, $data['gotVoucher']); + $this->assertEquals( + $this->user->state->force_active && config('enable_force_active'), + $data['forceActive'] + ); + $this->assertEquals( + $this->user->state->force_food && config('enable_force_food'), + $data['forceFood'] + ); + $this->assertEquals(UserVouchers::eligibleVoucherCount($this->user), $data['eligibleVoucherCount']); + return $this->response; + }); + $this->controller->editVoucher($request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\UserVoucherController::saveVoucher + */ + public function testSaveVoucherWithUnknownUserIdThrows(): void + { + $request = $this->request->withAttribute('user_id', 1234)->withParsedBody([]); + $this->expectException(ModelNotFoundException::class); + $this->controller->saveVoucher($request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\UserVoucherController::saveVoucher + * + * @dataProvider invalidSaveVoucherParams + */ + public function testSaveVoucherWithInvalidParamsThrows(array $body): void + { + $request = $this->request->withAttribute('user_id', $this->user->id)->withParsedBody($body); + $this->expectException(ValidationException::class); + $this->controller->saveVoucher($request); + } + + /** + * @covers \Engelsystem\Controllers\Admin\UserVoucherController::saveVoucher + */ + public function testSaveVoucher(): void + { + $got_voucher = 4; + $body = ['got_voucher' => $got_voucher]; + $request = $this->request->withAttribute('user_id', $this->user->id)->withParsedBody($body); + $this->setExpects($this->auth, 'user', null, $this->user, $this->any()); + $this->redirect->expects($this->once()) + ->method('to') + ->with('/users?action=view&user_id=' . $this->user->id) + ->willReturn($this->response); + + $this->controller->saveVoucher($request); + + $this->assertHasNotification('voucher.save.success'); + $this->assertTrue($this->log->hasInfoThatContains('vouchers.')); + + $this->assertEquals(4, $this->user->state->got_voucher); + } + + /** + * @covers \Engelsystem\Controllers\Admin\UserVoucherController::saveVoucher + */ + public function testSaveVoucherJsonResponce(): void + { + $got_voucher = 4; + $body = ['got_voucher' => $got_voucher]; + $response_body = [ + 'issued' => $got_voucher, + 'eligible' => $got_voucher + UserVouchers::eligibleVoucherCount($this->user), + 'total' => 4, + ]; + $request = $this->request + ->withAttribute('user_id', $this->user->id) + ->withParsedBody($body) + ->withHeader('accept', 'application/json'); + + $this->setExpects($this->auth, 'user', null, $this->user, $this->any()); + $this->setExpects($this->response, 'withHeader', ['content-type', 'application/json'], $this->response); + $this->setExpects($this->response, 'withContent', [json_encode($response_body)], $this->response); + + $this->controller->saveVoucher($request); + } + + /** + * @return array[] + */ + public function invalidSaveVoucherParams(): array + { + return [ + // missing got_voucher + [[]], + // got_voucher not int + [['got_voucher' => 3.14]], + ]; + } + + /** + * Setup environment + */ + public function setUp(): void + { + parent::setUp(); + $this->config->set('enable_voucher', true); + $this->config->set('voucher_settings', [ + 'initial_vouchers' => 0, + 'shifts_per_voucher' => 0, + 'hours_per_voucher' => 2, + // 'Y-m-d' formatted + 'voucher_start' => null, + ]); + + $this->app->bind('http.urlGenerator', UrlGenerator::class); + + $this->auth = $this->createMock(Authenticator::class); + $this->app->instance(Authenticator::class, $this->auth); + + $this->redirect = $this->createMock(Redirector::class); + $this->app->instance(Redirector::class, $this->redirect); + + $this->user = User::factory()->create(); + $this->setExpects($this->auth, 'user', null, $this->user, $this->any()); + + $this->controller = $this->app->make(UserVoucherController::class); + $this->controller->setValidator(new Validator()); + } +} diff --git a/tests/Unit/Controllers/Admin/UserWorklogControllerTest.php b/tests/Unit/Controllers/Admin/UserWorklogControllerTest.php index ed7467d6a..f8cf7fd05 100644 --- a/tests/Unit/Controllers/Admin/UserWorklogControllerTest.php +++ b/tests/Unit/Controllers/Admin/UserWorklogControllerTest.php @@ -67,7 +67,8 @@ public function testShowAddWorklog(): void $this->assertEquals($this->user->id, $data['userdata']->id); $this->assertEquals(Carbon::today(), $data['work_date']); $this->assertEquals(0, $data['work_hours']); - $this->assertEquals('', $data['comment']); + $this->assertEquals('', $data['description']); + $this->assertFalse($data['night_shift']); $this->assertFalse($data['is_edit']); return $this->response; }); @@ -101,7 +102,8 @@ public function testShowEditWorklog(): void 'user_id' => $this->user->id, 'worked_at' => new Carbon('2022-01-01'), 'hours' => 3.14, - 'comment' => 'a comment', + 'description' => 'a description', + 'night_shift' => true, ])->create(); $request = $this->request @@ -113,7 +115,8 @@ public function testShowEditWorklog(): void $this->assertEquals($this->user->id, $data['userdata']->id); $this->assertEquals(new Carbon('2022-01-01'), $data['work_date']); $this->assertEquals(3.14, $data['work_hours']); - $this->assertEquals('a comment', $data['comment']); + $this->assertEquals('a description', $data['description']); + $this->assertTrue($data['night_shift']); $this->assertTrue($data['is_edit']); return $this->response; }); @@ -148,10 +151,16 @@ public function testSaveWorklogWithInvalidParamsThrows(array $body): void */ public function testSaveNewWorklog(): void { - $work_date = Carbon::today(); + $work_date = Carbon::today()->format('Y-m-d'); $work_hours = 3.14; - $comment = str_repeat('X', 200); - $body = ['work_date' => $work_date, 'work_hours' => $work_hours, 'comment' => $comment]; + $description = str_repeat('X', 200); + $night_shift = true; + $body = [ + 'work_date' => $work_date, + 'work_hours' => $work_hours, + 'description' => $description, + 'night_shift' => $night_shift, + ]; $request = $this->request->withAttribute('user_id', $this->user->id)->withParsedBody($body); $this->setExpects($this->auth, 'user', null, $this->user, $this->any()); $this->redirect->expects($this->once()) @@ -162,14 +171,15 @@ public function testSaveNewWorklog(): void $this->controller->saveWorklog($request); $this->assertHasNotification('worklog.add.success'); - $this->assertTrue($this->log->hasInfoThatContains('Added worklog for')); + $this->assertTrue($this->log->hasInfoThatContains('Saved worklog')); $this->assertEquals(1, $this->user->worklogs->count()); $new_worklog = $this->user->worklogs[0]; $this->assertEquals($this->user->id, $new_worklog->user->id); - $this->assertEquals($work_date, $new_worklog->worked_at); + $this->assertEquals($work_date, $new_worklog->worked_at->format('Y-m-d')); $this->assertEquals($work_hours, $new_worklog->hours); - $this->assertEquals($comment, $new_worklog->comment); + $this->assertEquals($description, $new_worklog->description); + $this->assertEquals($night_shift, $new_worklog->night_shift); } /** @@ -178,7 +188,11 @@ public function testSaveNewWorklog(): void */ public function testOverwriteWorklogWithUnknownWorklogIdThrows(): void { - $body = ['work_date' => Carbon::today(), 'work_hours' => 3.14, 'comment' => 'a comment']; + $body = [ + 'work_date' => Carbon::today()->format('Y-m-d'), + 'work_hours' => 3.14, + 'description' => 'a description', + ]; $request = $this->request ->withAttribute('user_id', $this->user->id) ->withAttribute('worklog_id', 1234) @@ -197,7 +211,11 @@ public function testOverwriteWorklogWithWorklogNotAssociatedToUserThrows(): void /** @var Worklog $worklog */ $worklog = Worklog::factory(['user_id' => $user2->id])->create(); - $body = ['work_date' => Carbon::today(), 'work_hours' => 3.14, 'comment' => 'a comment']; + $body = [ + 'work_date' => Carbon::today()->format('Y-m-d'), + 'work_hours' => 3.14, + 'description' => 'a description', + ]; $request = $this->request ->withAttribute('user_id', $this->user->id) ->withAttribute('worklog_id', $worklog->id) @@ -213,10 +231,16 @@ public function testOverwriteWorklog(): void { /** @var Worklog $worklog */ $worklog = Worklog::factory(['user_id' => $this->user->id])->create(); - $work_date = Carbon::today(); + $work_date = Carbon::today()->format('Y-m-d'); $work_hours = 3.14; - $comment = str_repeat('X', 200); - $body = ['work_date' => $work_date, 'work_hours' => $work_hours, 'comment' => $comment]; + $description = str_repeat('X', 200); + $night_shift = true; + $body = [ + 'work_date' => $work_date, + 'work_hours' => $work_hours, + 'description' => $description, + 'night_shift' => $night_shift, + ]; $request = $this->request ->withAttribute('user_id', $this->user->id) @@ -232,9 +256,10 @@ public function testOverwriteWorklog(): void $this->assertHasNotification('worklog.edit.success'); $worklog = Worklog::find($worklog->id); - $this->assertEquals($work_date, $worklog->worked_at); + $this->assertEquals($work_date, $worklog->worked_at->format('Y-m-d')); $this->assertEquals($work_hours, $worklog->hours); - $this->assertEquals($comment, $worklog->comment); + $this->assertEquals($description, $worklog->description); + $this->assertEquals($night_shift, $worklog->night_shift); } /** @@ -334,18 +359,18 @@ public function testDeleteWorklog(): void */ public function invalidSaveWorklogParams(): array { - $today = Carbon::today(); + $today = Carbon::today()->format('Y-m-d'); return [ // missing work_date - [['work_hours' => 3.14, 'comment' => 'com']], + [['work_hours' => 3.14, 'description' => 'com']], // missing work_hours - [['work_date' => $today, 'comment' => 'com']], - // missing comment + [['work_date' => $today, 'description' => 'com']], + // missing description [['work_date' => $today, 'work_hours' => 3.14]], // too low work_hours - [['work_date' => $today, 'work_hours' => -.1, 'comment' => 'com']], + [['work_date' => $today, 'work_hours' => -.1, 'description' => 'com']], // too low work_hours - [['work_date' => $today, 'work_hours' => 3.14, 'comment' => str_repeat('X', 201)]], + [['work_date' => $today, 'work_hours' => 3.14, 'description' => str_repeat('X', 201)]], ]; } diff --git a/tests/Unit/Controllers/AngelTypesControllerTest.php b/tests/Unit/Controllers/AngelTypesControllerTest.php index 5c89bd5e6..a01cd1fd8 100644 --- a/tests/Unit/Controllers/AngelTypesControllerTest.php +++ b/tests/Unit/Controllers/AngelTypesControllerTest.php @@ -4,13 +4,56 @@ namespace Engelsystem\Test\Unit\Controllers; +use Engelsystem\Config\Config; use Engelsystem\Controllers\AngelTypesController; +use Engelsystem\Helpers\Authenticator; +use Engelsystem\Helpers\Carbon; +use Engelsystem\Http\Exceptions\HttpNotFound; +use Engelsystem\Http\Redirector; +use Engelsystem\Http\Request; use Engelsystem\Http\Response; -use Engelsystem\Test\Unit\TestCase; +use Engelsystem\Http\UrlGenerator; +use Engelsystem\Http\Validation\Validator; +use Engelsystem\Models\AngelType; +use Engelsystem\Models\User\User; +use Engelsystem\Test\Unit\HasDatabase; +use Firebase\JWT\JWT; +use Firebase\JWT\Key; +use Illuminate\Database\Eloquent\ModelNotFoundException; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\NullLogger; +use Psr\Log\Test\TestLogger; -class AngelTypesControllerTest extends TestCase +class AngelTypesControllerTest extends ControllerTest { + use HasDatabase; + + /** + * @covers \Engelsystem\Controllers\AngelTypesController::hasPermission + */ + public function testHasPermission(): void + { + /** @var Response|MockObject $response */ + $response = $this->createMock(Response::class); + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + /** @var AngelType $angelType */ + $angelType = AngelType::factory()->create(); + $request = (new Request())->withAttribute('angel_type_id', $angelType->id); + $user = User::factory()->create(); + + $controller = new AngelTypesController($response, $this->app->get(Config::class), $auth, new NullLogger()); + $this->assertFalse($controller->hasPermission($request, 'qrCode')); + $this->assertFalse($controller->hasPermission($request, 'join')); + $this->assertNull($controller->hasPermission($request, 'about')); + + $this->setExpects($auth, 'user', [], $user, $this->atLeastOnce()); + $this->assertTrue($controller->hasPermission($request, 'join')); + + $this->setExpects($auth, 'can', ['admin_user_angeltypes'], true); + $this->assertTrue($controller->hasPermission($request, 'qrCode')); + } + /** * @covers \Engelsystem\Controllers\AngelTypesController::__construct * @covers \Engelsystem\Controllers\AngelTypesController::about @@ -19,6 +62,8 @@ public function testIndex(): void { /** @var Response|MockObject $response */ $response = $this->createMock(Response::class); + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); $this->setExpects( $response, @@ -26,7 +71,240 @@ public function testIndex(): void ['pages/angeltypes/about'] ); - $controller = new AngelTypesController($response); + $controller = new AngelTypesController($response, new Config(), $auth, new NullLogger()); $controller->about(); } + + /** + * @covers \Engelsystem\Controllers\AngelTypesController::qrCode + */ + public function testQrCode(): void + { + /** @var Response|MockObject $response */ + $response = $this->createMock(Response::class); + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + /** @var AngelType $angelType */ + $angelType = AngelType::factory()->create(); + $request = (new Request())->withAttribute('angel_type_id', $angelType->id); + + $this->setExpects($response, 'withInput', [], $response); + $this->setExpects($response, 'withView', ['pages/angeltypes/qr'], $response); + + $controller = new AngelTypesController($response, $this->app->get(Config::class), $auth, new NullLogger()); + $controller->qrCode($request); + } + + /** + * @covers \Engelsystem\Controllers\AngelTypesController::qrCode + * @covers \Engelsystem\Controllers\AngelTypesController::join + * @covers \Engelsystem\Controllers\AngelTypesController::qrJoinEnabled + * @covers \Engelsystem\Controllers\AngelTypesController::getAngelType + */ + public function testQrCodePost(): void + { + /** @var Response|MockObject $response */ + $response = $this->createMock(Response::class); + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + /** @var UrlGenerator|MockObject $urlGenerator */ + $urlGenerator = $this->createMock(UrlGenerator::class); + $this->app->instance('http.urlGenerator', $urlGenerator); + /** @var User $user */ + $user = User::factory()->create(); + /** @var User $user2 */ + $user2 = User::factory()->create(); + /** @var AngelType $angelType */ + $angelType = AngelType::factory()->create(); + /** @var Redirector $redirect */ + $redirect = $this->createMock(Redirector::class); + $this->app->instance('redirect', $redirect); + $log = new TestLogger(); + $request = (new Request()) + ->withAttribute('angel_type_id', $angelType->id) + ->withMethod('post') + ->withParsedBody([ + 'minutes' => 42, + ]); + + $token = null; + $urlGenerator->expects($this->exactly(2)) + ->method('to') + ->willReturnCallback(function ($path, $parameters) use ($angelType, $user, &$token) { + if ($path === '/angeltypes') { + return '/angeltypes..'; + } + + $this->assertEquals('/angeltypes/' . $angelType->id . '/join', $path); + $this->assertArrayHasKey('token', $parameters); + $token = $parameters['token']; + $data = (array) JWT::decode( + $token, + new Key($this->config->get('app_key'), $this->config->get('jwt_algorithm')) + ); + $this->assertArrayHasKey('sub', $data); + $this->assertEquals('join_angel_type', $data['sub']); + $this->assertArrayHasKey('iat', $data); + $this->assertArrayHasKey('exp', $data); + $this->assertArrayHasKey('id', $data); + $this->assertArrayHasKey('jti', $data); + $this->assertEquals($angelType->id, $data['id']); + $this->assertArrayHasKey('by', $data); + $this->assertEquals($user->id, $data['by']); + return '/url..'; + }); + $auth->expects($this->exactly(2)) + ->method('user') + ->willReturnOnConsecutiveCalls($user, $user2); + $this->setExpects($response, 'withInput', [], $response); + $response->expects($this->once()) + ->method('withView') + ->willReturnCallback(function ($view, $data) use ($response) { + $this->assertEquals('pages/angeltypes/qr', $view); + $this->assertArrayHasKey('angel_type', $data); + $this->assertArrayHasKey('qr_data', $data); + $this->assertEquals('/url..', $data['qr_data']); + $this->assertArrayHasKey('qr_max_expiration_minutes', $data); + $this->assertEquals(60 * 24 * 5, $data['qr_max_expiration_minutes']); + return $response; + }); + $this->setExpects($redirect, 'to', ['/angeltypes..'], $response); + + $controller = new AngelTypesController($response, $this->config, $auth, $log); + $controller->setValidator(new Validator()); + + $controller->qrCode($request); + + $request = (new Request(['token' => $token])) + ->withAttribute('angel_type_id', $angelType->id); + $controller->join($request); + + $this->assertTrue($log->hasInfoThatContains('Joined angel type')); + $this->assertHasNotification('angeltype.add.success'); + + /** @var AngelType $userAngelType */ + $userAngelType = $user2->userAngelTypes->first(); + $this->assertNotEmpty($userAngelType); + $this->assertEquals($angelType->id, $userAngelType->id); + $this->assertEquals($user->id, $userAngelType->pivot->confirm_user_id); + } + + /** + * @covers \Engelsystem\Controllers\AngelTypesController::join + */ + public function testJoinDecodeError(): void + { + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + /** @var AngelType $angelType */ + $angelType = AngelType::factory()->create(); + + $controller = new AngelTypesController(new Response(), $this->config, $auth, new NullLogger()); + $controller->setValidator(new Validator()); + + $request = (new Request(['token' => 'some.test.code'])) + ->withAttribute('angel_type_id', $angelType->id); + + $this->expectException(HttpNotFound::class); + $controller->join($request); + } + + /** + * @covers \Engelsystem\Controllers\AngelTypesController::join + */ + public function testJoinDecodeExpired(): void + { + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + /** @var AngelType $angelType */ + $angelType = AngelType::factory()->create(); + $token = JWT::encode( + ['exp' => Carbon::now()->subMinute()->timestamp], + $this->config->get('app_key'), + $this->config->get('jwt_algorithm'), + ); + + $controller = new AngelTypesController(new Response(), $this->config, $auth, new NullLogger()); + $controller->setValidator(new Validator()); + + $request = (new Request(['token' => $token])) + ->withAttribute('angel_type_id', $angelType->id); + + $this->expectException(HttpNotFound::class); + $controller->join($request); + } + + /** + * @covers \Engelsystem\Controllers\AngelTypesController::join + */ + public function testJoinTokenMismatch(): void + { + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + /** @var AngelType $angelType */ + $angelType = AngelType::factory()->create(); + $token = JWT::encode( + ['sub' => 'do_something_different'], + $this->config->get('app_key'), + $this->config->get('jwt_algorithm'), + ); + + $controller = new AngelTypesController(new Response(), $this->config, $auth, new NullLogger()); + $controller->setValidator(new Validator()); + + $request = (new Request(['token' => $token])) + ->withAttribute('angel_type_id', $angelType->id); + + $this->expectException(HttpNotFound::class); + $controller->join($request); + } + + /** + * @covers \Engelsystem\Controllers\AngelTypesController::getAngelType + */ + public function testGetAngelTypeNotFound(): void + { + /** @var Response|MockObject $response */ + $response = $this->createMock(Response::class); + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + /** @var Request|MockObject $request */ + $request = $this->createMock(Request::class); + + $controller = new AngelTypesController($response, $this->app->get(Config::class), $auth, new NullLogger()); + + $this->expectException(ModelNotFoundException::class); + $controller->qrCode($request); + } + + /** + * @covers \Engelsystem\Controllers\AngelTypesController::qrJoinEnabled + */ + public function testQrJoinEnabledDisabled(): void + { + /** @var Response|MockObject $response */ + $response = $this->createMock(Response::class); + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + /** @var Request|MockObject $request */ + $request = $this->createMock(Request::class); + $this->config->set('join_qr_code', false); + + $controller = new AngelTypesController($response, $this->config, $auth, new NullLogger()); + + $this->expectException(HttpNotFound::class); + $controller->qrCode($request); + } + + public function setUp(): void + { + parent::setUp(); + $this->initDatabase(); + + $this->config->set([ + 'app_key' => 'S0me5ecUreTes1K3y', + 'jwt_algorithm' => 'HS256', + 'jwt_expiration_time' => 60 * 24 * 5, + ]); + } } diff --git a/tests/Unit/Controllers/Api/AngelTypeControllerTest.php b/tests/Unit/Controllers/Api/AngelTypeControllerTest.php index 12a1f4865..2d79451d4 100644 --- a/tests/Unit/Controllers/Api/AngelTypeControllerTest.php +++ b/tests/Unit/Controllers/Api/AngelTypeControllerTest.php @@ -63,7 +63,7 @@ public function testOfUser(): void $this->assertArrayHasKey('data', $data); $this->assertCount(3, $data['data']); $this->assertCount(1, collect($data['data'])->filter(function ($item) use ($items) { - return $item['name'] == $items->first()->angelType->name; + return $item['angeltype']['id'] == $items->first()->angelType->id; })); } diff --git a/tests/Unit/Controllers/Api/ApiBaseControllerTest.php b/tests/Unit/Controllers/Api/ApiBaseControllerTest.php index 96c705a67..132cde199 100644 --- a/tests/Unit/Controllers/Api/ApiBaseControllerTest.php +++ b/tests/Unit/Controllers/Api/ApiBaseControllerTest.php @@ -4,8 +4,10 @@ namespace Engelsystem\Test\Unit\Controllers\Api; +use Engelsystem\Helpers\Uuid; use Engelsystem\Http\UrlGeneratorInterface; use Engelsystem\Test\Unit\Controllers\ControllerTest as TestCase; +use Illuminate\Support\Str; use League\OpenAPIValidation\PSR7\Exception\Validation\InvalidBody; use League\OpenAPIValidation\PSR7\Exception\ValidationFailed; use League\OpenAPIValidation\PSR7\OperationAddress as OpenApiAddress; @@ -48,5 +50,7 @@ public function setUp(): void return $path . ($query ? '?' . $query : ''); }); $this->app->instance('http.urlGenerator', $url); + + Str::createUuidsUsing(Uuid::class . '::uuid'); } } diff --git a/tests/Unit/Controllers/Api/ShiftsControllerTest.php b/tests/Unit/Controllers/Api/ShiftsControllerTest.php index 1d2788f11..4372ff21a 100644 --- a/tests/Unit/Controllers/Api/ShiftsControllerTest.php +++ b/tests/Unit/Controllers/Api/ShiftsControllerTest.php @@ -20,6 +20,7 @@ use Engelsystem\Models\User\PersonalData; use Engelsystem\Models\User\User; use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Support\Str; class ShiftsControllerTest extends ApiBaseControllerTest { @@ -88,7 +89,6 @@ public function testEntriesByAngelType(): void { /** @var ShiftEntry $firstEntry */ $firstEntry = $this->shiftB->shiftEntries->first(); - $request = new Request(); $request = $request->withAttribute('angeltype_id', $firstEntry->angelType->id); @@ -241,9 +241,17 @@ public function setUp(): void 'location_id' => $this->location->id, ]); - (new ScheduleShift(['shift_id' => $this->shiftB->id, 'schedule_id' => $this->schedule1->id, 'guid' => 'a'])) + (new ScheduleShift([ + 'shift_id' => $this->shiftB->id, + 'schedule_id' => $this->schedule1->id, + 'guid' => Str::uuid(), + ])) ->save(); - (new ScheduleShift(['shift_id' => $this->shiftC->id, 'schedule_id' => $this->schedule2->id, 'guid' => 'b'])) + (new ScheduleShift([ + 'shift_id' => $this->shiftC->id, + 'schedule_id' => $this->schedule2->id, + 'guid' => Str::uuid(), + ])) ->save(); // "Empty" entry to be skipped @@ -279,16 +287,18 @@ public function setUp(): void ShiftEntry::factory(2)->create([ 'shift_id' => $this->shiftB->id, 'angel_type_id' => $byLocation->angel_type_id, + 'freeloaded_by' => null, ]); // By shift type via schedule ShiftEntry::factory(3)->create([ 'shift_id' => $this->shiftC->id, 'angel_type_id' => $byShiftType->angel_type_id, + 'freeloaded_by' => null, ]); // Additional (not required by shift nor location) - ShiftEntry::factory(5)->create(['shift_id' => $this->shiftA->id]); + ShiftEntry::factory(5)->create(['shift_id' => $this->shiftA->id, 'freeloaded_by' => null]); foreach (User::all() as $user) { // Generate user data diff --git a/tests/Unit/Controllers/Api/UsersControllerTest.php b/tests/Unit/Controllers/Api/UsersControllerTest.php index b5a6554f7..4e8728eb9 100644 --- a/tests/Unit/Controllers/Api/UsersControllerTest.php +++ b/tests/Unit/Controllers/Api/UsersControllerTest.php @@ -8,15 +8,45 @@ use Engelsystem\Helpers\Authenticator; use Engelsystem\Http\Request; use Engelsystem\Http\Response; +use Engelsystem\Models\AngelType; use Engelsystem\Models\User\Contact; use Engelsystem\Models\User\PersonalData; use Engelsystem\Models\User\Settings; use Engelsystem\Models\User\State; use Engelsystem\Models\User\User; +use Engelsystem\Models\Worklog; use PHPUnit\Framework\MockObject\MockObject; class UsersControllerTest extends ApiBaseControllerTest { + /** + * @covers \Engelsystem\Controllers\Api\UsersController::index + */ + public function testIndex(): void + { + /** @var User $user */ + $user = User::factory()->create(); + $controller = new UsersController(new Response()); + + $response = $controller->index(); + $this->validateApiResponse('/users', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + + $this->assertArrayHasKey('data', $data); + $this->assertIsArray($data['data']); + $this->assertNotEmpty($data['data']); + + $firstUser = $data['data'][0]; + $this->assertArrayHasKey('id', $firstUser); + $this->assertEquals($user->id, $firstUser['id']); + $this->assertArrayHasKey('name', $firstUser); + $this->assertEquals($user->name, $firstUser['name']); + } + /** * @covers \Engelsystem\Controllers\Api\UsersController::user * @covers \Engelsystem\Controllers\Api\Resources\UserDetailResource::toArray @@ -89,4 +119,73 @@ public function testUserById(): void $this->assertEquals($otherUser->id, $data['data']['id']); $this->assertArrayNotHasKey('dates', $data['data']); } + + /** + * @covers \Engelsystem\Controllers\Api\UsersController::entriesByAngeltype + * @covers \Engelsystem\Controllers\Api\Resources\UserAngelTypeReferenceResource::toArray + */ + public function testEntriesByAngeltype(): void + { + /** @var User $user */ + $user = User::factory()->create(); + $controller = new UsersController(new Response()); + /** @var AngelType $angelType */ + $angelType = AngelType::factory()->create(); + $user->userAngelTypes()->attach($angelType); + $request = new Request([], [], ['angeltype_id' => $angelType->id]); + + $response = $controller->entriesByAngeltype($request); + $this->validateApiResponse('/angeltypes/{id}/users', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + + $this->assertArrayHasKey('data', $data); + $this->assertIsArray($data['data']); + $this->assertNotEmpty($data['data']); + + $firstEntry = $data['data'][0]; + $this->assertArrayHasKey('user', $firstEntry); + $this->assertArrayHasKey('confirmed', $firstEntry); + $this->assertArrayHasKey('supporter', $firstEntry); + + $firstUser = $firstEntry['user']; + $this->assertArrayHasKey('id', $firstUser); + $this->assertEquals($user->id, $firstUser['id']); + } + + /** + * @covers \Engelsystem\Controllers\Api\UsersController::worklogs + * @covers \Engelsystem\Controllers\Api\Resources\WorklogResource::toArray + */ + public function testWorklogs(): void + { + /** @var User $user */ + $user = User::factory()->create(); + $controller = new UsersController(new Response()); + /** @var Worklog $worklog */ + $worklog = Worklog::factory()->create(['user_id' => $user->id, 'hours' => 1.23]); + $request = new Request([], [], ['user_id' => $user->id]); + + $response = $controller->worklogs($request); + $this->validateApiResponse('/users/{id}/worklogs', 'get', $response); + + $this->assertEquals(['application/json'], $response->getHeader('content-type')); + $this->assertJson($response->getContent()); + + $data = json_decode($response->getContent(), true); + + $this->assertArrayHasKey('data', $data); + $this->assertIsArray($data['data']); + $this->assertNotEmpty($data['data']); + + $firstEntry = $data['data'][0]; + $this->assertArrayHasKey('id', $firstEntry); + $this->assertArrayHasKey('description', $firstEntry); + $this->assertArrayHasKey('hours', $firstEntry); + + $this->assertEquals($worklog->hours, $firstEntry['hours']); + } } diff --git a/tests/Unit/Controllers/BaseControllerTest.php b/tests/Unit/Controllers/BaseControllerTest.php index 587059e99..db2c21fbc 100644 --- a/tests/Unit/Controllers/BaseControllerTest.php +++ b/tests/Unit/Controllers/BaseControllerTest.php @@ -4,6 +4,7 @@ namespace Engelsystem\Test\Unit\Controllers; +use Engelsystem\Http\Request; use Engelsystem\Test\Unit\Controllers\Stub\ControllerImplementation; use PHPUnit\Framework\TestCase; @@ -26,4 +27,17 @@ public function testGetPermissions(): void $this->assertTrue(method_exists($controller, 'setValidator')); } + + /** + * @covers \Engelsystem\Controllers\BaseController::hasPermission + */ + public function testHasPermission(): void + { + $request = new Request(); + $controller = new ControllerImplementation(); + + $this->assertTrue($controller->hasPermission($request, 'yay')); + $this->assertNull($controller->hasPermission($request, 'test')); + $this->assertFalse($controller->hasPermission($request, 'nope')); + } } diff --git a/tests/Unit/Controllers/ControllerTest.php b/tests/Unit/Controllers/ControllerTest.php index f6bf84472..0db71f27d 100644 --- a/tests/Unit/Controllers/ControllerTest.php +++ b/tests/Unit/Controllers/ControllerTest.php @@ -51,7 +51,7 @@ protected function assertHasNotification(string $value, NotificationType $type = $this->assertTrue(in_array($value, $messages), 'Has ' . $type->value . ' notification: ' . $value); } - protected function assertHasNoNotifications(NotificationType $type = null): void + protected function assertHasNoNotifications(?NotificationType $type = null): void { $messages = $this->session->get('messages' . ($type ? '.' . $type->value : ''), []); $this->assertEmpty($messages, 'Has no' . ($type ? ' ' . $type->value : '') . ' notification.'); diff --git a/tests/Unit/Controllers/Metrics/ControllerTest.php b/tests/Unit/Controllers/Metrics/ControllerTest.php index 3b44e9e6c..a2537e558 100644 --- a/tests/Unit/Controllers/Metrics/ControllerTest.php +++ b/tests/Unit/Controllers/Metrics/ControllerTest.php @@ -27,6 +27,7 @@ class ControllerTest extends TestCase * @covers \Engelsystem\Controllers\Metrics\Controller::__construct * @covers \Engelsystem\Controllers\Metrics\Controller::metrics * @covers \Engelsystem\Controllers\Metrics\Controller::formatStats + * @covers \Engelsystem\Controllers\Metrics\Controller::checkAuth */ public function testMetrics(): void { @@ -193,52 +194,6 @@ public function testMetrics(): void $controller->metrics(); } - /** - * @covers \Engelsystem\Controllers\Metrics\Controller::checkAuth - * @covers \Engelsystem\Controllers\Metrics\Controller::stats - */ - public function testStats(): void - { - /** @var Response|MockObject $response */ - /** @var Request|MockObject $request */ - /** @var MetricsEngine|MockObject $engine */ - /** @var Stats|MockObject $stats */ - /** @var Config $config */ - /** @var Version|MockObject $version */ - list($response, $request, $engine, $stats, $config, $version) = $this->getMocks(); - - $response->expects($this->once()) - ->method('withHeader') - ->with('Content-Type', 'application/json') - ->willReturn($response); - $response->expects($this->once()) - ->method('withContent') - ->with(json_encode([ - 'user_count' => 20, - 'arrived_user_count' => 10, - 'done_work_hours' => 99, - 'users_in_action' => 5, - ])) - ->willReturn($response); - - $request->expects($this->once()) - ->method('get') - ->with('api_key') - ->willReturn('ApiKey987'); - - $config->set('api_key', 'ApiKey987'); - - $stats->expects($this->once()) - ->method('workSeconds') - ->with(true) - ->willReturn((int) (60 * 60 * 99.47)); - $this->setExpects($stats, 'usersState', null, 10, $this->exactly(3)); - $this->setExpects($stats, 'currentlyWorkingUsers', null, 5); - - $controller = new Controller($response, $engine, $config, $request, $stats, $version); - $controller->stats(); - } - /** * @covers \Engelsystem\Controllers\Metrics\Controller::checkAuth */ @@ -262,8 +217,8 @@ public function testCheckAuth(): void $controller = new Controller($response, $engine, $config, $request, $stats, $version); $this->expectException(HttpForbidden::class); - $this->expectExceptionMessage(json_encode(['error' => 'The api_key is invalid'])); - $controller->stats(); + $this->expectExceptionMessage('The api_key is invalid'); + $controller->metrics(); } protected function getMocks(): array diff --git a/tests/Unit/Controllers/Metrics/StatsTest.php b/tests/Unit/Controllers/Metrics/StatsTest.php index 1c61e6715..b27a6306f 100644 --- a/tests/Unit/Controllers/Metrics/StatsTest.php +++ b/tests/Unit/Controllers/Metrics/StatsTest.php @@ -152,11 +152,11 @@ public function testWorklogSeconds(): void { $this->addUsers(); $worklogData = [ - 'user_id' => 1, - 'creator_id' => 1, - 'hours' => 2.4, - 'comment' => '', - 'worked_at' => new Carbon(), + 'user_id' => 1, + 'creator_id' => 1, + 'hours' => 2.4, + 'description' => '', + 'worked_at' => new Carbon(), ]; (new Worklog($worklogData))->save(); (new Worklog(['hours' => 1.2, 'user_id' => 3] + $worklogData))->save(); @@ -216,9 +216,9 @@ public function testLocations(): void public function testAngelTypes(): void { (new AngelType(['id' => 1, 'name' => 'AngelType 1', 'restricted' => true]))->save(); - (new AngelType(['id' => 2, 'name' => 'Second AngelType']))->save(); + (new AngelType(['id' => 2, 'name' => 'Second AngelType', 'restricted' => false]))->save(); (new AngelType(['id' => 3, 'name' => 'Another AngelType', 'restricted' => true]))->save(); - (new AngelType(['id' => 4, 'name' => 'Old AngelType']))->save(); + (new AngelType(['id' => 4, 'name' => 'Old AngelType', 'restricted' => false]))->save(); UserAngelType::factory()->create(['angel_type_id' => 1, 'confirm_user_id' => 1, 'supporter' => true]); UserAngelType::factory()->create(['angel_type_id' => 1, 'confirm_user_id' => null, 'supporter' => false]); UserAngelType::factory()->create(['angel_type_id' => 1, 'confirm_user_id' => 1, 'supporter' => false]); @@ -396,6 +396,17 @@ public function testForceActiveUsers(): void $this->assertEquals(2, $stats->forceActiveUsers()); } + /** + * @covers \Engelsystem\Controllers\Metrics\Stats::forceFoodUsers + */ + public function testForceFoodUsers(): void + { + $this->addUsers(); + + $stats = new Stats($this->database); + $this->assertEquals(2, $stats->forceFoodUsers()); + } + /** * @covers \Engelsystem\Controllers\Metrics\Stats::usersPronouns */ @@ -570,17 +581,29 @@ protected function addUsers(): void { $this->addUser(); $this->addUser([], ['shirt_size' => 'L'], ['email_human' => true, 'email_shiftinfo' => true]); - $this->addUser(['arrived' => 1], [], ['email_human' => true, 'email_goodie' => true, 'email_news' => true]); - $this->addUser(['arrived' => 1], ['pronoun' => 'unicorn'], ['language' => 'lo_RM', 'email_shiftinfo' => true]); - $this->addUser(['arrived' => 1, 'got_voucher' => 2], ['shirt_size' => 'XXL'], ['language' => 'lo_RM']); $this->addUser( - ['arrived' => 1, 'got_voucher' => 9, 'force_active' => true, 'user_info' => 'Info'], + ['arrival_date' => Carbon::now()], + [], + ['email_human' => true, 'email_goodie' => true, 'email_news' => true] + ); + $this->addUser( + ['arrival_date' => Carbon::now()], + ['pronoun' => 'unicorn'], + ['language' => 'lo_RM', 'email_shiftinfo' => true] + ); + $this->addUser( + ['arrival_date' => Carbon::now(), 'got_voucher' => 2], + ['shirt_size' => 'XXL'], + ['language' => 'lo_RM'] + ); + $this->addUser( + ['arrival_date' => Carbon::now(), 'got_voucher' => 9, 'force_active' => true, 'user_info' => 'Info'], [], ['theme' => 1], ['drive_car' => true, 'drive_12t' => true, 'drive_confirmed' => true, 'ifsg_certificate_light' => true] ); $this->addUser( - ['arrived' => 1, 'got_voucher' => 3], + ['arrival_date' => Carbon::now(), 'got_voucher' => 3, 'force_food' => true], ['pronoun' => 'per'], ['theme' => 1, 'email_human' => true], [ @@ -591,8 +614,12 @@ protected function addUsers(): void 'ifsg_confirmed' => true, ] ); - $this->addUser(['arrived' => 1, 'active' => 1, 'got_goodie' => true, 'force_active' => true]); - $this->addUser(['arrived' => 1, 'active' => 1, 'got_goodie' => true], ['shirt_size' => 'L'], ['theme' => 4]); + $this->addUser(['arrival_date' => Carbon::now(), 'active' => 1, 'got_goodie' => true, 'force_active' => true]); + $this->addUser( + ['arrival_date' => Carbon::now(), 'active' => 1, 'got_goodie' => true, 'force_food' => true], + ['shirt_size' => 'L'], + ['theme' => 4] + ); } protected function addUser( diff --git a/tests/Unit/Controllers/OAuthControllerTest.php b/tests/Unit/Controllers/OAuthControllerTest.php index 413321ff7..4007d5353 100644 --- a/tests/Unit/Controllers/OAuthControllerTest.php +++ b/tests/Unit/Controllers/OAuthControllerTest.php @@ -232,7 +232,7 @@ public function testIndexInvalidState(): void } $this->assertFalse($this->session->has('oauth2_state')); - $this->log->hasWarningThatContains('Invalid'); + $this->assertTrue($this->log->hasWarningThatContains('Invalid')); $this->assertNotNull($exception, 'Exception not thrown'); $this->assertEquals('oauth.invalid-state', $exception->getMessage()); } @@ -241,37 +241,29 @@ public function testIndexInvalidState(): void * @covers \Engelsystem\Controllers\OAuthController::index * @covers \Engelsystem\Controllers\OAuthController::handleOAuthError */ - public function testIndexProviderError(): void + public function testIndexProviderErrorOnResourceInfo(): void { /** @var AccessToken|MockObject $accessToken */ $accessToken = $this->createMock(AccessToken::class); - $thrown = false; /** @var GenericProvider|MockObject $provider */ $provider = $this->createMock(GenericProvider::class); - $provider->expects($this->exactly(2)) - ->method('getAccessToken') - ->with('authorization_code', ['code' => 'lorem-ipsum-code']) - ->willReturnCallback(function () use (&$thrown, $accessToken) { - if (!$thrown) { - $thrown = true; - throw new IdentityProviderException( - 'Oops', - 42, - ['error' => 'some_error', 'error_description' => 'Some kind of error'] - ); - } - - return $accessToken; - }); + $this->setExpects( + $provider, + 'getAccessToken', + ['authorization_code', ['code' => 'lorem-ipsum-code']], + $accessToken + ); $provider->expects($this->once()) ->method('getResourceOwner') ->with($accessToken) - ->willThrowException(new IdentityProviderException( - 'Something\'s wrong!', - 1337, - '500 Internal server error' - )); + ->willReturnCallback(function (): void { + throw new IdentityProviderException( + 'Something\'s wrong!', + 1337, + '500 Internal server error' + ); + }); $this->session->set('oauth2_state', 'some-internal-state'); @@ -281,9 +273,8 @@ public function testIndexProviderError(): void ->withQueryParams(['code' => 'lorem-ipsum-code', 'state' => 'some-internal-state']); $controller = $this->getMock(['getProvider']); - $this->setExpects($controller, 'getProvider', ['testprovider'], $provider, 2); + $this->setExpects($controller, 'getProvider', ['testprovider'], $provider, 1); - // Invalid state $exception = null; try { $controller->index($request); @@ -291,10 +282,42 @@ public function testIndexProviderError(): void $exception = $e; } - $this->log->hasErrorThatContains('Some kind of error'); - $this->log->hasErrorThatContains('some_error'); + $this->assertTrue($this->log->hasErrorThatPasses(function ($rec): bool { + return str_contains($rec['message'], 'identity provider error') + && $rec['context']['error'] == 'Something\'s wrong!'; + })); $this->assertNotNull($exception, 'Exception not thrown'); $this->assertEquals('oauth.provider-error', $exception->getMessage()); + } + + /** + * @covers \Engelsystem\Controllers\OAuthController::index + * @covers \Engelsystem\Controllers\OAuthController::handleOAuthError + */ + public function testIndexProviderErrorIdentityProvider(): void + { + /** @var GenericProvider|MockObject $provider */ + $provider = $this->createMock(GenericProvider::class); + $provider->expects($this->once()) + ->method('getAccessToken') + ->with('authorization_code', ['code' => 'lorem-ipsum-code']) + ->willReturnCallback(function (): void { + throw new IdentityProviderException( + 'Oops', + 42, + ['error' => 'some_error', 'error_description' => 'Some kind of error'] + ); + }); + + $this->session->set('oauth2_state', 'some-internal-state'); + + $request = new Request(); + $request = $request + ->withAttribute('provider', 'testprovider') + ->withQueryParams(['code' => 'lorem-ipsum-code', 'state' => 'some-internal-state']); + + $controller = $this->getMock(['getProvider']); + $this->setExpects($controller, 'getProvider', ['testprovider'], $provider, 1); // Error while getting data $exception = null; @@ -304,7 +327,11 @@ public function testIndexProviderError(): void $exception = $e; } - $this->log->hasErrorThatContains('500'); + $this->assertTrue($this->log->hasErrorThatPasses(function ($rec): bool { + return str_contains($rec['message'], 'identity provider error') + && $rec['context']['error'] == 'Oops' + && str_contains($rec['context']['description'], 'Some kind of error'); + })); $this->assertNotNull($exception, 'Exception not thrown'); $this->assertEquals('oauth.provider-error', $exception->getMessage()); } @@ -355,7 +382,7 @@ public function testIndexAlreadyConnectedToAUser(): void } /** - * @covers \Engelsystem\Controllers\OAuthController::index + * @covers \Engelsystem\Controllers\OAuthController::index * @dataProvider oAuthErrorCodeProvider */ public function testIndexOAuthErrorResponse(string $oauth_error_code): void @@ -364,8 +391,8 @@ public function testIndexOAuthErrorResponse(string $oauth_error_code): void $request = new Request(); $request = $request - ->withAttribute('provider', 'testprovider') - ->withQueryParams(['error' => $oauth_error_code]); + ->withAttribute('provider', 'testprovider') + ->withQueryParams(['error' => $oauth_error_code]); $exception = null; try { @@ -574,20 +601,69 @@ public function testConnect(): void /** * @covers \Engelsystem\Controllers\OAuthController::disconnect */ - public function testDisconnect(): void + public function testDisconnectIfAllowIsTrue(): void { + $oauthConfig = $this->config->get('oauth'); + $oauthConfig['testprovider']['allow_user_disconnect'] = true; + + $this->runDisconnectTest($oauthConfig, true); + } + + /** + * @covers \Engelsystem\Controllers\OAuthController::disconnect + */ + public function testDisconnectIfAllowIsNull(): void + { + $oauthConfig = $this->config->get('oauth'); + $oauthConfig['testprovider']['allow_user_disconnect'] = null; + + $this->runDisconnectTest($oauthConfig, true); + } + + /** + * @covers \Engelsystem\Controllers\OAuthController::disconnect + */ + public function testDisconnectIfAllowIsUnset(): void + { + $oauthConfig = $this->config->get('oauth'); + unset($oauthConfig['testprovider']['allow_user_disconnect']); + + $this->runDisconnectTest($oauthConfig, true); + } + + /** + * @covers \Engelsystem\Controllers\OAuthController::disconnect + */ + public function testDisconnectIfAllowIsFalse(): void + { + $oauthConfig = $this->config->get('oauth'); + $oauthConfig['testprovider']['allow_user_disconnect'] = false; + + $this->runDisconnectTest($oauthConfig, false); + } + + private function runDisconnectTest(mixed $oauthConfig, bool $shouldDisconnect): void + { + $this->config->set('oauth', $oauthConfig); + $controller = $this->getMock(['addNotification']); - $this->setExpects($controller, 'addNotification', ['oauth.disconnected']); + $request = (new Request())->withAttribute('provider', 'testprovider'); - $request = (new Request()) - ->withAttribute('provider', 'testprovider'); + if (!$shouldDisconnect) { + $this->expectException(HttpNotFound::class); + $controller->disconnect($request); + return; // Should never happen, creates cleaner errors + } + + $this->setExpects($controller, 'addNotification', ['oauth.disconnected']); $this->setExpects($this->auth, 'user', null, $this->authenticatedUser); $this->setExpects($this->redirect, 'back', null, new Response()); $controller->disconnect($request); + $this->assertCount(1, OAuth::all()); - $this->log->hasInfoThatContains('Disconnected'); + $this->assertTrue($this->log->hasInfoThatContains('Disconnected')); } protected function getMock(array $mockMethods = []): OAuthController | MockObject diff --git a/tests/Unit/Controllers/SettingsControllerTest.php b/tests/Unit/Controllers/SettingsControllerTest.php index 17ab2dc60..17bff5bef 100644 --- a/tests/Unit/Controllers/SettingsControllerTest.php +++ b/tests/Unit/Controllers/SettingsControllerTest.php @@ -17,6 +17,7 @@ use Engelsystem\Http\UrlGenerator; use Engelsystem\Http\Validation\Validator; use Engelsystem\Models\AngelType; +use Engelsystem\Models\OAuth; use Engelsystem\Models\Session as SessionModel; use Engelsystem\Models\User\License; use Engelsystem\Models\User\Settings; @@ -1000,6 +1001,22 @@ public function testSettingsMenuWithOAuth(): void $this->assertEquals(['title' => 'settings.oauth', 'hidden' => true], $menu['http://localhost/settings/oauth']); } + /** + * @covers \Engelsystem\Controllers\SettingsController::checkOauthHidden + */ + public function testSettingsMenuWithOAuthShownWhenConnected(): void + { + // Provider configured as hidden + $providersHidden = ['foo' => ['lorem' => 'ipsum', 'hidden' => true]]; + config(['oauth' => $providersHidden]); + + OAuth::factory()->create(['provider' => 'foo', 'user_id' => $this->user->id]); + + $menu = $this->controller->settingsMenu(); + $this->assertArrayHasKey('http://localhost/settings/oauth', $menu); + $this->assertEquals(['title' => 'settings.oauth', 'hidden' => false], $menu['http://localhost/settings/oauth']); + } + /** * @covers \Engelsystem\Controllers\SettingsController::settingsMenu */ diff --git a/tests/Unit/Controllers/Stub/ControllerImplementation.php b/tests/Unit/Controllers/Stub/ControllerImplementation.php index c3a6af55c..be95f9e53 100644 --- a/tests/Unit/Controllers/Stub/ControllerImplementation.php +++ b/tests/Unit/Controllers/Stub/ControllerImplementation.php @@ -5,6 +5,7 @@ namespace Engelsystem\Test\Unit\Controllers\Stub; use Engelsystem\Controllers\BaseController; +use Psr\Http\Message\ServerRequestInterface; class ControllerImplementation extends BaseController { @@ -16,4 +17,13 @@ class ControllerImplementation extends BaseController 'dolor', ], ]; + + public function hasPermission(ServerRequestInterface $request, string $method): ?bool + { + return match ($method) { + 'yay' => true, + 'nope' => false, + default => parent::hasPermission($request, $method), + }; + } } diff --git a/tests/Unit/Database/DatabaseServiceProviderTest.php b/tests/Unit/Database/DatabaseServiceProviderTest.php index 15d8b62b4..6100b56f2 100644 --- a/tests/Unit/Database/DatabaseServiceProviderTest.php +++ b/tests/Unit/Database/DatabaseServiceProviderTest.php @@ -36,9 +36,10 @@ public function testRegister(): void ] ); - $app->expects($this->exactly(7)) + $app->expects($this->exactly(8)) ->method('instance') ->withConsecutive( + [PDO::class, $pdo], [CapsuleManager::class, $dbManager], [Db::class, $dbManager], [Connection::class, $connection], diff --git a/tests/Unit/Events/Listener/OAuth2Test.php b/tests/Unit/Events/Listener/OAuth2Test.php index f23d8ad81..7923968bd 100644 --- a/tests/Unit/Events/Listener/OAuth2Test.php +++ b/tests/Unit/Events/Listener/OAuth2Test.php @@ -53,7 +53,7 @@ public function testLogin(): void $this->assertNotNull($test); $this->assertFalse($test->pivot->supporter); $this->assertNull($test->pivot->confirm_user_id); - $this->assertTrue($this->log->hasInfoThatContains('Added to angeltype')); + $this->assertTrue($this->log->hasInfoThatContains('Added to angel type')); /** @var AngelType $lorem */ $lorem = $userAngelTypes->where('pivot.angel_type_id', 42)->first(); diff --git a/tests/Unit/Events/Listener/ShiftsTest.php b/tests/Unit/Events/Listener/ShiftsTest.php index 669d9addd..55c13fa3c 100644 --- a/tests/Unit/Events/Listener/ShiftsTest.php +++ b/tests/Unit/Events/Listener/ShiftsTest.php @@ -53,7 +53,7 @@ public function testDeletingCreateWorklogs(): void $this->assertCount(1, $this->user->worklogs); $this->assertEquals($this->shift->isNightShift() ? 4 : 2, $this->user->worklogs[0]->hours); - $this->assertEquals('Text', $this->user->worklogs[0]->comment); + $this->assertEquals('Text', $this->user->worklogs[0]->description); $this->assertTrue($this->log->hasInfoThatContains('Created worklog entry')); } diff --git a/tests/Unit/Exceptions/HandlerTest.php b/tests/Unit/Exceptions/HandlerTest.php index 767fd2274..d4e91ab5a 100644 --- a/tests/Unit/Exceptions/HandlerTest.php +++ b/tests/Unit/Exceptions/HandlerTest.php @@ -29,21 +29,49 @@ public function testCreate(): void $this->assertEquals(Environment::DEVELOPMENT, $anotherHandler->getEnvironment()); } + public function errorHandlerProvider(): array + { + return [ + // Environment, Error level, should display message or (if false) ignore the output by returning + + [Environment::PRODUCTION, E_RECOVERABLE_ERROR, true], + [Environment::PRODUCTION, E_WARNING, false], + [Environment::PRODUCTION, E_NOTICE, false], + [Environment::PRODUCTION, E_DEPRECATED, false], + [Environment::PRODUCTION, E_COMPILE_ERROR, true], + [Environment::PRODUCTION, E_USER_WARNING, false], + [Environment::PRODUCTION, E_USER_NOTICE, false], + [Environment::PRODUCTION, E_USER_DEPRECATED, false], + + [Environment::DEVELOPMENT, E_RECOVERABLE_ERROR, true], + [Environment::DEVELOPMENT, E_WARNING, true], + [Environment::DEVELOPMENT, E_NOTICE, true], + [Environment::DEVELOPMENT, E_DEPRECATED, true], + [Environment::DEVELOPMENT, E_COMPILE_ERROR, true], + [Environment::DEVELOPMENT, E_USER_WARNING, true], + [Environment::DEVELOPMENT, E_USER_NOTICE, true], + [Environment::DEVELOPMENT, E_USER_DEPRECATED, true], + ]; + } + /** * @covers \Engelsystem\Exceptions\Handler::errorHandler() + * @dataProvider errorHandlerProvider */ - public function testErrorHandler(): void + public function testErrorHandler(Environment $env, int $level, bool $showError): void { /** @var Handler|MockObject $handler */ $handler = $this->getMockBuilder(Handler::class) + ->setConstructorArgs([$env]) ->onlyMethods(['exceptionHandler']) ->getMock(); $handler->expects($this->once()) ->method('exceptionHandler') - ->with($this->isInstanceOf(ErrorException::class)); + ->with($this->isInstanceOf(ErrorException::class), $showError); - $handler->errorHandler(1, 'Foo and bar!', '/lo/rem.php', 123); + $return = $handler->errorHandler($level, 'Foo and bar!', '/lo/rem.php', 123); + $this->assertEquals($showError, $return); } /** @@ -78,7 +106,7 @@ public function testExceptionHandler(): void $this->expectOutputString($errorMessage); $handler->exceptionHandler($exception); - $return = $handler->exceptionHandler($exception, true); + $return = $handler->exceptionHandler($exception, false); $this->assertEquals($errorMessage, $return); } diff --git a/tests/Unit/Exceptions/Handlers/LegacyTest.php b/tests/Unit/Exceptions/Handlers/LegacyTest.php index 46e05968b..7217237a7 100644 --- a/tests/Unit/Exceptions/Handlers/LegacyTest.php +++ b/tests/Unit/Exceptions/Handlers/LegacyTest.php @@ -54,7 +54,7 @@ public function testReport(): void $logger = new TestLogger(); $logger2 = $this->createMock(TestLogger::class); $logger2->expects($this->once()) - ->method('critical') + ->method('log') ->willReturnCallback(function (): void { throw new ErrorException(); }); diff --git a/tests/Unit/Factories/UserTest.php b/tests/Unit/Factories/UserTest.php index 07a55f994..2f1895aa1 100644 --- a/tests/Unit/Factories/UserTest.php +++ b/tests/Unit/Factories/UserTest.php @@ -174,7 +174,7 @@ public function testMaximumConfigInvalid(): void 'email' => 'notanemail', 'password' => 'a', 'tshirt_size' => 'A', - 'planned_arrival_date' => $this->now->subDays(7), + 'planned_arrival_date' => $this->now->subDays(7)->format('Y-m-d'), 'dect' => str_repeat('a', 50), 'mobile' => str_repeat('a', 50), ], @@ -413,7 +413,7 @@ public function testBuildUpAndTearDownDates(): void 'email' => 'fritz@example.com', 'password' => 's3cret', 'password_confirmation' => 's3cret', - 'planned_arrival_date' => $this->now->subDays(7), + 'planned_arrival_date' => $this->now->subDays(7)->format('Y-m-d'), ], [ 'planned_arrival_date' => [ diff --git a/tests/Unit/HasDatabase.php b/tests/Unit/HasDatabase.php index 787ffdb30..f81483126 100644 --- a/tests/Unit/HasDatabase.php +++ b/tests/Unit/HasDatabase.php @@ -18,7 +18,7 @@ trait HasDatabase protected Database $database; /** - * Setup in memory database + * Setup in memory database, cache migrated state between tests */ protected function initDatabase(): void { @@ -36,6 +36,17 @@ protected function initDatabase(): void $this->app->instance(ServerRequestInterface::class, new Request()); + $this->restoreDatabase($connection); + + if (!$connection->getSchemaBuilder()->hasTable('migrations')) { + $this->runMigration(); + + $this->storeDatabase($connection); + } + } + + protected function runMigration(): void + { /** @var Migrate $migration */ $migration = $this->app->get('db.migration'); $migration->initMigration(); @@ -46,7 +57,7 @@ protected function initDatabase(): void ->insert( [ // Migrations that can be skipped as they only use legacy tables - // or only change data not available/relevant in migrations + // or only change data not available/relevant in test migrations ['migration' => '2018_01_01_000001_import_install_sql'], ['migration' => '2018_01_01_000002_import_update_sql'], ['migration' => '2018_01_01_000003_fix_old_tables'], @@ -68,9 +79,61 @@ protected function initDatabase(): void ['migration' => '2022_10_21_000000_add_hide_register_to_angeltypes'], ['migration' => '2022_11_06_000000_shifttype_remove_angeltype'], ['migration' => '2023_05_21_000001_cleanup_short_api_keys'], + ['migration' => '2025_12_12_000000_change_oauth_identifier_database_encoding_to_bin'], ] ); $migration->run(__DIR__ . '/../../db/migrations'); } + + protected function storeDatabase(Connection $connection): void + { + $schema = $connection->getSchemaBuilder(); + $dbState = []; + foreach ($schema->getTables() as $table) { + // Get table structure + $name = $table['name']; + $sql = $connection + ->table('sqlite_master') + ->where('name', $name) + ->first() + ->sql; + + // Save database content + $rows = []; + $data = $connection + ->table($name) + ->get(); + foreach ($data as $row) { + $rows[] = (array) $row; + } + + $dbState[$name] = [ + 'name' => $name, + 'info' => $table, + 'sql' => $sql, + 'rows' => $rows, + ]; + } + + RuntimeTest::$dbState = $dbState; + } + + protected function restoreDatabase(Connection $connection): void + { + // Create tables + foreach (RuntimeTest::$dbState as $table) { + $connection->statement($table['sql']); + } + + // Restore data + $schema = $connection->getSchemaBuilder(); + $schema->disableForeignKeyConstraints(); + foreach (RuntimeTest::$dbState as $table) { + $connection + ->table($table['name']) + ->insert($table['rows']); + } + $schema->enableForeignKeyConstraints(); + } } diff --git a/tests/Unit/Helpers/CacheServiceProviderTest.php b/tests/Unit/Helpers/CacheServiceProviderTest.php new file mode 100644 index 000000000..0077eae4d --- /dev/null +++ b/tests/Unit/Helpers/CacheServiceProviderTest.php @@ -0,0 +1,31 @@ +instance('path.cache', '/tmp'); + + $serviceProvider = new CacheServiceProvider($app); + $serviceProvider->register(); + + $this->assertTrue($app->bound('cache')); + $this->assertArrayHasKey(Cache::class, $app->contextual); + + $cache = $app->get(Cache::class); + $this->assertInstanceOf(Cache::class, $cache); + } +} diff --git a/tests/Unit/Helpers/CacheTest.php b/tests/Unit/Helpers/CacheTest.php new file mode 100644 index 000000000..d82af5e92 --- /dev/null +++ b/tests/Unit/Helpers/CacheTest.php @@ -0,0 +1,119 @@ +cacheDir); + $value = new stdClass(); + + $this->assertNull($cache->get('this-is-not-there')); + $this->assertEquals('', $cache->get('give-me-my-string', '')); + $this->assertEquals(42, $cache->get('meaning-of-life', 42)); + $this->assertEquals(['key' => 'value'], $cache->get('array-data', ['key' => 'value'])); + $this->assertEquals($value, $cache->get('some-object', $value)); + + $this->assertCount(0, $this->listCacheFiles()); + } + + /** + * @covers \Engelsystem\Helpers\Cache::get + */ + public function testGetDefaultCallback(): void + { + $cache = new Cache($this->cacheDir); + + $this->assertEquals('some test', $cache->get('this-has-a-callback', fn() => 'some test')); + $this->assertEquals('some test', $cache->get('this-has-a-callback', 'unused-default-value')); + + $cacheFile = $this->cacheDir . '/this-has-a-callback.cache'; + $this->assertFileExists($cacheFile); + $this->assertEquals(serialize('some test'), file_get_contents($cacheFile)); + + $this->assertCount(1, $this->listCacheFiles()); + } + + /** + * @covers \Engelsystem\Helpers\Cache::get + * @covers \Engelsystem\Helpers\Cache::cacheFilePath + */ + public function testGetReadFile(): void + { + $cache = new Cache($this->cacheDir); + + $cacheFile = $this->cacheDir . '/cached.cache'; + file_put_contents($cacheFile, serialize('cached-value')); + + $this->assertEquals('cached-value', $cache->get('cached')); + } + + /** + * @covers \Engelsystem\Helpers\Cache::forget + */ + public function testForgetNotExisting(): void + { + $cache = new Cache($this->cacheDir); + $cache->forget('not-cached-value'); + + $this->assertCount(0, $this->listCacheFiles()); + } + + /** + * @covers \Engelsystem\Helpers\Cache::forget + * @covers \Engelsystem\Helpers\Cache::cacheFilePath + */ + public function testForget(): void + { + $cache = new Cache($this->cacheDir); + + $cacheFile = $this->cacheDir . '/rm-cache.cache'; + file_put_contents($cacheFile, serialize('removed-cache-value')); + + $cache->forget('rm-cache'); + $this->assertNull($cache->get('rm-cache')); + $this->assertFileDoesNotExist($cacheFile); + } + + /** + * @covers \Engelsystem\Helpers\Cache::get + * @covers \Engelsystem\Helpers\Cache::forget + * @covers \Engelsystem\Helpers\Cache::cacheFilePath + */ + public function testGetForgetOld(): void + { + $cache = new Cache($this->cacheDir); + + $cacheFile = $this->cacheDir . '/cached.cache'; + file_put_contents($cacheFile, serialize('cached-value')); + touch($cacheFile, time() - 60 * 60 * 2); + + $this->assertEquals('default', $cache->get('cached', 'default')); + $this->assertFileDoesNotExist($cacheFile); + } + + public function tearDown(): void + { + foreach ($this->listCacheFiles() as $file) { + unlink($this->cacheDir . '/' . $file); + } + } + + protected function listCacheFiles(): array + { + return array_diff(scandir($this->cacheDir), ['..', '.', '.gitignore']); + } +} diff --git a/tests/Unit/Helpers/CarbonTest.php b/tests/Unit/Helpers/CarbonTest.php index 3566dd814..bb01e61ef 100644 --- a/tests/Unit/Helpers/CarbonTest.php +++ b/tests/Unit/Helpers/CarbonTest.php @@ -45,24 +45,4 @@ public function testCreateFromInvalidDatetime(string $value): void $date = Carbon::createFromDatetime($value); self::assertNull($date); } - - /** - * @covers \Engelsystem\Helpers\Carbon::createTimestampFromDatetime - * @dataProvider validDates - */ - public function testCreateTimestampFromValidDatetime(string $value, Carbon $expected): void - { - $timestamp = Carbon::createTimestampFromDatetime($value); - self::assertSame($expected->timestamp, $timestamp); - } - - /** - * @covers \Engelsystem\Helpers\Carbon::createTimestampFromDatetime - * @dataProvider invalidDates - */ - public function testCreateTimestampFromInvalidDatetime(string $value): void - { - $timestamp = Carbon::createTimestampFromDatetime($value); - self::assertNull($timestamp); - } } diff --git a/tests/Unit/Helpers/GoodieTest.php b/tests/Unit/Helpers/GoodieTest.php new file mode 100644 index 000000000..d0615e448 --- /dev/null +++ b/tests/Unit/Helpers/GoodieTest.php @@ -0,0 +1,53 @@ +assertEquals('0', $result->getValue(new SQLiteGrammar(new Connection(new PDO('sqlite::memory:'))))); + } + + /** + * @covers \Engelsystem\Helpers\Goodie::userScore + * @covers \Engelsystem\Helpers\Goodie::worklogScoreQuery + */ + public function testUserScore(): void + { + /** @var User $user */ + $user = User::factory()->create(); + Worklog::factory()->create(['user_id' => $user->id, 'hours' => 42.23]); + + $result = Goodie::userScore($user); + + $this->assertEquals(42.23, $result); + } + + protected function setUp(): void + { + parent::setUp(); + + $this->initDatabase(); + $this->app->instance('config', new Config(['night_shifts' => ['enabled' => false]])); + } +} diff --git a/tests/Unit/Helpers/MarkdownTest.php b/tests/Unit/Helpers/MarkdownTest.php new file mode 100644 index 000000000..553417353 --- /dev/null +++ b/tests/Unit/Helpers/MarkdownTest.php @@ -0,0 +1,88 @@ +test', 'test'], + ['bold', '<b>bold</b>', 'bold'], + [ + '* test', + '' . PHP_EOL . 'test' . PHP_EOL . '', + '' . PHP_EOL . 'test' . PHP_EOL . '', + ], + ['', '<div></div>', ''], + [ + '[Test](https://example.com)', + 'Test', + 'Test', + ], + ['[Test](javascript:alert("hi"))', 'Test', 'Test'], + [ + '', + '<script>alert("ho")</script>', + '<script>alert("ho")</script>', // Looks kind of broken but thats fine + ], + [ + 'https://example.com/link', + 'https://example.com/link', + 'https://example.com/link', + ], + ]; + } + + /** + * @covers \Engelsystem\Helpers\Markdown::render + * @covers \Engelsystem\Helpers\Markdown::getRenderer + * @dataProvider mapping + */ + public function testRender(string $text, string $expected): void + { + $uuid = new Markdown(); + $result = $uuid->render($text); + + $this->assertEquals($expected, $result); + } + + /** + * @covers \Engelsystem\Helpers\Markdown::render + * @covers \Engelsystem\Helpers\Markdown::getRenderer + * @dataProvider mapping + */ + public function testRenderRaw(string $text, string $expected, string $withAllowHtml): void + { + $uuid = new Markdown(); + $result = $uuid->render($text, true); + + $this->assertEquals($withAllowHtml, $result); + } + + /** + * @covers \Engelsystem\Helpers\Markdown::render + * @covers \Engelsystem\Helpers\Markdown::getRenderer + */ + public function testRenderTable(): void + { + $text = '| test | value |' . PHP_EOL . '| --- | :--- |' . PHP_EOL . '| row | content |'; + + $uuid = new Markdown(); + $result = $uuid->render($text, true); + + $this->assertStringNotContainsString('|', $result); + $this->assertStringNotContainsString('---', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('table-striped', $result); + $this->assertStringContainsString('test', $result); + $this->assertStringContainsString('text-start', $result); + $this->assertStringContainsString('content', $result); + } +} diff --git a/tests/Unit/Helpers/Schedule/Assets/schedule-extended.xml b/tests/Unit/Helpers/Schedule/Assets/schedule-extended.xml index 18e774253..6963a5cfb 100644 --- a/tests/Unit/Helpers/Schedule/Assets/schedule-extended.xml +++ b/tests/Unit/Helpers/Schedule/Assets/schedule-extended.xml @@ -43,6 +43,7 @@ Any describing stuff? https://foo.bar/baz/schedule/ipsum https://foo.bar/baz/schedule/ipsum#feedback + https://some.example/event/ipsum https://lorem.ipsum/foo/bar.png Some Person diff --git a/tests/Unit/Helpers/Schedule/EventTest.php b/tests/Unit/Helpers/Schedule/EventTest.php index 1489c404e..44aff7140 100644 --- a/tests/Unit/Helpers/Schedule/EventTest.php +++ b/tests/Unit/Helpers/Schedule/EventTest.php @@ -38,6 +38,7 @@ class EventTest extends TestCase * @covers \Engelsystem\Helpers\Schedule\Event::getAttachments * @covers \Engelsystem\Helpers\Schedule\Event::getUrl * @covers \Engelsystem\Helpers\Schedule\Event::getFeedbackUrl + * @covers \Engelsystem\Helpers\Schedule\Event::getOriginUrl * @covers \Engelsystem\Helpers\Schedule\Event::getVideoDownloadUrl * @covers \Engelsystem\Helpers\Schedule\Event::getEndDate */ @@ -83,6 +84,7 @@ public function testCreateDefault(): void $this->assertNull($event->getUrl()); $this->assertNull($event->getVideoDownloadUrl()); $this->assertNull($event->getFeedbackUrl()); + $this->assertNull($event->getOriginUrl()); $this->assertEquals('2020-12-28T20:20:00+00:00', $event->getEndDate()->format(Carbon::RFC3339)); } @@ -111,6 +113,7 @@ public function testCreateDefault(): void * @covers \Engelsystem\Helpers\Schedule\Event::getAttachments * @covers \Engelsystem\Helpers\Schedule\Event::getUrl * @covers \Engelsystem\Helpers\Schedule\Event::getFeedbackUrl + * @covers \Engelsystem\Helpers\Schedule\Event::getOriginUrl * @covers \Engelsystem\Helpers\Schedule\Event::getVideoDownloadUrl */ public function testCreate(): void @@ -140,7 +143,8 @@ public function testCreate(): void $attachments, 'https://foo.bar/2-lorem', 'https://videos.orem.ipsum/2-lorem.mp4', - 'https://videos.orem.ipsum/2-lorem/feedback' + 'https://videos.orem.ipsum/2-lorem/feedback', + 'https://some.example/2-lorem/', ); $this->assertEquals('/foo/bar.png', $event->getLogo()); @@ -154,6 +158,7 @@ public function testCreate(): void $this->assertEquals('https://foo.bar/2-lorem', $event->getUrl()); $this->assertEquals('https://videos.orem.ipsum/2-lorem.mp4', $event->getVideoDownloadUrl()); $this->assertEquals('https://videos.orem.ipsum/2-lorem/feedback', $event->getFeedbackUrl()); + $this->assertEquals('https://some.example/2-lorem/', $event->getOriginUrl()); $event->setTitle('Event title'); $this->assertEquals('Event title', $event->getTitle()); diff --git a/tests/Unit/Helpers/Schedule/ScheduleTest.php b/tests/Unit/Helpers/Schedule/ScheduleTest.php index 794464a84..b030b69cf 100644 --- a/tests/Unit/Helpers/Schedule/ScheduleTest.php +++ b/tests/Unit/Helpers/Schedule/ScheduleTest.php @@ -37,6 +37,7 @@ public function testCreate(): void } /** + * @covers \Engelsystem\Helpers\Schedule\Schedule::getAllRooms * @covers \Engelsystem\Helpers\Schedule\Schedule::getRooms */ public function testGetRooms(): void @@ -45,25 +46,28 @@ public function testGetRooms(): void $room1 = new Room('Test 1'); $room2 = new Room('Test 2'); $room3 = new Room('Test 3'); + $room4 = new Room('Test 2'); $days = [ new Day( '2042-01-01', new Carbon('2042-01-01T00:00:00+00:00'), new Carbon('2042-01-01T23:59:00+00:00'), 1, - [$room1, $room2] + [$room1, $room2], ), new Day( '2042-01-02', - new Carbon('2042-02-01T00:00:00+00:00'), - new Carbon('2042-02-01T23:59:00+00:00'), + new Carbon('2042-02-02T00:00:00+00:00'), + new Carbon('2042-02-02T23:59:00+00:00'), 2, - [new Room('Test 2'), $room3] + [$room4, $room3], ), ]; $schedule = new Schedule('Lorem 1.3.3.7', $conference, $days); - $this->assertEquals(['Test 1' => $room1, 'Test 2' => $room2, 'Test 3' => $room3], $schedule->getRooms()); + $this->assertEquals(['Test 1' => $room1, 'Test 2' => $room4, 'Test 3' => $room3], $schedule->getRooms()); + $this->assertEquals([$room1, $room2, $room4, $room3], $schedule->getAllRooms()); + $this->assertTrue($room4 === $schedule->getRooms()['Test 2']); // Rooms should use last room occurrence $schedule = new Schedule('Lorem 1.3.3.0', $conference, []); $this->assertEquals([], $schedule->getRooms()); diff --git a/tests/Unit/Helpers/Schedule/XmlParserTest.php b/tests/Unit/Helpers/Schedule/XmlParserTest.php index 7b6e29150..47eb5c509 100644 --- a/tests/Unit/Helpers/Schedule/XmlParserTest.php +++ b/tests/Unit/Helpers/Schedule/XmlParserTest.php @@ -113,6 +113,7 @@ public function testLoad(): void $this->assertEquals('Any describing stuff?', $event->getDescription()); $this->assertEquals('https://foo.bar/baz/schedule/ipsum', $event->getUrl()); $this->assertEquals('https://foo.bar/baz/schedule/ipsum#feedback', $event->getFeedbackUrl()); + $this->assertEquals('https://some.example/event/ipsum', $event->getOriginUrl()); $this->assertEquals('https://lorem.ipsum/foo/bar.png', $event->getLogo()); $this->assertEquals([1234 => 'Some Person', 1337 => 'Another Person'], $event->getPersons()); $this->assertEquals([ diff --git a/tests/Unit/Helpers/ShiftsRendererTest.php b/tests/Unit/Helpers/ShiftsRendererTest.php new file mode 100644 index 000000000..b82b9e5d0 --- /dev/null +++ b/tests/Unit/Helpers/ShiftsRendererTest.php @@ -0,0 +1,95 @@ +create(); + /** @var AngelType $angelType */ + $angelType = AngelType::factory()->create(); + /** @var Schedule $scheduleLocation */ + $scheduleLocation = Schedule::factory()->create(['needed_from_shift_type' => false]); + /** @var Schedule $scheduleType */ + $scheduleType = Schedule::factory()->create(['needed_from_shift_type' => true]); + + /** @var Shift $shiftNormal */ + $shiftNormal = Shift::factory()->create(); + $shiftNormal->scheduleShift()->delete(); + /** @var Shift $shiftScheduleLocation */ + $shiftScheduleLocation = Shift::factory()->create(); + /** @var Shift $shiftScheduleType */ + $shiftScheduleType = Shift::factory()->create(); + + $scheduleShiftLocation = new ScheduleShift(['guid' => Str::uuid()]); + $scheduleShiftLocation->schedule()->associate($scheduleLocation); + $scheduleShiftLocation->shift()->associate($shiftScheduleLocation); + $scheduleShiftLocation->save(); + $scheduleShiftType = new ScheduleShift(['guid' => Str::uuid()]); + $scheduleShiftType->schedule()->associate($scheduleType); + $scheduleShiftType->shift()->associate($shiftScheduleType); + $scheduleShiftType->save(); + + $shiftNormal->neededAngelTypes()->create(['angel_type_id' => $angelType->id, 'count' => 3]); + + ShiftEntry::factory()->create([ + 'shift_id' => $shiftNormal, + 'angel_type_id' => $angelType, + 'user_id' => $user, + ]); + + /** @var ShiftsRenderer|MockObject $renderer */ + $renderer = $this->getMockBuilder(ShiftsRenderer::class) + ->onlyMethods(['renderShiftCalendar']) + ->getMock(); + $renderer->expects($this->once()) + ->method('renderShiftCalendar') + ->willReturnCallback(function (array | Collection $shifts, array $neededAngelTypes, array $shiftEntries) { + $this->assertCount(3, $shifts); + + $angelType = $neededAngelTypes[1][0]; + $this->assertNotEmpty($angelType); + $this->assertArrayHasKey('name', $angelType); + $this->assertArrayHasKey('restricted', $angelType); + $this->assertArrayHasKey('shift_self_signup', $angelType); + + $entry = $shiftEntries[1][0]; + $this->assertNotEmpty($entry); + + return 'rendered table'; + }); + + $output = $renderer->render(Shift::all()); + $this->assertEquals('rendered table', $output); + } + + protected function setUp(): void + { + parent::setUp(); + $this->initDatabase(); + Str::createUuidsUsing(Uuid::class . '::uuid'); + } +} diff --git a/tests/Unit/Helpers/Stub/cache/.gitignore b/tests/Unit/Helpers/Stub/cache/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/tests/Unit/Helpers/Stub/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/Unit/Helpers/UserVouchersTest.php b/tests/Unit/Helpers/UserVouchersTest.php new file mode 100644 index 000000000..f87cac3e1 --- /dev/null +++ b/tests/Unit/Helpers/UserVouchersTest.php @@ -0,0 +1,156 @@ +config->set('enable_voucher', true); + $this->config->set('voucher_settings', [ + 'initial_vouchers' => 0, + 'shifts_per_voucher' => 0, + 'hours_per_voucher' => 2, + 'voucher_start' => null, + ]); + + $this->user = User::factory()->create(); + $user2 = User::factory()->create(); + + // user + // start more than 3 days ago and ended, 2 hours long + $shift1 = Shift::factory()->create([ + 'start' => Carbon::now()->subDays(3)->subHour(), + 'end' => Carbon::now()->subDays(3)->addHour(), + ]); + ShiftEntry::factory()->create([ + 'shift_id' => $shift1->id, + 'user_id' => $this->user->id, + 'freeloaded_by' => null, + ]); + + // started less than 1 day ago and ended, 2 hours long + $shift2 = Shift::factory()->create([ + 'start' => Carbon::now()->subHours(3), + 'end' => Carbon::now()->subHour(), + ]); + ShiftEntry::factory()->create([ + 'shift_id' => $shift2->id, + 'user_id' => $this->user->id, + 'freeloaded_by' => null, + ]); + // entry freeloaded + ShiftEntry::factory()->create([ + 'shift_id' => $shift1->id, + 'user_id' => $this->user->id, + 'freeloaded_by' => $user2->id, + ]); + + // started less than 1 day ago and ended, 4 hours long + $shift3 = Shift::factory()->create([ + 'start' => Carbon::now()->subHours(5), + 'end' => Carbon::now()->subHour(), + ]); + ShiftEntry::factory()->create([ + 'shift_id' => $shift3->id, + 'user_id' => $this->user->id, + 'freeloaded_by' => null, + ]); + + // shifts still running, 2 hours long + $shift4 = Shift::factory()->create([ + 'start' => Carbon::now()->subHour(), + 'end' => Carbon::now()->addHour(), + ]); + ShiftEntry::factory()->create([ + 'shift_id' => $shift4->id, + 'user_id' => $this->user->id, + 'freeloaded_by' => null, + ]); + + // worklog 3 days ago, 2 hours long + Worklog::factory()->create([ + 'user_id' => $this->user->id, + 'creator_id' => $user2->id, + 'hours' => 4, + 'worked_at' => Carbon::today()->subDays(3), + ]); + // worklog today, 4 hours long + Worklog::factory()->create([ + 'user_id' => $this->user->id, + 'creator_id' => $user2->id, + 'hours' => 4, + 'worked_at' => Carbon::today(), + ]); + // worklog tomorrow, 2 hours long + Worklog::factory()->create([ + 'user_id' => $this->user->id, + 'creator_id' => $user2->id, + 'hours' => 4, + 'worked_at' => Carbon::tomorrow(), + ]); + } + + /** + * @return Array + */ + public function provideTestData(): array + { + return [ + // settings, userId, got_voucher, expected vouchers + 'initial_vouchers 2, user, got voucher 2' => [['initial_vouchers' => 2], 1, 2, 8], + 'shifts_per_voucher 1, hours_per_voucher 0, voucher_start 2 days ago, user, got voucher 0' => [[ + 'shifts_per_voucher' => 1, + 'hours_per_voucher' => 0, + 'voucher_start' => Carbon::now()->subDays(2)->format('Y-m-d'), + ], 1, 0, 3], + 'default settings, user2, got voucher 2' => [null, 2, 2, 0], + ]; + } + + /** + * @dataProvider provideTestData + * @covers \Engelsystem\Helpers\UserVouchers::eligibleVoucherCount + */ + public function testEligibleVoucherCount( + array | null $voucherSettings, + int $userId, + int $gotVoucher, + int $expected + ): void { + if ($voucherSettings) { + $this->config->set( + 'voucher_settings', + array_merge($this->config->get('voucher_settings'), $voucherSettings) + ); + } + + $user = User::find($userId); + $user->state->got_voucher = $gotVoucher; + $user->state->save(); + + $this->assertEquals($expected, UserVouchers::eligibleVoucherCount($user)); + } + + /** + * @covers \Engelsystem\Helpers\UserVouchers::eligibleVoucherCount + */ + public function testUserVouchersWithVouchersDisabled(): void + { + $this->config->set('enable_voucher', false); + $this->assertEquals(0, UserVouchers::eligibleVoucherCount($this->user)); + } +} diff --git a/tests/Unit/HelpersTest.php b/tests/Unit/HelpersTest.php index b39a6e467..16f5879af 100644 --- a/tests/Unit/HelpersTest.php +++ b/tests/Unit/HelpersTest.php @@ -9,6 +9,7 @@ use Engelsystem\Container\Container; use Engelsystem\Events\EventDispatcher; use Engelsystem\Helpers\Authenticator; +use Engelsystem\Helpers\Cache; use Engelsystem\Helpers\Translation\Translator; use Engelsystem\Http\Redirector; use Engelsystem\Http\Request; @@ -158,6 +159,23 @@ public function testBack(): void $this->assertEquals($response, $return); } + /** + * @covers \cache + */ + public function testCache(): void + { + $cache = $this->createMock(Cache::class); + $this->setExpects($cache, 'get', ['test', 'default', 42], 'default'); + + $app = new Application(); + $app->instance('cache', $cache); + + $this->assertEquals($cache, cache()); + + $return = cache('test', 'default', 42); + $this->assertEquals('default', $return); + } + /** * @covers \config_path */ diff --git a/tests/Unit/Http/PaginationServiceProviderTest.php b/tests/Unit/Http/PaginationServiceProviderTest.php new file mode 100644 index 000000000..a5d839170 --- /dev/null +++ b/tests/Unit/Http/PaginationServiceProviderTest.php @@ -0,0 +1,29 @@ +register(); + + $app->instance('request', new Request()); + + $this->assertTrue(Paginator::resolveCurrentPath('/default') != '/default'); + } +} diff --git a/tests/Unit/Http/RequestServiceProviderTest.php b/tests/Unit/Http/RequestServiceProviderTest.php index df87c91b9..ed61489da 100644 --- a/tests/Unit/Http/RequestServiceProviderTest.php +++ b/tests/Unit/Http/RequestServiceProviderTest.php @@ -106,7 +106,7 @@ public function provideRequestPathPrefix(): array * @covers \Engelsystem\Http\RequestServiceProvider::createRequestWithoutPrefix * @dataProvider provideRequestPathPrefix */ - public function testCreateRequestWithoutPrefix(string $requestUri, string $expected, string $url = null): void + public function testCreateRequestWithoutPrefix(string $requestUri, string $expected, ?string $url = null): void { $_SERVER['REQUEST_URI'] = $requestUri; $config = new Config([ diff --git a/tests/Unit/Http/RequestTest.php b/tests/Unit/Http/RequestTest.php index 2af4bf33f..82f51b83b 100644 --- a/tests/Unit/Http/RequestTest.php +++ b/tests/Unit/Http/RequestTest.php @@ -69,6 +69,31 @@ public function testHas(): void $this->assertFalse($request->has('baz')); } + /** + * @covers \Engelsystem\Http\Request::get + */ + public function testGet(): void + { + $request = new Request([ + // Query / GET + 'a' => 'From query', + 'g' => 'From query', + ], [ + // Request / POST + 'a' => 'From request', + 'g' => 'From request', + 'p' => 'From request', + ], [ + // Attributes + 'a' => 'From attributes', + ]); + + $this->assertEquals('From attributes', $request->get('a')); + $this->assertEquals('From query', $request->get('g')); + $this->assertEquals('From request', $request->get('p')); + $this->assertEquals('default value', $request->get('not-existing', 'default value')); + } + /** * @covers \Engelsystem\Http\Request::hasPostData */ diff --git a/tests/Unit/Http/Validation/Rules/CheckedTest.php b/tests/Unit/Http/Validation/Rules/CheckedTest.php index f7c14ce30..02ad17c29 100644 --- a/tests/Unit/Http/Validation/Rules/CheckedTest.php +++ b/tests/Unit/Http/Validation/Rules/CheckedTest.php @@ -10,14 +10,14 @@ class CheckedTest extends TestCase { /** - * @covers \Engelsystem\Http\Validation\Rules\Checked::validate + * @covers \Engelsystem\Http\Validation\Rules\Checked::isValid * @see TruthyTest */ - public function testValidate(): void + public function testIsValid(): void { $rule = new Checked(); - $this->assertTrue($rule->validate('on')); - $this->assertFalse($rule->validate(null)); + $this->assertTrue($rule->isValid('on')); + $this->assertFalse($rule->isValid(null)); } } diff --git a/tests/Unit/Http/Validation/Rules/ComparesDateTimeTest.php b/tests/Unit/Http/Validation/Rules/ComparesDateTimeTest.php index 181657328..7cbd8bb8b 100644 --- a/tests/Unit/Http/Validation/Rules/ComparesDateTimeTest.php +++ b/tests/Unit/Http/Validation/Rules/ComparesDateTimeTest.php @@ -18,11 +18,14 @@ class ComparesDateTimeTest extends TestCase public function testValidate(): void { $rule = new UsesComparesDateTime('2024-01-02 13:37'); - $rule->setCallback(function ($input) { + $rule->setCallback(function ($input, $comparison) { /** @var Carbon $input */ $this->assertInstanceOf(Carbon::class, $input); $this->assertEquals('2042-10-11 00:00:00', $input->toDateTimeString()); + $this->assertInstanceOf(Carbon::class, $comparison); + $this->assertEquals('2024-01-02 13:37:00', $comparison->toDateTimeString()); + return true; }); @@ -63,8 +66,9 @@ public function testValidateDateTimeStaysSame(): void $b = Carbon::now(); $rule = new UsesComparesDateTime($a); - $rule->setCallback(function ($input) use ($b) { + $rule->setCallback(function ($input, $comparison) use ($a, $b) { $this->assertEquals($b, $input); + $this->assertEquals($a, $comparison); return false; }); diff --git a/tests/Unit/Http/Validation/Rules/InTest.php b/tests/Unit/Http/Validation/Rules/InTest.php index 03f8c248e..758934b09 100644 --- a/tests/Unit/Http/Validation/Rules/InTest.php +++ b/tests/Unit/Http/Validation/Rules/InTest.php @@ -16,6 +16,10 @@ public function testConstruct(): void { $rule = new In('foo,bar'); - $this->assertEquals(['foo', 'bar'], $rule->haystack); + $this->assertTrue($rule->validate('foo')); + $this->assertTrue($rule->validate('bar')); + + $this->assertFalse($rule->validate('baz')); + $this->assertFalse($rule->validate('foo,bar')); } } diff --git a/tests/Unit/Http/Validation/Rules/MinTest.php b/tests/Unit/Http/Validation/Rules/MinTest.php index 60e95d546..7a395a42f 100644 --- a/tests/Unit/Http/Validation/Rules/MinTest.php +++ b/tests/Unit/Http/Validation/Rules/MinTest.php @@ -29,5 +29,11 @@ public function testValidate(): void $this->assertTrue($rule->validate('2042-01-01')); $this->assertTrue($rule->validate('2042-01-02')); $this->assertTrue($rule->validate('2345-01-01')); + + $rule = new Min(3); + $this->assertFalse($rule->validate('')); + $this->assertFalse($rule->validate('TE')); + $this->assertTrue($rule->validate('TES')); + $this->assertTrue($rule->validate('FOO BAR')); } } diff --git a/tests/Unit/Http/Validation/Rules/Stub/UsesComparesDateTime.php b/tests/Unit/Http/Validation/Rules/Stub/UsesComparesDateTime.php index 24d5ef7ec..f439672b0 100644 --- a/tests/Unit/Http/Validation/Rules/Stub/UsesComparesDateTime.php +++ b/tests/Unit/Http/Validation/Rules/Stub/UsesComparesDateTime.php @@ -12,11 +12,11 @@ class UsesComparesDateTime protected mixed $callback = null; - public function compare(mixed $input): bool + public function compare(mixed $left, mixed $right): bool { /** @var callable $callback */ $callback = $this->callback; - return $callback($input); + return $callback($left, $right); } public function setCallback(callable $callback): void diff --git a/tests/Unit/Http/Validation/Rules/UsernameTest.php b/tests/Unit/Http/Validation/Rules/UsernameTest.php index 705d97457..66a119331 100644 --- a/tests/Unit/Http/Validation/Rules/UsernameTest.php +++ b/tests/Unit/Http/Validation/Rules/UsernameTest.php @@ -30,7 +30,7 @@ public function setUp(): void /** * @return array */ - public function provideValidateWithDefaultConfigTestData(): array + public function provideIsValidWithDefaultConfigTestData(): array { return [ 'empty string' => ['', false], @@ -46,22 +46,22 @@ public function provideValidateWithDefaultConfigTestData(): array } /** - * @covers \Engelsystem\Http\Validation\Rules\Username::validate - * @dataProvider provideValidateWithDefaultConfigTestData + * @covers \Engelsystem\Http\Validation\Rules\Username::isValid + * @dataProvider provideIsValidWithDefaultConfigTestData */ - public function testValidateWithDefaultConfig(mixed $value, bool $expectedValid): void + public function testIsValidWithDefaultConfig(mixed $value, bool $expectedValid): void { - self::assertSame($expectedValid, $this->subject->validate($value)); + self::assertSame($expectedValid, $this->subject->isValid($value)); } /** - * @covers \Engelsystem\Http\Validation\Rules\Username::validate + * @covers \Engelsystem\Http\Validation\Rules\Username::isValid */ - public function testMissingConfigRaisesException(): void + public function testIsValidMissingConfigRaisesException(): void { $this->config->set('username_regex', null); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('username_regex not set in config'); - $this->subject->validate('test'); + $this->subject->isValid('test'); } } diff --git a/tests/Unit/Http/Validation/ValidatorTest.php b/tests/Unit/Http/Validation/ValidatorTest.php index b301557d3..52ee7b0e5 100644 --- a/tests/Unit/Http/Validation/ValidatorTest.php +++ b/tests/Unit/Http/Validation/ValidatorTest.php @@ -14,6 +14,7 @@ class ValidatorTest extends TestCase * @covers \Engelsystem\Http\Validation\Validator::validate * @covers \Engelsystem\Http\Validation\Validator::getData * @covers \Engelsystem\Http\Validation\Validator::getErrors + * @covers \Engelsystem\Http\Validation\Validator::configureValidationFactory */ public function testValidate(): void { diff --git a/tests/Unit/Mail/EngelsystemMailerTest.php b/tests/Unit/Mail/EngelsystemMailerTest.php index 6f558ce9e..860fdadd9 100644 --- a/tests/Unit/Mail/EngelsystemMailerTest.php +++ b/tests/Unit/Mail/EngelsystemMailerTest.php @@ -109,7 +109,7 @@ public function testSend(): void $symfonyMailer->expects($this->once()) ->method('send') - ->willReturnCallback(function (RawMessage $message, Envelope $envelope = null): void { + ->willReturnCallback(function (RawMessage $message, ?Envelope $envelope = null): void { $this->assertStringContainsString('foo@bar.baz', $message->toString()); $this->assertStringContainsString('Foo Bar', $message->toString()); $this->assertStringContainsString('Mail test', $message->toString()); diff --git a/tests/Unit/Mail/MailerTest.php b/tests/Unit/Mail/MailerTest.php index 8afd94f1c..7b0c1cb78 100644 --- a/tests/Unit/Mail/MailerTest.php +++ b/tests/Unit/Mail/MailerTest.php @@ -48,7 +48,7 @@ public function testSend(): void $symfonyMailer = $this->createMock(MailerInterface::class); $symfonyMailer->expects($this->once()) ->method('send') - ->willReturnCallback(function (RawMessage $message, Envelope $envelope = null): void { + ->willReturnCallback(function (RawMessage $message, ?Envelope $envelope = null): void { $this->assertStringContainsString('to@xam.pel', $message->toString()); $this->assertStringContainsString('foo@bar.baz', $message->toString()); $this->assertStringContainsString('Test Tester', $message->toString()); @@ -75,7 +75,7 @@ public function testSendException(): void $symfonyMailer = $this->createMock(MailerInterface::class); $symfonyMailer->expects($this->once()) ->method('send') - ->willReturnCallback(function (RawMessage $message, Envelope $envelope = null): void { + ->willReturnCallback(function (RawMessage $message, ?Envelope $envelope = null): void { throw new TransportException('Unable to connect to port 42'); }); diff --git a/tests/Unit/Middleware/ApiRouteHandlerTest.php b/tests/Unit/Middleware/ApiRouteHandlerTest.php index 61b2eab3c..b2f88c359 100644 --- a/tests/Unit/Middleware/ApiRouteHandlerTest.php +++ b/tests/Unit/Middleware/ApiRouteHandlerTest.php @@ -155,7 +155,7 @@ public function testProcessGenericException(): void /** @var RequestHandlerInterface|MockObject $handler */ $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); $errorHandler = $this->createMock(Handler::class); - $this->setExpects($errorHandler, 'exceptionHandler', [$e, true], '', $this->once()); + $this->setExpects($errorHandler, 'exceptionHandler', [$e, false], '', $this->once()); $this->app->instance('error.handler', $errorHandler); $handler->expects($this->once()) diff --git a/tests/Unit/Middleware/LegacyMiddlewareTest.php b/tests/Unit/Middleware/LegacyMiddlewareTest.php index fe6beeff6..a8a50dd16 100644 --- a/tests/Unit/Middleware/LegacyMiddlewareTest.php +++ b/tests/Unit/Middleware/LegacyMiddlewareTest.php @@ -79,4 +79,37 @@ public function testProcess(): void $middleware->process($request, $handler); } + + /** + * @covers \Engelsystem\Middleware\LegacyMiddleware::process + */ + public function testProcessResponseInterface(): void + { + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); + + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + $auth->expects($this->exactly(2)) + ->method('can') + ->withConsecutive(['users.arrive.list'], ['admin_arrive']) + ->willReturnOnConsecutiveCalls(true, false); + + $request = new Request([], [], [], [], [], ['REQUEST_URI' => 'admin-arrive']); + $this->app->instance('request', $request); + + $responseInstance = new Response(); + /** @var LegacyMiddleware|MockObject $middleware */ + $middleware = $this->getMockBuilder(LegacyMiddleware::class) + ->setConstructorArgs([$this->app, $auth]) + ->onlyMethods(['loadPage', 'renderPage']) + ->getMock(); + $middleware->expects($this->once()) + ->method('loadPage') + ->with('admin_arrive') + ->willReturn(['', $responseInstance]); + + $response = $middleware->process($request, $handler); + $this->assertEquals($responseInstance, $response); + } } diff --git a/tests/Unit/Middleware/RequestHandlerTest.php b/tests/Unit/Middleware/RequestHandlerTest.php index fcf8327e7..da93dd852 100644 --- a/tests/Unit/Middleware/RequestHandlerTest.php +++ b/tests/Unit/Middleware/RequestHandlerTest.php @@ -262,6 +262,34 @@ public function testCheckPermissionsAny(): void $this->assertEquals(200, $response->getStatusCode()); } + /** + * @covers \Engelsystem\Middleware\RequestHandler::checkPermissions + */ + public function testCheckPermissionsHasPermission(): void + { + /** @var RequestHandlerInterface|MockObject $handler */ + list(, , $handler) = $this->getMocks(); + $this->app->instance('response', new Response()); + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + $this->app->instance('auth', $auth); + + $controller = new ControllerImplementation(); + $request = (new Request()) + ->withAttribute('route-request-handler', [$controller, 'allow']); + + $middleware = new RequestHandler($this->app); + + $response = $middleware->process($request, $handler); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('yay', $response->getBody()->getContents()); + + $request = (new Request()) + ->withAttribute('route-request-handler', [$controller, 'deny']); + $this->expectException(HttpForbidden::class); + $middleware->process($request, $handler); + } + protected function getMocks(): array { /** @var Application|MockObject $container */ diff --git a/tests/Unit/Middleware/Stub/ControllerImplementation.php b/tests/Unit/Middleware/Stub/ControllerImplementation.php index 2d6227742..766320178 100644 --- a/tests/Unit/Middleware/Stub/ControllerImplementation.php +++ b/tests/Unit/Middleware/Stub/ControllerImplementation.php @@ -5,6 +5,7 @@ namespace Engelsystem\Test\Unit\Middleware\Stub; use Engelsystem\Controllers\BaseController; +use Psr\Http\Message\ServerRequestInterface; class ControllerImplementation extends BaseController { @@ -17,4 +18,23 @@ public function actionStub(): string { return ''; } + + public function hasPermission(ServerRequestInterface $request, string $method): ?bool + { + return match ($method) { + 'allow' => true, + 'deny' => false, + default => parent::hasPermission($request, $method), + }; + } + + public function allow(): string + { + return 'yay'; + } + + public function deny(): string + { + return 'nope'; + } } diff --git a/tests/Unit/Models/LogEntryTest.php b/tests/Unit/Models/LogEntryTest.php index 944d6ec3a..b20e83e97 100644 --- a/tests/Unit/Models/LogEntryTest.php +++ b/tests/Unit/Models/LogEntryTest.php @@ -22,7 +22,7 @@ public function testFilter(): void 'I\'m an info' => LogLevel::INFO, '*Insert explosion here*' => LogLevel::EMERGENCY, 'Tracing along' => LogLevel::DEBUG, - 'Oops,no notice given' => LogLevel::NOTICE, + 'Oops, no notice given' => LogLevel::NOTICE, 'It\'s happening' => LogLevel::INFO, 'Something is wrong' => LogLevel::ERROR, 'Ohi' => LogLevel::INFO, @@ -35,7 +35,7 @@ public function testFilter(): void } $this->assertCount(11, LogEntry::filter()); - $this->assertCount(3, LogEntry::filter(LogLevel::INFO)); + $this->assertCount(3, LogEntry::filter(null, null, LogLevel::INFO)); $this->assertCount(1, LogEntry::filter('Oops')); $this->assertCount(1, LogEntry::filter(null, $user->id)); diff --git a/tests/Unit/Models/Shifts/ShiftTest.php b/tests/Unit/Models/Shifts/ShiftTest.php index f138d34d7..9e3de7a0f 100644 --- a/tests/Unit/Models/Shifts/ShiftTest.php +++ b/tests/Unit/Models/Shifts/ShiftTest.php @@ -6,6 +6,7 @@ use Engelsystem\Config\Config; use Engelsystem\Helpers\Carbon; +use Engelsystem\Models\AngelType; use Engelsystem\Models\Location; use Engelsystem\Models\Shifts\NeededAngelType; use Engelsystem\Models\Shifts\Schedule; @@ -99,6 +100,26 @@ public function testSchedule(): void $this->assertEquals(1, Shift::find(3)->schedule->id); } + /** + * @covers \Engelsystem\Models\Shifts\Shift::scheduleShift + */ + public function testScheduleShift(): void + { + /** @var Schedule $schedule */ + $schedule = Schedule::factory()->create(); + /** @var Collection|Shift[] $shifts */ + $shifts = Shift::factory(4)->create(); + + (new ScheduleShift(['shift_id' => $shifts[0]->id, 'schedule_id' => $schedule->id, 'guid' => 'd']))->save(); + (new ScheduleShift(['shift_id' => $shifts[1]->id, 'schedule_id' => $schedule->id, 'guid' => 'e']))->save(); + (new ScheduleShift(['shift_id' => $shifts[2]->id, 'schedule_id' => $schedule->id, 'guid' => 'f']))->save(); + + $this->assertEquals('d', Shift::find(1)->scheduleShift->guid); + $this->assertEquals('e', Shift::find(2)->scheduleShift->guid); + $this->assertEquals('f', Shift::find(3)->scheduleShift->guid); + $this->assertNull(Shift::find(4)->scheduleShift?->guid); + } + /** * @covers \Engelsystem\Models\Shifts\Shift::shiftEntries */ @@ -113,6 +134,130 @@ public function testShiftEntries(): void $this->assertCount(5, $shift->shiftEntries); } + /** + * @covers \Engelsystem\Models\Shifts\Shift::scopeNeedsUsers + */ + public function testScopeNeedsUsers(): void + { + /** @var AngelType $angelType */ + $angelType = AngelType::factory()->create(); + /** @var Shift $shift */ + $shift = Shift::factory()->create(); + Shift::factory()->create(); + + $this->assertCount(2, Shift::all()); + $this->assertCount(0, Shift::scopes('needsUsers')->get()); + + NeededAngelType::factory()->create(['angel_type_id' => $angelType->id, 'shift_id' => $shift->id]); + + $this->assertTrue(Shift::count() >= 2); + $this->assertCount(1, Shift::scopes('needsUsers')->get()); + } + + /** + * @covers \Engelsystem\Models\Shifts\Shift::scopeNeedsUsers + */ + public function testScopeNeedsUsersFromSchedule(): void + { + /** @var Schedule $schedule1 */ + $schedule1 = Schedule::factory()->create(['needed_from_shift_type' => true]); + /** @var Schedule $schedule2 */ + $schedule2 = Schedule::factory()->create(['needed_from_shift_type' => false]); + $shiftType = $schedule1->shiftType; + /** @var AngelType $angelType */ + $angelType = AngelType::factory()->create(); + /** @var Shift $shift1 Via schedule shift type */ + $shift1 = Shift::factory()->create(['shift_type_id' => $shiftType->id]); + /** @var Shift $shift2 Via schedule location */ + $shift2 = Shift::factory()->create(); + /** @var Shift $shift3 Direct */ + $shift3 = Shift::factory()->create(); + /** @var Shift $shift4 Via schedule location, no needed angel types */ + $shift4 = Shift::factory()->create(); + /** @var Shift $shift5 Empty shift */ + $shift5 = Shift::factory()->create(); + $location = $shift2->location; + + ScheduleShift::factory()->create(['shift_id' => $shift1->id, 'schedule_id' => $schedule1->id]); + ScheduleShift::factory()->create(['shift_id' => $shift2->id, 'schedule_id' => $schedule2->id]); + ScheduleShift::factory()->create(['shift_id' => $shift4->id, 'schedule_id' => $schedule2->id]); + + NeededAngelType::factory()->create(['angel_type_id' => $angelType->id, 'shift_type_id' => $shiftType->id]); + NeededAngelType::factory()->create(['angel_type_id' => $angelType->id, 'location_id' => $location->id]); + NeededAngelType::factory()->create(['angel_type_id' => $angelType->id, 'shift_id' => $shift3->id]); + + $this->assertTrue(Shift::count() >= 5); + + $shifts = Shift::scopes('needsUsers')->get()->pluck('id'); + $this->assertContains($shift1->id, $shifts, 'Shift should be selected via schedule shift type'); + $this->assertContains($shift2->id, $shifts, 'Shift should be selected via schedule location selected'); + $this->assertContains($shift3->id, $shifts, 'Shift should be selected via direct requirement selected'); + $this->assertNotContains($shift4->id, $shifts, 'Empty schedule location shift selected'); + $this->assertNotContains($shift5->id, $shifts, 'Empty shift selected'); + } + + /** + * @covers \Engelsystem\Models\Shifts\Shift::nextShift + */ + public function testNextShift(): void + { + $location = Location::factory()->create(); + $shiftType = ShiftType::factory()->create(); + $shift = Shift::factory()->create([ + 'location_id' => $location->id, + 'shift_type_id' => $shiftType->id, + 'title' => 'Rocket start', + 'start' => Carbon::now(), + ]); + $nextShift = Shift::factory()->create([ + 'location_id' => $location->id, + 'shift_type_id' => $shiftType->id, + 'title' => 'Rocket start', + 'start' => Carbon::now()->addHour(), + ]); + $otherShift = Shift::factory()->create([ + 'location_id' => $location->id, + 'shift_type_id' => $shiftType->id, + 'title' => 'Rocket starts', + 'start' => Carbon::now()->addHours(3), + ]); + + $this->assertEquals($nextShift->id, $shift->nextShift()->id); + $this->assertEquals($otherShift->id, $nextShift->nextShift()->id); + $this->assertNull($otherShift->nextShift()); + } + + /** + * @covers \Engelsystem\Models\Shifts\Shift::previousShift + */ + public function testPreviousShift(): void + { + $location = Location::factory()->create(); + $shiftType = ShiftType::factory()->create(); + $shift = Shift::factory()->create([ + 'location_id' => $location->id, + 'shift_type_id' => $shiftType->id, + 'title' => 'Rocket start', + 'end' => Carbon::now(), + ]); + $previousShift = Shift::factory()->create([ + 'location_id' => $location->id, + 'shift_type_id' => $shiftType->id, + 'title' => 'Rocket start', + 'end' => Carbon::now()->subHour(), + ]); + $otherShift = Shift::factory()->create([ + 'location_id' => $location->id, + 'shift_type_id' => $shiftType->id, + 'title' => 'Rocket starts', + 'end' => Carbon::now()->subHours(3), + ]); + + $this->assertEquals($previousShift->id, $shift->previousShift()->id); + $this->assertEquals($otherShift->id, $previousShift->previousShift()->id); + $this->assertNull($otherShift->previousShift()); + } + /** * @covers \Engelsystem\Models\Shifts\Shift::isNightShift */ diff --git a/tests/Unit/Models/User/UserStateTest.php b/tests/Unit/Models/User/UserStateTest.php new file mode 100644 index 000000000..0ce8afe9e --- /dev/null +++ b/tests/Unit/Models/User/UserStateTest.php @@ -0,0 +1,41 @@ +assertFalse($state->arrived); + + $state->arrival_date = Carbon::now(); + $this->assertTrue($state->arrived); + } + + /** + * @covers \Engelsystem\Models\User\State::scopeWhereArrived + */ + public function testScopeWhereArrived(): void + { + $state = State::factory()->create([ + 'arrival_date' => null, + ]); + $this->assertCount(0, State::whereArrived(true)->get()); + $this->assertCount(1, State::whereArrived(false)->get()); + + $state->arrival_date = Carbon::now(); + $state->save(); + $this->assertCount(1, State::whereArrived(true)->get()); + $this->assertCount(0, State::whereArrived(false)->get()); + } +} diff --git a/tests/Unit/Models/User/UserTest.php b/tests/Unit/Models/User/UserTest.php index e381ea11e..0b522271a 100644 --- a/tests/Unit/Models/User/UserTest.php +++ b/tests/Unit/Models/User/UserTest.php @@ -77,6 +77,7 @@ public function hasOneRelationsProvider(): array 'state', [ 'force_active' => true, + 'force_food' => true, ], ], [ @@ -437,11 +438,11 @@ public function testWorklogs(): void { ($user = new User($this->data))->save(); $worklogEntry = Worklog::create([ - 'user_id' => $user->id, - 'creator_id' => $user->id, - 'hours' => 1, - 'comment' => '', - 'worked_at' => Carbon::now(), + 'user_id' => $user->id, + 'creator_id' => $user->id, + 'hours' => 1, + 'description' => '', + 'worked_at' => Carbon::now(), ]); $worklogs = $user->worklogs; @@ -457,11 +458,11 @@ public function testWorklogsCreated(): void { ($user = new User($this->data))->save(); $worklogEntry = Worklog::create([ - 'user_id' => $user->id, - 'creator_id' => $user->id, - 'hours' => 1, - 'comment' => '', - 'worked_at' => Carbon::now(), + 'user_id' => $user->id, + 'creator_id' => $user->id, + 'hours' => 1, + 'description' => '', + 'worked_at' => Carbon::now(), ]); $worklogs = $user->worklogsCreated; diff --git a/tests/Unit/Models/WorklogTest.php b/tests/Unit/Models/WorklogTest.php index dcf96380f..f2c70abf6 100644 --- a/tests/Unit/Models/WorklogTest.php +++ b/tests/Unit/Models/WorklogTest.php @@ -22,8 +22,9 @@ public function testCreator(): void $worklog->user()->associate($user1); $worklog->creator()->associate($user2); $worklog->hours = 4.2; - $worklog->comment = 'Lorem ipsum'; + $worklog->description = 'Lorem ipsum'; $worklog->worked_at = new Carbon(); + $worklog->night_shift = false; $worklog->save(); $savedWorklog = Worklog::first(); diff --git a/tests/Unit/Renderer/Twig/Extensions/LegacyTest.php b/tests/Unit/Renderer/Twig/Extensions/LegacyTest.php index 67da45678..90d46950c 100644 --- a/tests/Unit/Renderer/Twig/Extensions/LegacyTest.php +++ b/tests/Unit/Renderer/Twig/Extensions/LegacyTest.php @@ -4,8 +4,11 @@ namespace Engelsystem\Test\Unit\Renderer\Twig\Extensions; +use Engelsystem\Helpers\ShiftsRenderer; use Engelsystem\Http\Request; +use Engelsystem\Models\Shifts\Shift; use Engelsystem\Renderer\Twig\Extensions\Legacy; +use Illuminate\Database\Eloquent\Collection; use PHPUnit\Framework\MockObject\MockObject; class LegacyTest extends ExtensionTest @@ -26,14 +29,52 @@ public function testGetFunctions(): void $this->assertExtensionExists('menuUserShiftState', 'User_shift_state_render', $functions, $isSafeHtml); $this->assertExtensionExists('menuUserHints', 'header_render_hints', $functions, $isSafeHtml); $this->assertExtensionExists('menuLanguages', 'make_language_select', $functions, $isSafeHtml); + $this->assertExtensionExists('renderShifts', [$extension, 'renderShifts'], $functions, $isSafeHtml); $this->assertExtensionExists('page', [$extension, 'getPage'], $functions); } + /** + * @covers \Engelsystem\Renderer\Twig\Extensions\Legacy::getFilters + */ + public function testGetFilters(): void + { + /** @var Request|MockObject $request */ + $request = $this->createMock(Request::class); + + $extension = new Legacy($request); + $filters = $extension->getFilters(); + + $this->assertFilterExists('dateWithEventDay', 'dateWithEventDay', $filters); + } + + /** + * @covers \Engelsystem\Renderer\Twig\Extensions\Legacy::renderShifts + */ + public function testRenderShifts(): void + { + /** @var Request|MockObject $request */ + $request = $this->createMock(Request::class); + $renderingShifts = [new Shift()]; + /** @var ShiftsRenderer|MockObject $shiftsRenderer */ + $shiftsRenderer = $this->createMock(ShiftsRenderer::class); + $shiftsRenderer->expects($this->once()) + ->method('render') + ->willReturnCallback(function (array | Collection $shifts) use ($renderingShifts) { + $this->assertEquals($renderingShifts, $shifts); + return 'rendered shifts'; + }); + $this->app->instance(ShiftsRenderer::class, $shiftsRenderer); + + $extension = new Legacy($request); + $output = $extension->renderShifts($renderingShifts); + $this->assertEquals('rendered shifts', $output); + } + /** * @covers \Engelsystem\Renderer\Twig\Extensions\Legacy::__construct * @covers \Engelsystem\Renderer\Twig\Extensions\Legacy::getPage */ - public function testIsAuthenticated(): void + public function testGetPage(): void { /** @var Request|MockObject $request */ $request = $this->createMock(Request::class); diff --git a/tests/Unit/Renderer/Twig/Extensions/MarkdownTest.php b/tests/Unit/Renderer/Twig/Extensions/MarkdownTest.php index 3208097b0..066867eb1 100644 --- a/tests/Unit/Renderer/Twig/Extensions/MarkdownTest.php +++ b/tests/Unit/Renderer/Twig/Extensions/MarkdownTest.php @@ -4,8 +4,8 @@ namespace Engelsystem\Test\Unit\Renderer\Twig\Extensions; +use Engelsystem\Helpers\Markdown as MarkdownRenderer; use Engelsystem\Renderer\Twig\Extensions\Markdown; -use Parsedown; class MarkdownTest extends ExtensionTest { @@ -14,7 +14,7 @@ class MarkdownTest extends ExtensionTest */ public function testGetFilters(): void { - $extension = new Markdown(new Parsedown()); + $extension = new Markdown(new MarkdownRenderer()); $filters = $extension->getFilters(); $this->assertFilterExists('markdown', [$extension, 'render'], $filters); @@ -27,12 +27,17 @@ public function testGetFilters(): void */ public function testRender(): void { - $extension = new Markdown(new Parsedown()); + $extension = new Markdown(new MarkdownRenderer()); $this->assertEquals( '<i>Lorem</i> "Ipsum"', $extension->render('Lorem *"Ipsum"*'), ); + + $this->assertEquals( + '', + $extension->render(null), + ); } /** @@ -40,8 +45,7 @@ public function testRender(): void */ public function testRenderHtml(): void { - $renderer = new Parsedown(); - $extension = new Markdown($renderer); + $extension = new Markdown(new MarkdownRenderer()); $this->assertEquals( 'Lorem "Ipsum"', diff --git a/tests/Unit/Renderer/Twig/Extensions/QrTest.php b/tests/Unit/Renderer/Twig/Extensions/QrTest.php new file mode 100644 index 000000000..d04f3f5cd --- /dev/null +++ b/tests/Unit/Renderer/Twig/Extensions/QrTest.php @@ -0,0 +1,36 @@ +getFunctions(); + + $this->assertExtensionExists('qr', [$extension, 'getQr'], $functions, ['is_safe' => ['html']]); + } + + /** + * @covers \Engelsystem\Renderer\Twig\Extensions\Qr::getQr + */ + public function testGetQr(): void + { + $extension = new Qr(); + + $generatedCode = $extension->getQr('Test'); + $this->assertStringContainsString('assertStringContainsString('width="200" height="200"', $generatedCode); + + $generatedCode = $extension->getQr('Test', 1337); + $this->assertStringContainsString('width="1337" height="1337"', $generatedCode); + } +} diff --git a/tests/Unit/RuntimeTest.php b/tests/Unit/RuntimeTest.php new file mode 100644 index 000000000..99049a279 --- /dev/null +++ b/tests/Unit/RuntimeTest.php @@ -0,0 +1,13 @@ +once(); diff --git a/yarn.lock b/yarn.lock index 614ebc613..aa24d5d88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,1008 +2,820 @@ # yarn lockfile v1 -"@aashutoshrathi/word-wrap@^1.2.3": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" - integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== - -"@ampproject/remapping@^2.1.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" - integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== - dependencies: - "@jridgewell/gen-mapping" "^0.1.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6": +"@babel/code-frame@^7.0.0": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== dependencies: "@babel/highlight" "^7.18.6" -"@babel/code-frame@^7.22.13": - version "7.22.13" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" - integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== +"@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== dependencies: - "@babel/highlight" "^7.22.13" - chalk "^2.4.2" + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.20.5": - version "7.20.14" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.14.tgz#4106fc8b755f3e3ee0a0a7c27dde5de1d2b2baf8" - integrity sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw== +"@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.5.tgz#a8a4962e1567121ac0b3b487f52107443b455c7f" + integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA== "@babel/core@^7.20.12": - version "7.20.12" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" - integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helpers" "^7.20.7" - "@babel/parser" "^7.20.7" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.12" - "@babel/types" "^7.20.7" - convert-source-map "^1.7.0" + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e" + integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.5" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.28.3" + "@babel/helpers" "^7.28.4" + "@babel/parser" "^7.28.5" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" - json5 "^2.2.2" - semver "^6.3.0" + json5 "^2.2.3" + semver "^6.3.1" "@babel/eslint-parser@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz#4f68f6b0825489e00a24b41b6a1ae35414ecd2f4" - integrity sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ== + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.28.5.tgz#0b8883a4a1c2cbed7b3cd9d7765d80e8f480b9ae" + integrity sha512-fcdRcWahONYo+JRnJg1/AekOacGvKx12Gu0qXJXFi2WBqQA1i7+O5PaxRB7kxE/Op94dExnCiiar6T09pvdHpA== dependencies: "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" eslint-visitor-keys "^2.1.0" - semver "^6.3.0" - -"@babel/generator@^7.20.7": - version "7.20.14" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.14.tgz#9fa772c9f86a46c6ac9b321039400712b96f64ce" - integrity sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg== - dependencies: - "@babel/types" "^7.20.7" - "@jridgewell/gen-mapping" "^0.3.2" - jsesc "^2.5.1" - -"@babel/generator@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" - integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== - dependencies: - "@babel/types" "^7.23.0" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/helper-annotate-as-pure@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" - integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb" - integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.18.6" - "@babel/types" "^7.18.9" - -"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0", "@babel/helper-compilation-targets@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb" - integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ== - dependencies: - "@babel/compat-data" "^7.20.5" - "@babel/helper-validator-option" "^7.18.6" - browserslist "^4.21.3" + semver "^6.3.1" + +"@babel/generator@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298" + integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ== + dependencies: + "@babel/parser" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" + integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== + dependencies: + "@babel/types" "^7.27.3" + +"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" + integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== + dependencies: + "@babel/compat-data" "^7.27.2" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" lru-cache "^5.1.1" - semver "^6.3.0" - -"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.20.5", "@babel/helper-create-class-features-plugin@^7.20.7": - version "7.20.12" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz#4349b928e79be05ed2d1643b20b99bb87c503819" - integrity sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-member-expression-to-functions" "^7.20.7" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.20.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/helper-split-export-declaration" "^7.18.6" - -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.20.5.tgz#5ea79b59962a09ec2acf20a963a01ab4d076ccca" - integrity sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - regexpu-core "^5.2.1" - -"@babel/helper-define-polyfill-provider@^0.3.3": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz#8612e55be5d51f0cd1f36b4a5a83924e89884b7a" - integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww== - dependencies: - "@babel/helper-compilation-targets" "^7.17.7" - "@babel/helper-plugin-utils" "^7.16.7" - debug "^4.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz#472d0c28028850968979ad89f173594a6995da46" + integrity sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-member-expression-to-functions" "^7.28.5" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.28.5" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz#7c1ddd64b2065c7f78034b25b43346a7e19ed997" + integrity sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + regexpu-core "^6.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.5": + version "0.6.5" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz#742ccf1cb003c07b48859fc9fa2c1bbe40e5f753" + integrity sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg== + dependencies: + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + debug "^4.4.1" lodash.debounce "^4.0.8" - resolve "^1.14.2" - semver "^6.1.2" - -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== - -"@babel/helper-environment-visitor@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" - integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== - -"@babel/helper-explode-assignable-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" - integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" - integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== - dependencies: - "@babel/template" "^7.18.10" - "@babel/types" "^7.19.0" - -"@babel/helper-function-name@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" - integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== - dependencies: - "@babel/template" "^7.22.15" - "@babel/types" "^7.23.0" - -"@babel/helper-hoist-variables@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" - integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-hoist-variables@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" - integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-member-expression-to-functions@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz#a6f26e919582275a93c3aa6594756d71b0bb7f05" - integrity sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw== - dependencies: - "@babel/types" "^7.20.7" - -"@babel/helper-module-imports@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" - integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz#df4c7af713c557938c50ea3ad0117a7944b2f1b0" - integrity sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.20.2" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/helper-validator-identifier" "^7.19.1" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.10" - "@babel/types" "^7.20.7" - -"@babel/helper-optimise-call-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" - integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== - -"@babel/helper-remap-async-to-generator@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" - integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-wrap-function" "^7.18.9" - "@babel/types" "^7.18.9" - -"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz#243ecd2724d2071532b2c8ad2f0f9f083bcae331" - integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-member-expression-to-functions" "^7.20.7" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/helper-simple-access@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" - integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== - dependencies: - "@babel/types" "^7.20.2" - -"@babel/helper-skip-transparent-expression-wrappers@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" - integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== - dependencies: - "@babel/types" "^7.20.0" - -"@babel/helper-split-export-declaration@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" - integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-split-export-declaration@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" - integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== - -"@babel/helper-string-parser@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" - integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== - -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== - -"@babel/helper-validator-identifier@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" - integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== - -"@babel/helper-validator-option@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" - integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== - -"@babel/helper-wrap-function@^7.18.9": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz#75e2d84d499a0ab3b31c33bcfe59d6b8a45f62e3" - integrity sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q== - dependencies: - "@babel/helper-function-name" "^7.19.0" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" - -"@babel/helpers@^7.20.7": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.13.tgz#e3cb731fb70dc5337134cadc24cbbad31cc87ad2" - integrity sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg== - dependencies: - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.13" - "@babel/types" "^7.20.7" + resolve "^1.22.10" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-member-expression-to-functions@^7.27.1", "@babel/helper-member-expression-to-functions@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150" + integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg== + dependencies: + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + +"@babel/helper-module-imports@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.3": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz#a2b37d3da3b2344fe085dab234426f2b9a2fa5f6" + integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.28.3" + +"@babel/helper-optimise-call-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== + dependencies: + "@babel/types" "^7.27.1" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" + integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== + +"@babel/helper-remap-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" + integrity sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-wrap-function" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-replace-supers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz#b1ed2d634ce3bdb730e4b52de30f8cccfd692bc0" + integrity sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-skip-transparent-expression-wrappers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.25.9", "@babel/helper-validator-identifier@^7.27.1", "@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helper-wrap-function@^7.27.1": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz#fe4872092bc1438ffd0ce579e6f699609f9d0a7a" + integrity sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g== + dependencies: + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.3" + "@babel/types" "^7.28.2" + +"@babel/helpers@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827" + integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== + dependencies: + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.4" "@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== - dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/highlight@^7.22.13": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" - integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.9.tgz#8141ce68fc73757946f983b343f1231f4691acc6" + integrity sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw== dependencies: - "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-validator-identifier" "^7.25.9" chalk "^2.4.2" js-tokens "^4.0.0" + picocolors "^1.0.0" -"@babel/parser@^7.20.7": - version "7.20.15" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.15.tgz#eec9f36d8eaf0948bb88c87a46784b5ee9fd0c89" - integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg== - -"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" - integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" - integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ== +"@babel/parser@^7.27.2", "@babel/parser@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08" + integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/types" "^7.28.5" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz#d9c85589258539a22a901033853101a6198d4ef1" - integrity sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ== +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz#fbde57974707bbfa0376d34d425ff4fa6c732421" + integrity sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/plugin-proposal-optional-chaining" "^7.20.7" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.5" -"@babel/plugin-proposal-async-generator-functions@^7.20.1": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326" - integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz#43f70a6d7efd52370eefbdf55ae03d91b293856d" + integrity sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA== dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-remap-async-to-generator" "^7.18.9" - "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-proposal-class-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" - integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz#beb623bd573b8b6f3047bd04c32506adc3e58a72" + integrity sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA== dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-proposal-class-static-block@^7.18.6": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.20.7.tgz#92592e9029b13b15be0f7ce6a7aedc2879ca45a7" - integrity sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ== +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz#e134a5479eb2ba9c02714e8c1ebf1ec9076124fd" + integrity sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.20.7" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" -"@babel/plugin-proposal-dynamic-import@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz#72bcf8d408799f547d759298c3c27c7e7faa4d94" - integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw== +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.28.3": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz#373f6e2de0016f73caf8f27004f61d167743742a" + integrity sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.3" -"@babel/plugin-proposal-export-namespace-from@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203" - integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== -"@babel/plugin-proposal-json-strings@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz#7e8788c1811c393aff762817e7dbf1ebd0c05f0b" - integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ== +"@babel/plugin-syntax-import-assertions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz#88894aefd2b03b5ee6ad1562a7c8e1587496aecd" + integrity sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-proposal-logical-assignment-operators@^7.18.9": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz#dfbcaa8f7b4d37b51e8bfb46d94a5aea2bb89d83" - integrity sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug== +"@babel/plugin-syntax-import-attributes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz#34c017d54496f9b11b61474e7ea3dfd5563ffe07" + integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" - integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-proposal-numeric-separator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" - integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== +"@babel/plugin-transform-arrow-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a" + integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-proposal-object-rest-spread@^7.20.2": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" - integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== +"@babel/plugin-transform-async-generator-functions@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz#1276e6c7285ab2cd1eccb0bc7356b7a69ff842c2" + integrity sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q== dependencies: - "@babel/compat-data" "^7.20.5" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.20.7" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/traverse" "^7.28.0" -"@babel/plugin-proposal-optional-catch-binding@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb" - integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== +"@babel/plugin-transform-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz#9a93893b9379b39466c74474f55af03de78c66e7" + integrity sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" -"@babel/plugin-proposal-optional-chaining@^7.18.9", "@babel/plugin-proposal-optional-chaining@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.20.7.tgz#49f2b372519ab31728cc14115bb0998b15bfda55" - integrity sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ== +"@babel/plugin-transform-block-scoped-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz#558a9d6e24cf72802dd3b62a4b51e0d62c0f57f9" + integrity sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-proposal-private-methods@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" - integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== +"@babel/plugin-transform-block-scoping@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz#e0d3af63bd8c80de2e567e690a54e84d85eb16f6" + integrity sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g== dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-proposal-private-property-in-object@^7.18.6": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.20.5.tgz#309c7668f2263f1c711aa399b5a9a6291eef6135" - integrity sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ== +"@babel/plugin-transform-class-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz#dd40a6a370dfd49d32362ae206ddaf2bb082a925" + integrity sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA== dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-create-class-features-plugin" "^7.20.5" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e" - integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== +"@babel/plugin-transform-class-static-block@^7.28.3": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz#d1b8e69b54c9993bc558203e1f49bfc979bfd852" + integrity sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-create-class-features-plugin" "^7.28.3" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== +"@babel/plugin-transform-classes@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz#75d66175486788c56728a73424d67cbc7473495c" + integrity sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA== dependencies: - "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-globals" "^7.28.0" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/traverse" "^7.28.4" -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== +"@babel/plugin-transform-computed-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz#81662e78bf5e734a97982c2b7f0a793288ef3caa" + integrity sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw== dependencies: - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/template" "^7.27.1" -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== +"@babel/plugin-transform-destructuring@^7.28.0", "@babel/plugin-transform-destructuring@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz#b8402764df96179a2070bb7b501a1586cf8ad7a7" + integrity sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.5" -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== +"@babel/plugin-transform-dotall-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz#aa6821de864c528b1fecf286f0a174e38e826f4d" + integrity sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw== dependencies: - "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== +"@babel/plugin-transform-duplicate-keys@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz#f1fbf628ece18e12e7b32b175940e68358f546d1" + integrity sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-import-assertions@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz#bb50e0d4bea0957235390641209394e87bdb9cc4" - integrity sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ== +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz#5043854ca620a94149372e69030ff8cb6a9eb0ec" + integrity sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ== dependencies: - "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== +"@babel/plugin-transform-dynamic-import@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz#4c78f35552ac0e06aa1f6e3c573d67695e8af5a4" + integrity sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A== dependencies: - "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== +"@babel/plugin-transform-explicit-resource-management@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz#45be6211b778dbf4b9d54c4e8a2b42fa72e09a1a" + integrity sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.28.0" -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== +"@babel/plugin-transform-exponentiation-operator@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz#7cc90a8170e83532676cfa505278e147056e94fe" + integrity sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw== dependencies: - "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== +"@babel/plugin-transform-export-namespace-from@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz#71ca69d3471edd6daa711cf4dfc3400415df9c23" + integrity sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== +"@babel/plugin-transform-for-of@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz#bc24f7080e9ff721b63a70ac7b2564ca15b6c40a" + integrity sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw== dependencies: - "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== +"@babel/plugin-transform-function-name@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz#4d0bf307720e4dce6d7c30fcb1fd6ca77bdeb3a7" + integrity sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== +"@babel/plugin-transform-json-strings@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz#a2e0ce6ef256376bd527f290da023983527a4f4c" + integrity sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q== dependencies: - "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== +"@babel/plugin-transform-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz#baaefa4d10a1d4206f9dcdda50d7d5827bb70b24" + integrity sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== +"@babel/plugin-transform-logical-assignment-operators@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz#d028fd6db8c081dee4abebc812c2325e24a85b0e" + integrity sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-arrow-functions@^7.18.6": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz#bea332b0e8b2dab3dafe55a163d8227531ab0551" - integrity sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ== +"@babel/plugin-transform-member-expression-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz#37b88ba594d852418e99536f5612f795f23aeaf9" + integrity sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-async-to-generator@^7.18.6": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz#dfee18623c8cb31deb796aa3ca84dda9cea94354" - integrity sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q== +"@babel/plugin-transform-modules-amd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz#a4145f9d87c2291fe2d05f994b65dba4e3e7196f" + integrity sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA== dependencies: - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-remap-async-to-generator" "^7.18.9" + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-block-scoped-functions@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz#9187bf4ba302635b9d70d986ad70f038726216a8" - integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ== +"@babel/plugin-transform-modules-commonjs@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz#8e44ed37c2787ecc23bdc367f49977476614e832" + integrity sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-block-scoping@^7.20.2": - version "7.20.15" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.15.tgz#3e1b2aa9cbbe1eb8d644c823141a9c5c2a22392d" - integrity sha512-Vv4DMZ6MiNOhu/LdaZsT/bsLRxgL94d269Mv4R/9sp6+Mp++X/JqypZYypJXLlM4mlL352/Egzbzr98iABH1CA== +"@babel/plugin-transform-modules-systemjs@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz#7439e592a92d7670dfcb95d0cbc04bd3e64801d2" + integrity sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-module-transforms" "^7.28.3" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.5" -"@babel/plugin-transform-classes@^7.20.2": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.7.tgz#f438216f094f6bb31dc266ebfab8ff05aecad073" - integrity sha512-LWYbsiXTPKl+oBlXUGlwNlJZetXD5Am+CyBdqhPsDVjM9Jc8jwBJFrKhHf900Kfk2eZG1y9MAG3UNajol7A4VQ== +"@babel/plugin-transform-modules-umd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz#63f2cf4f6dc15debc12f694e44714863d34cd334" + integrity sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w== dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-replace-supers" "^7.20.7" - "@babel/helper-split-export-declaration" "^7.18.6" - globals "^11.1.0" + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-computed-properties@^7.18.9": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz#704cc2fd155d1c996551db8276d55b9d46e4d0aa" - integrity sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ== +"@babel/plugin-transform-named-capturing-groups-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz#f32b8f7818d8fc0cc46ee20a8ef75f071af976e1" + integrity sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/template" "^7.20.7" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-destructuring@^7.20.2": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz#8bda578f71620c7de7c93af590154ba331415454" - integrity sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA== +"@babel/plugin-transform-new-target@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz#259c43939728cad1706ac17351b7e6a7bea1abeb" + integrity sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz#b286b3e7aae6c7b861e45bed0a2fafd6b1a4fef8" - integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== +"@babel/plugin-transform-nullish-coalescing-operator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz#4f9d3153bf6782d73dd42785a9d22d03197bc91d" + integrity sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-duplicate-keys@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz#687f15ee3cdad6d85191eb2a372c4528eaa0ae0e" - integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw== +"@babel/plugin-transform-numeric-separator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz#614e0b15cc800e5997dadd9bd6ea524ed6c819c6" + integrity sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-exponentiation-operator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz#421c705f4521888c65e91fdd1af951bfefd4dacd" - integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw== +"@babel/plugin-transform-object-rest-spread@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz#9ee1ceca80b3e6c4bac9247b2149e36958f7f98d" + integrity sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.28.0" + "@babel/plugin-transform-parameters" "^7.27.7" + "@babel/traverse" "^7.28.4" -"@babel/plugin-transform-for-of@^7.18.8": - version "7.18.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz#6ef8a50b244eb6a0bdbad0c7c61877e4e30097c1" - integrity sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ== +"@babel/plugin-transform-object-super@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz#1c932cd27bf3874c43a5cac4f43ebf970c9871b5" + integrity sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" -"@babel/plugin-transform-function-name@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz#cc354f8234e62968946c61a46d6365440fc764e0" - integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== +"@babel/plugin-transform-optional-catch-binding@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz#84c7341ebde35ccd36b137e9e45866825072a30c" + integrity sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q== dependencies: - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-function-name" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-literals@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz#72796fdbef80e56fba3c6a699d54f0de557444bc" - integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg== +"@babel/plugin-transform-optional-chaining@^7.27.1", "@babel/plugin-transform-optional-chaining@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz#8238c785f9d5c1c515a90bf196efb50d075a4b26" + integrity sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" -"@babel/plugin-transform-member-expression-literals@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz#ac9fdc1a118620ac49b7e7a5d2dc177a1bfee88e" - integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA== +"@babel/plugin-transform-parameters@^7.27.7": + version "7.27.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz#1fd2febb7c74e7d21cf3b05f7aebc907940af53a" + integrity sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-modules-amd@^7.19.6": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz#3daccca8e4cc309f03c3a0c4b41dc4b26f55214a" - integrity sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g== +"@babel/plugin-transform-private-methods@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz#fdacbab1c5ed81ec70dfdbb8b213d65da148b6af" + integrity sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA== dependencies: - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-modules-commonjs@^7.19.6": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.20.11.tgz#8cb23010869bf7669fd4b3098598b6b2be6dc607" - integrity sha512-S8e1f7WQ7cimJQ51JkAaDrEtohVEitXjgCGAS2N8S31Y42E+kWwfSz83LYz57QdBm7q9diARVqanIaH2oVgQnw== +"@babel/plugin-transform-private-property-in-object@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz#4dbbef283b5b2f01a21e81e299f76e35f900fb11" + integrity sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ== dependencies: - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-modules-systemjs@^7.19.6": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz#467ec6bba6b6a50634eea61c9c232654d8a4696e" - integrity sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw== +"@babel/plugin-transform-property-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz#07eafd618800591e88073a0af1b940d9a42c6424" + integrity sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ== dependencies: - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-validator-identifier" "^7.19.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-modules-umd@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz#81d3832d6034b75b54e62821ba58f28ed0aab4b9" - integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ== +"@babel/plugin-transform-regenerator@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz#9d3fa3bebb48ddd0091ce5729139cd99c67cea51" + integrity sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA== dependencies: - "@babel/helper-module-transforms" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-named-capturing-groups-regex@^7.19.1": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz#626298dd62ea51d452c3be58b285d23195ba69a8" - integrity sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA== +"@babel/plugin-transform-regexp-modifiers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz#df9ba5577c974e3f1449888b70b76169998a6d09" + integrity sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.20.5" - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-new-target@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz#d128f376ae200477f37c4ddfcc722a8a1b3246a8" - integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw== +"@babel/plugin-transform-reserved-words@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz#40fba4878ccbd1c56605a4479a3a891ac0274bb4" + integrity sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-object-super@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" - integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA== +"@babel/plugin-transform-shorthand-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz#532abdacdec87bfee1e0ef8e2fcdee543fe32b90" + integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-replace-supers" "^7.18.6" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-parameters@^7.20.1", "@babel/plugin-transform-parameters@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz#0ee349e9d1bc96e78e3b37a7af423a4078a7083f" - integrity sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA== +"@babel/plugin-transform-spread@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz#1a264d5fc12750918f50e3fe3e24e437178abb08" + integrity sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" -"@babel/plugin-transform-property-literals@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz#e22498903a483448e94e032e9bbb9c5ccbfc93a3" - integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg== +"@babel/plugin-transform-sticky-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz#18984935d9d2296843a491d78a014939f7dcd280" + integrity sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-regenerator@^7.18.6": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz#57cda588c7ffb7f4f8483cc83bdcea02a907f04d" - integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ== +"@babel/plugin-transform-template-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz#1a0eb35d8bb3e6efc06c9fd40eb0bcef548328b8" + integrity sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - regenerator-transform "^0.15.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-reserved-words@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz#b1abd8ebf8edaa5f7fe6bbb8d2133d23b6a6f76a" - integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA== +"@babel/plugin-transform-typeof-symbol@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz#70e966bb492e03509cf37eafa6dcc3051f844369" + integrity sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-shorthand-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz#6d6df7983d67b195289be24909e3f12a8f664dc9" - integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== +"@babel/plugin-transform-unicode-escapes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz#3e3143f8438aef842de28816ece58780190cf806" + integrity sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-spread@^7.19.0": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz#c2d83e0b99d3bf83e07b11995ee24bf7ca09401e" - integrity sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw== +"@babel/plugin-transform-unicode-property-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz#bdfe2d3170c78c5691a3c3be934c8c0087525956" + integrity sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-sticky-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz#c6706eb2b1524028e317720339583ad0f444adcc" - integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q== +"@babel/plugin-transform-unicode-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz#25948f5c395db15f609028e370667ed8bae9af97" + integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-template-literals@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz#04ec6f10acdaa81846689d63fae117dd9c243a5e" - integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA== +"@babel/plugin-transform-unicode-sets-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz#6ab706d10f801b5c72da8bb2548561fa04193cd1" + integrity sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-typeof-symbol@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz#c8cea68263e45addcd6afc9091429f80925762c0" - integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-unicode-escapes@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246" - integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-unicode-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz#194317225d8c201bbae103364ffe9e2cea36cdca" - integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" "@babel/preset-env@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.20.2.tgz#9b1642aa47bb9f43a86f9630011780dab7f86506" - integrity sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg== - dependencies: - "@babel/compat-data" "^7.20.1" - "@babel/helper-compilation-targets" "^7.20.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-validator-option" "^7.18.6" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9" - "@babel/plugin-proposal-async-generator-functions" "^7.20.1" - "@babel/plugin-proposal-class-properties" "^7.18.6" - "@babel/plugin-proposal-class-static-block" "^7.18.6" - "@babel/plugin-proposal-dynamic-import" "^7.18.6" - "@babel/plugin-proposal-export-namespace-from" "^7.18.9" - "@babel/plugin-proposal-json-strings" "^7.18.6" - "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" - "@babel/plugin-proposal-numeric-separator" "^7.18.6" - "@babel/plugin-proposal-object-rest-spread" "^7.20.2" - "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" - "@babel/plugin-proposal-optional-chaining" "^7.18.9" - "@babel/plugin-proposal-private-methods" "^7.18.6" - "@babel/plugin-proposal-private-property-in-object" "^7.18.6" - "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.20.0" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.18.6" - "@babel/plugin-transform-async-to-generator" "^7.18.6" - "@babel/plugin-transform-block-scoped-functions" "^7.18.6" - "@babel/plugin-transform-block-scoping" "^7.20.2" - "@babel/plugin-transform-classes" "^7.20.2" - "@babel/plugin-transform-computed-properties" "^7.18.9" - "@babel/plugin-transform-destructuring" "^7.20.2" - "@babel/plugin-transform-dotall-regex" "^7.18.6" - "@babel/plugin-transform-duplicate-keys" "^7.18.9" - "@babel/plugin-transform-exponentiation-operator" "^7.18.6" - "@babel/plugin-transform-for-of" "^7.18.8" - "@babel/plugin-transform-function-name" "^7.18.9" - "@babel/plugin-transform-literals" "^7.18.9" - "@babel/plugin-transform-member-expression-literals" "^7.18.6" - "@babel/plugin-transform-modules-amd" "^7.19.6" - "@babel/plugin-transform-modules-commonjs" "^7.19.6" - "@babel/plugin-transform-modules-systemjs" "^7.19.6" - "@babel/plugin-transform-modules-umd" "^7.18.6" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.19.1" - "@babel/plugin-transform-new-target" "^7.18.6" - "@babel/plugin-transform-object-super" "^7.18.6" - "@babel/plugin-transform-parameters" "^7.20.1" - "@babel/plugin-transform-property-literals" "^7.18.6" - "@babel/plugin-transform-regenerator" "^7.18.6" - "@babel/plugin-transform-reserved-words" "^7.18.6" - "@babel/plugin-transform-shorthand-properties" "^7.18.6" - "@babel/plugin-transform-spread" "^7.19.0" - "@babel/plugin-transform-sticky-regex" "^7.18.6" - "@babel/plugin-transform-template-literals" "^7.18.9" - "@babel/plugin-transform-typeof-symbol" "^7.18.9" - "@babel/plugin-transform-unicode-escapes" "^7.18.10" - "@babel/plugin-transform-unicode-regex" "^7.18.6" - "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.20.2" - babel-plugin-polyfill-corejs2 "^0.3.3" - babel-plugin-polyfill-corejs3 "^0.6.0" - babel-plugin-polyfill-regenerator "^0.4.1" - core-js-compat "^3.25.1" - semver "^6.3.0" - -"@babel/preset-modules@^0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" - integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.28.5.tgz#82dd159d1563f219a1ce94324b3071eb89e280b0" + integrity sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg== + dependencies: + "@babel/compat-data" "^7.28.5" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.28.5" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.27.1" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.27.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.28.3" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions" "^7.27.1" + "@babel/plugin-syntax-import-attributes" "^7.27.1" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.27.1" + "@babel/plugin-transform-async-generator-functions" "^7.28.0" + "@babel/plugin-transform-async-to-generator" "^7.27.1" + "@babel/plugin-transform-block-scoped-functions" "^7.27.1" + "@babel/plugin-transform-block-scoping" "^7.28.5" + "@babel/plugin-transform-class-properties" "^7.27.1" + "@babel/plugin-transform-class-static-block" "^7.28.3" + "@babel/plugin-transform-classes" "^7.28.4" + "@babel/plugin-transform-computed-properties" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.28.5" + "@babel/plugin-transform-dotall-regex" "^7.27.1" + "@babel/plugin-transform-duplicate-keys" "^7.27.1" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-dynamic-import" "^7.27.1" + "@babel/plugin-transform-explicit-resource-management" "^7.28.0" + "@babel/plugin-transform-exponentiation-operator" "^7.28.5" + "@babel/plugin-transform-export-namespace-from" "^7.27.1" + "@babel/plugin-transform-for-of" "^7.27.1" + "@babel/plugin-transform-function-name" "^7.27.1" + "@babel/plugin-transform-json-strings" "^7.27.1" + "@babel/plugin-transform-literals" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.28.5" + "@babel/plugin-transform-member-expression-literals" "^7.27.1" + "@babel/plugin-transform-modules-amd" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-modules-systemjs" "^7.28.5" + "@babel/plugin-transform-modules-umd" "^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-new-target" "^7.27.1" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.27.1" + "@babel/plugin-transform-numeric-separator" "^7.27.1" + "@babel/plugin-transform-object-rest-spread" "^7.28.4" + "@babel/plugin-transform-object-super" "^7.27.1" + "@babel/plugin-transform-optional-catch-binding" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.28.5" + "@babel/plugin-transform-parameters" "^7.27.7" + "@babel/plugin-transform-private-methods" "^7.27.1" + "@babel/plugin-transform-private-property-in-object" "^7.27.1" + "@babel/plugin-transform-property-literals" "^7.27.1" + "@babel/plugin-transform-regenerator" "^7.28.4" + "@babel/plugin-transform-regexp-modifiers" "^7.27.1" + "@babel/plugin-transform-reserved-words" "^7.27.1" + "@babel/plugin-transform-shorthand-properties" "^7.27.1" + "@babel/plugin-transform-spread" "^7.27.1" + "@babel/plugin-transform-sticky-regex" "^7.27.1" + "@babel/plugin-transform-template-literals" "^7.27.1" + "@babel/plugin-transform-typeof-symbol" "^7.27.1" + "@babel/plugin-transform-unicode-escapes" "^7.27.1" + "@babel/plugin-transform-unicode-property-regex" "^7.27.1" + "@babel/plugin-transform-unicode-regex" "^7.27.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.27.1" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.14" + babel-plugin-polyfill-corejs3 "^0.13.0" + babel-plugin-polyfill-regenerator "^0.6.5" + core-js-compat "^3.43.0" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b" - integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== - dependencies: - regenerator-runtime "^0.13.11" - -"@babel/template@^7.18.10", "@babel/template@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" - integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/template@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" - integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== - dependencies: - "@babel/code-frame" "^7.22.13" - "@babel/parser" "^7.22.15" - "@babel/types" "^7.22.15" - -"@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.13", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7": - version "7.23.2" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" - integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== - dependencies: - "@babel/code-frame" "^7.22.13" - "@babel/generator" "^7.23.0" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.0" - "@babel/types" "^7.23.0" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.4.4": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" - integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg== - dependencies: - "@babel/helper-string-parser" "^7.19.4" - "@babel/helper-validator-identifier" "^7.19.1" - to-fast-properties "^2.0.0" +"@babel/runtime@^7.9.2": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" + integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.27.1", "@babel/template@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" + +"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4", "@babel/traverse@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b" + integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.5" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.5" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.5" + debug "^4.3.1" -"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" - integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== +"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.28.5", "@babel/types@^7.4.4": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b" + integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== dependencies: - "@babel/helper-string-parser" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.20" - to-fast-properties "^2.0.0" + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" "@discoveryjs/json-ext@^0.5.0": version "0.5.7" @@ -1011,36 +823,16 @@ integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== "@eslint-community/eslint-utils@^4.2.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" - integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + version "4.9.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz#7308df158e064f0dd8b8fdb58aa14fa2a7f913b3" + integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g== dependencies: - eslint-visitor-keys "^3.3.0" - -"@eslint-community/regexpp@^4.4.0": - version "4.5.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" - integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== + eslint-visitor-keys "^3.4.3" "@eslint-community/regexpp@^4.6.1": - version "4.10.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" - integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== - -"@eslint/eslintrc@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.0.tgz#82256f164cc9e0b59669efc19d57f8092706841d" - integrity sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.6.0" - globals "^13.19.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" + version "4.12.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== "@eslint/eslintrc@^2.1.4": version "2.1.4" @@ -1057,31 +849,17 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.44.0": - version "8.44.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af" - integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw== - -"@eslint/js@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" - integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== - -"@humanwhocodes/config-array@^0.11.10": - version "0.11.10" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" - integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== - dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" - minimatch "^3.0.5" +"@eslint/js@8.57.1": + version "8.57.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== -"@humanwhocodes/config-array@^0.11.13": - version "0.11.14" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" - integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== dependencies: - "@humanwhocodes/object-schema" "^2.0.2" + "@humanwhocodes/object-schema" "^2.0.3" debug "^4.3.1" minimatch "^3.0.5" @@ -1090,80 +868,50 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== - -"@humanwhocodes/object-schema@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" - integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== +"@humanwhocodes/object-schema@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== -"@jest/schemas@^29.4.0": - version "29.4.0" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.0.tgz#0d6ad358f295cc1deca0b643e6b4c86ebd539f17" - integrity sha512-0E01f/gOZeNTG76i5eWWSupvSHaIINrTie7vCyjiYFKgzNdyEGd12BUv4oNBFHOqlHDbtoJi3HrQ38KCC90NsQ== +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== dependencies: - "@sinclair/typebox" "^0.25.16" + "@sinclair/typebox" "^0.27.8" -"@jest/types@^29.4.1": - version "29.4.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.4.1.tgz#f9f83d0916f50696661da72766132729dcb82ecb" - integrity sha512-zbrAXDUOnpJ+FMST2rV7QZOgec8rskg2zv8g2ajeqitp4tvZiyqTCYXANrKsM+ryj5o+LI+ZN2EgU9drrkiwSA== +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== dependencies: - "@jest/schemas" "^29.4.0" + "@jest/schemas" "^29.6.3" "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" "@types/node" "*" "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jridgewell/gen-mapping@^0.1.0": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" - integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== dependencies: - "@jridgewell/set-array" "^1.0.0" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/gen-mapping@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== - dependencies: - "@jridgewell/set-array" "^1.0.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" -"@jridgewell/gen-mapping@^0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@jridgewell/resolve-uri@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== - "@jridgewell/resolve-uri@^3.1.0": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" - integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== - -"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== - -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== "@jridgewell/source-map@^0.3.3": version "0.3.6" @@ -1173,25 +921,12 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/sourcemap-codec@^1.4.14": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== - -"@jridgewell/trace-mapping@^0.3.17": - version "0.3.20" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" - integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== -"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": +"@jridgewell/trace-mapping@^0.3.20": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== @@ -1199,13 +934,13 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@jridgewell/trace-mapping@^0.3.9": - version "0.3.17" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" - integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" @@ -1245,10 +980,10 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== -"@sinclair/typebox@^0.25.16": - version "0.25.21" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.21.tgz#763b05a4b472c93a8db29b2c3e359d55b29ce272" - integrity sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g== +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== "@trysound/sax@0.2.0": version "0.2.0" @@ -1261,33 +996,40 @@ integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" - integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== "@types/istanbul-lib-report@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" - integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== dependencies: "@types/istanbul-lib-coverage" "*" "@types/istanbul-reports@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" - integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== dependencies: "@types/istanbul-lib-report" "*" -"@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@^7.0.8": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/node@*": - version "18.11.19" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.19.tgz#35e26df9ec441ab99d73e99e9aca82935eea216d" - integrity sha512-YUgMWAQBWLObABqrvx8qKO1enAvBUdjZOAWQ5grBAkp5LQv45jBvYKZ3oFS9iKRCQyFjqw6iuEa1vmFqtxYLZw== + version "22.13.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.4.tgz#3fe454d77cd4a2d73c214008b3e331bfaaf5038a" + integrity sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg== + dependencies: + undici-types "~6.20.0" "@types/parse-json@^4.0.0": version "4.0.0" @@ -1295,21 +1037,21 @@ integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/yargs-parser@*": - version "21.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" - integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== "@types/yargs@^17.0.8": - version "17.0.22" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.22.tgz#7dd37697691b5f17d020f3c63e7a45971ff71e9a" - integrity sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g== + version "17.0.33" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" + integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== dependencies: "@types/yargs-parser" "*" "@ungap/structured-clone@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" - integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + version "1.3.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== "@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": version "1.12.1" @@ -1467,11 +1209,16 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.7.1, acorn@^8.8.2: version "8.12.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== +acorn@^8.9.0: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + adjust-sourcemap-loader@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" @@ -1492,14 +1239,14 @@ ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv-keywords@^5.0.0: +ajv-keywords@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== dependencies: fast-deep-equal "^3.1.3" -ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1509,15 +1256,15 @@ ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.8.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== +ajv@^8.0.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.2.2" ansi-regex@^5.0.1: version "5.0.1" @@ -1571,35 +1318,40 @@ babel-loader@^9.1.2: find-cache-dir "^3.3.2" schema-utils "^4.0.0" -babel-plugin-polyfill-corejs2@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz#5d1bd3836d0a19e1b84bbf2d9640ccb6f951c122" - integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q== +babel-plugin-polyfill-corejs2@^0.4.14: + version "0.4.14" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz#8101b82b769c568835611542488d463395c2ef8f" + integrity sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg== dependencies: - "@babel/compat-data" "^7.17.7" - "@babel/helper-define-polyfill-provider" "^0.3.3" - semver "^6.1.1" + "@babel/compat-data" "^7.27.7" + "@babel/helper-define-polyfill-provider" "^0.6.5" + semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz#56ad88237137eade485a71b52f72dbed57c6230a" - integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA== +babel-plugin-polyfill-corejs3@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz#bb7f6aeef7addff17f7602a08a6d19a128c30164" + integrity sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A== dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.3" - core-js-compat "^3.25.1" + "@babel/helper-define-polyfill-provider" "^0.6.5" + core-js-compat "^3.43.0" -babel-plugin-polyfill-regenerator@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz#390f91c38d90473592ed43351e801a9d3e0fd747" - integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw== +babel-plugin-polyfill-regenerator@^0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz#32752e38ab6f6767b92650347bf26a31b16ae8c5" + integrity sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg== dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.3" + "@babel/helper-define-polyfill-provider" "^0.6.5" balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +baseline-browser-mapping@^2.8.25: + version "2.8.28" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz#9ef511f5a7c19d74a94cafcbf951608398e9bdb3" + integrity sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ== + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -1626,17 +1378,17 @@ bootstrap@^5.2.3: integrity sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ== brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + version "2.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== dependencies: balanced-match "^1.0.0" @@ -1647,7 +1399,17 @@ braces@~3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.0.0, browserslist@^4.16.6, browserslist@^4.21.10, browserslist@^4.21.3, browserslist@^4.21.4: +browserslist@^4.0.0, browserslist@^4.21.4: + version "4.24.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" + integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== + dependencies: + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + node-releases "^2.0.19" + update-browserslist-db "^1.1.1" + +browserslist@^4.21.10: version "4.23.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== @@ -1657,6 +1419,17 @@ browserslist@^4.0.0, browserslist@^4.16.6, browserslist@^4.21.10, browserslist@^ node-releases "^2.0.18" update-browserslist-db "^1.1.0" +browserslist@^4.24.0, browserslist@^4.26.3: + version "4.28.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.0.tgz#9cefece0a386a17a3cd3d22ebf67b9deca1b5929" + integrity sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ== + dependencies: + baseline-browser-mapping "^2.8.25" + caniuse-lite "^1.0.30001754" + electron-to-chromium "^1.5.249" + node-releases "^2.0.27" + update-browserslist-db "^1.1.4" + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -1677,17 +1450,27 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001426: +caniuse-lite@^1.0.0: + version "1.0.30001700" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz#26cd429cf09b4fd4e745daf4916039c794d720f6" + integrity sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ== + +caniuse-lite@^1.0.30001426: version "1.0.30001450" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz#022225b91200589196b814b51b1bbe45144cf74f" integrity sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew== caniuse-lite@^1.0.30001646: - version "1.0.30001655" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz#0ce881f5a19a2dcfda2ecd927df4d5c1684b982f" - integrity sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg== + version "1.0.30001707" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz#c5e104d199e6f4355a898fcd995a066c7eb9bf41" + integrity sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw== + +caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001754: + version "1.0.30001755" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz#c01cfb1c30f5acf1229391666ec03492f4c332ff" + integrity sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA== -chalk@^2.0.0, chalk@^2.4.2: +chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1734,9 +1517,9 @@ chrome-trace-event@^1.0.2: integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== ci-info@^3.2.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f" - integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w== + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== clone-deep@^4.0.1: version "4.0.1" @@ -1816,12 +1599,17 @@ convert-source-map@^1.7.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== -core-js-compat@^3.25.1: - version "3.27.2" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.27.2.tgz#607c50ad6db8fd8326af0b2883ebb987be3786da" - integrity sha512-welaYuF7ZtbYKGrIy7y3eb40d37rG1FvzEOfe7hSLd2iD6duMDqUhRfSvCGyC46HhR6Y8JXXdZ2lnRUMkPBpvg== +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +core-js-compat@^3.43.0: + version "3.46.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.46.0.tgz#0c87126a19a1af00371e12b02a2b088a40f3c6f7" + integrity sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law== dependencies: - browserslist "^4.21.4" + browserslist "^4.26.3" core-js@^3.27.2: version "3.27.2" @@ -1849,9 +1637,9 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: which "^2.0.1" css-declaration-sorter@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz#be5e1d71b7a992433fb1c542c7a1b835e45682ec" - integrity sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w== + version "6.4.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71" + integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g== css-loader@^6.7.3: version "6.7.3" @@ -1908,22 +1696,22 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -cssnano-preset-default@^5.2.13: - version "5.2.13" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.13.tgz#e7353b0c57975d1bdd97ac96e68e5c1b8c68e990" - integrity sha512-PX7sQ4Pb+UtOWuz8A1d+Rbi+WimBIxJTRyBdgGp1J75VU0r/HFQeLnMYgHiCAp6AR4rqrc7Y4R+1Rjk3KJz6DQ== +cssnano-preset-default@^5.2.14: + version "5.2.14" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz#309def4f7b7e16d71ab2438052093330d9ab45d8" + integrity sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A== dependencies: css-declaration-sorter "^6.3.1" cssnano-utils "^3.1.0" postcss-calc "^8.2.3" - postcss-colormin "^5.3.0" + postcss-colormin "^5.3.1" postcss-convert-values "^5.1.3" postcss-discard-comments "^5.1.2" postcss-discard-duplicates "^5.1.0" postcss-discard-empty "^5.1.1" postcss-discard-overridden "^5.1.0" postcss-merge-longhand "^5.1.7" - postcss-merge-rules "^5.1.3" + postcss-merge-rules "^5.1.4" postcss-minify-font-values "^5.1.0" postcss-minify-gradients "^5.1.1" postcss-minify-params "^5.1.4" @@ -1938,7 +1726,7 @@ cssnano-preset-default@^5.2.13: postcss-normalize-url "^5.1.0" postcss-normalize-whitespace "^5.1.1" postcss-ordered-values "^5.1.3" - postcss-reduce-initial "^5.1.1" + postcss-reduce-initial "^5.1.2" postcss-reduce-transforms "^5.1.0" postcss-svgo "^5.1.0" postcss-unique-selectors "^5.1.1" @@ -1949,11 +1737,11 @@ cssnano-utils@^3.1.0: integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== cssnano@^5.1.8: - version "5.1.14" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.14.tgz#07b0af6da73641276fe5a6d45757702ebae2eb05" - integrity sha512-Oou7ihiTocbKqi0J1bB+TRJIQX5RMR3JghA8hcWSw9mjBLQ5Y3RWqEDoYG3sRNlAbCIXpqMoZGbq5KDR3vdzgw== + version "5.1.15" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.15.tgz#ded66b5480d5127fcb44dac12ea5a983755136bf" + integrity sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw== dependencies: - cssnano-preset-default "^5.2.13" + cssnano-preset-default "^5.2.14" lilconfig "^2.0.3" yaml "^1.10.2" @@ -1964,12 +1752,12 @@ csso@^4.2.0: dependencies: css-tree "^1.1.2" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== +debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.4.1: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: - ms "2.1.2" + ms "^2.1.3" deep-is@^0.1.3: version "0.1.4" @@ -1977,9 +1765,9 @@ deep-is@^0.1.3: integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== deepmerge@^4.2.2: - version "4.3.0" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.0.tgz#65491893ec47756d44719ae520e0e2609233b59b" - integrity sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og== + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== doctrine@^3.0.0: version "3.0.0" @@ -2033,10 +1821,15 @@ editorconfig@^1.0.2: minimatch "9.0.1" semver "^7.5.3" +electron-to-chromium@^1.5.249, electron-to-chromium@^1.5.73: + version "1.5.254" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz#94b84c0a5faff94b334536090a9dec1c74b10130" + integrity sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg== + electron-to-chromium@^1.5.4: - version "1.5.13" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6" - integrity sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q== + version "1.5.123" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.123.tgz#fae5bdba0ba27045895176327aa79831aba0790c" + integrity sha512-refir3NlutEZqlKaBLK0tzlVLe5P2wDKS7UQt/3SpibizgsRAPOsqQC3ffw1nlv3ze5gjRQZYHoPymgVZkplFA== emojis-list@^3.0.0: version "3.0.0" @@ -2073,7 +1866,7 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== -escalade@^3.1.2: +escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== @@ -2110,7 +1903,7 @@ eslint-scope@5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.2.0, eslint-scope@^7.2.2: +eslint-scope@^7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== @@ -2123,26 +1916,21 @@ eslint-visitor-keys@^2.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" - integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== - -eslint-visitor-keys@^3.4.3: +eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8.40.0: - version "8.56.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" - integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== +eslint@^8.40.0, eslint@^8.44.0: + version "8.57.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.56.0" - "@humanwhocodes/config-array" "^0.11.13" + "@eslint/js" "8.57.1" + "@humanwhocodes/config-array" "^0.13.0" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" "@ungap/structured-clone" "^1.2.0" @@ -2177,61 +1965,7 @@ eslint@^8.40.0: strip-ansi "^6.0.1" text-table "^0.2.0" -eslint@^8.44.0: - version "8.44.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.44.0.tgz#51246e3889b259bbcd1d7d736a0c10add4f0e500" - integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.1.0" - "@eslint/js" "8.44.0" - "@humanwhocodes/config-array" "^0.11.10" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.2.0" - eslint-visitor-keys "^3.4.1" - espree "^9.6.0" - esquery "^1.4.2" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - graphemer "^1.4.0" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.3" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - -espree@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.0.tgz#80869754b1c6560f32e3b6929194a3fe07c5b82f" - integrity sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A== - dependencies: - acorn "^8.9.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" - -espree@^9.6.1: +espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -2241,9 +1975,9 @@ espree@^9.6.1: eslint-visitor-keys "^3.4.1" esquery@^1.4.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" - integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" @@ -2289,15 +2023,20 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-uri@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + fastest-levenshtein@^1.0.12: version "1.0.16" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== fastq@^1.6.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" - integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + version "1.19.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== dependencies: reusify "^1.0.4" @@ -2341,17 +2080,18 @@ find-up@^5.0.0: path-exists "^4.0.0" flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: - flatted "^3.1.0" + flatted "^3.2.9" + keyv "^4.5.3" rimraf "^3.0.2" -flatted@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +flatted@^3.2.9: + version "3.3.3" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== fraction.js@^4.2.0: version "4.2.0" @@ -2368,10 +2108,10 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== fuse.js@^6.6.2: version "6.6.2" @@ -2414,15 +2154,10 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - globals@^13.19.0: - version "13.20.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" - integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" @@ -2446,12 +2181,12 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: - function-bind "^1.1.1" + function-bind "^1.1.2" icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" @@ -2459,19 +2194,19 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== ignore@^5.2.0: - version "5.2.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" - integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== immutable@^4.0.0: version "4.2.4" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.4.tgz#83260d50889526b4b531a5e293709a77f7c55a2a" integrity sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w== -import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== +import-fresh@^3.2.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== dependencies: parent-module "^1.0.0" resolve-from "^4.0.0" @@ -2519,12 +2254,12 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-core-module@^2.9.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" - integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== +is-core-module@^2.16.1, is-core-module@^2.9.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== dependencies: - has "^1.0.3" + hasown "^2.0.2" is-extglob@^2.1.1: version "2.1.1" @@ -2565,12 +2300,12 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== -jest-util@^29.4.1: - version "29.4.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.4.1.tgz#2eeed98ff4563b441b5a656ed1a786e3abc3e4c4" - integrity sha512-bQy9FPGxVutgpN4VRc0hk6w7Hx/m6L53QxpDreTZgJd9gfx/AV2MjyPde9tGyZRINAUrSv57p2inGBu2dRLmkQ== +jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== dependencies: - "@jest/types" "^29.4.1" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" ci-info "^3.2.0" @@ -2587,12 +2322,12 @@ jest-worker@^27.4.5: supports-color "^8.0.0" jest-worker@^29.1.2: - version "29.4.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.4.1.tgz#7cb4a99a38975679600305650f86f4807460aab1" - integrity sha512-O9doU/S1EBe+yp/mstQ0VpPwpv0Clgn68TkNwGxL6/usX/KUW9Arnn4ag8C3jc6qHcXznhsT5Na1liYzAsuAbQ== + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== dependencies: "@types/node" "*" - jest-util "^29.4.1" + jest-util "^29.7.0" merge-stream "^2.0.0" supports-color "^8.0.0" @@ -2602,21 +2337,21 @@ js-tokens@^4.0.0: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== dependencies: argparse "^2.0.1" -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsesc@^3.0.2, jsesc@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" @@ -2638,11 +2373,18 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json5@^2.1.2, json5@^2.2.2: +json5@^2.1.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + kind-of@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -2662,9 +2404,9 @@ levn@^0.4.1: type-check "~0.4.0" lilconfig@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4" - integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== + version "2.1.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== lines-and-columns@^1.1.6: version "1.2.4" @@ -2783,12 +2525,12 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanoid@^3.3.6: +nanoid@^3.3.6, nanoid@^3.3.8: version "3.3.8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== @@ -2804,9 +2546,14 @@ neo-async@^2.6.2: integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== node-releases@^2.0.18: - version "2.0.18" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" - integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + +node-releases@^2.0.19, node-releases@^2.0.27: + version "2.0.27" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" + integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -2838,16 +2585,16 @@ once@^1.3.0: wrappy "1" optionator@^0.9.3: - version "0.9.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" - integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: - "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" + word-wrap "^1.2.5" p-limit@^2.2.0: version "2.3.0" @@ -2924,15 +2671,10 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picocolors@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" - integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== +picocolors@^1.0.0, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: version "2.3.1" @@ -2954,12 +2696,12 @@ postcss-calc@^8.2.3: postcss-selector-parser "^6.0.9" postcss-value-parser "^4.2.0" -postcss-colormin@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.0.tgz#3cee9e5ca62b2c27e84fce63affc0cfb5901956a" - integrity sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg== +postcss-colormin@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.1.tgz#86c27c26ed6ba00d96c79e08f3ffb418d1d1988f" + integrity sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ== dependencies: - browserslist "^4.16.6" + browserslist "^4.21.4" caniuse-api "^3.0.0" colord "^2.9.1" postcss-value-parser "^4.2.0" @@ -3009,10 +2751,10 @@ postcss-merge-longhand@^5.1.7: postcss-value-parser "^4.2.0" stylehacks "^5.1.1" -postcss-merge-rules@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.3.tgz#8f97679e67cc8d08677a6519afca41edf2220894" - integrity sha512-LbLd7uFC00vpOuMvyZop8+vvhnfRGpp2S+IMQKeuOZZapPRY4SMq5ErjQeHbHsjCUgJkRNrlU+LmxsKIqPKQlA== +postcss-merge-rules@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz#2f26fa5cacb75b1402e213789f6766ae5e40313c" + integrity sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g== dependencies: browserslist "^4.21.4" caniuse-api "^3.0.0" @@ -3150,10 +2892,10 @@ postcss-ordered-values@^5.1.3: cssnano-utils "^3.1.0" postcss-value-parser "^4.2.0" -postcss-reduce-initial@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.1.tgz#c18b7dfb88aee24b1f8e4936541c29adbd35224e" - integrity sha512-//jeDqWcHPuXGZLoolFrUXBDyuEGbr9S2rMo19bkTIjBQ4PqkaO+oI8wua5BOUxpfi97i3PCoInsiFIEBfkm9w== +postcss-reduce-initial@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz#798cd77b3e033eae7105c18c9d371d989e1382d6" + integrity sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg== dependencies: browserslist "^4.21.4" caniuse-api "^3.0.0" @@ -3165,7 +2907,7 @@ postcss-reduce-transforms@^5.1.0: dependencies: postcss-value-parser "^4.2.0" -postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: +postcss-selector-parser@^6.0.2: version "6.0.11" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== @@ -3173,6 +2915,14 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: + version "6.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-svgo@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" @@ -3193,7 +2943,7 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.2.14, postcss@^8.4.17, postcss@^8.4.19, postcss@^8.4.31: +postcss@^8.2.14, postcss@^8.4.19, postcss@^8.4.31: version "8.4.31" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== @@ -3202,6 +2952,15 @@ postcss@^8.2.14, postcss@^8.4.17, postcss@^8.4.19, postcss@^8.4.31: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.17: + version "8.5.2" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.2.tgz#e7b99cb9d2ec3e8dd424002e7c16517cb2b846bd" + integrity sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA== + dependencies: + nanoid "^3.3.8" + picocolors "^1.1.1" + source-map-js "^1.2.1" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -3213,9 +2972,9 @@ prettier@^2.8.3: integrity sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw== punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== queue-microtask@^1.2.2: version "1.2.3" @@ -3250,10 +3009,10 @@ redux@^4.2.0: dependencies: "@babel/runtime" "^7.9.2" -regenerate-unicode-properties@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" - integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== +regenerate-unicode-properties@^10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz#aa113812ba899b630658c7623466be71e1f86f66" + integrity sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g== dependencies: regenerate "^1.4.2" @@ -3262,46 +3021,39 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.11: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - -regenerator-transform@^0.15.1: - version "0.15.1" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" - integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== - dependencies: - "@babel/runtime" "^7.8.4" +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== regex-parser@^2.2.11: version "2.2.11" resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== -regexpu-core@^5.2.1: - version "5.2.2" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.2.2.tgz#3e4e5d12103b64748711c3aad69934d7718e75fc" - integrity sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw== +regexpu-core@^6.3.1: + version "6.4.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.4.0.tgz#3580ce0c4faedef599eccb146612436b62a176e5" + integrity sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA== dependencies: regenerate "^1.4.2" - regenerate-unicode-properties "^10.1.0" - regjsgen "^0.7.1" - regjsparser "^0.9.1" + regenerate-unicode-properties "^10.2.2" + regjsgen "^0.8.0" + regjsparser "^0.13.0" unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.1.0" + unicode-match-property-value-ecmascript "^2.2.1" -regjsgen@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.7.1.tgz#ee5ef30e18d3f09b7c369b76e7c2373ed25546f6" - integrity sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA== +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== -regjsparser@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" - integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== +regjsparser@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.13.0.tgz#01f8351335cf7898d43686bc74d2dd71c847ecc0" + integrity sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q== dependencies: - jsesc "~0.5.0" + jsesc "~3.1.0" require-from-string@^2.0.2: version "2.0.2" @@ -3336,7 +3088,7 @@ resolve-url-loader@^5.0.0: postcss "^8.2.14" source-map "0.6.1" -resolve@^1.14.2, resolve@^1.20.0: +resolve@^1.20.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -3345,10 +3097,19 @@ resolve@^1.14.2, resolve@^1.20.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.10: + version "1.22.11" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== + dependencies: + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== rimraf@^3.0.2: version "3.0.2" @@ -3396,30 +3157,28 @@ schema-utils@^3.1.1, schema-utils@^3.2.0: ajv-keywords "^3.5.2" schema-utils@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" - integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== + version "4.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.0.tgz#3b669f04f71ff2dfb5aba7ce2d5a9d79b35622c0" + integrity sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g== dependencies: "@types/json-schema" "^7.0.9" - ajv "^8.8.0" + ajv "^8.9.0" ajv-formats "^2.1.1" - ajv-keywords "^5.0.0" + ajv-keywords "^5.1.0" -semver@7.5.3, semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^7.3.8, semver@^7.5.3: +semver@^6.0.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.8, semver@^7.5.3: version "7.5.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== dependencies: lru-cache "^6.0.0" -serialize-javascript@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" - integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== - dependencies: - randombytes "^2.1.0" - -serialize-javascript@^6.0.1: +serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== @@ -3450,11 +3209,16 @@ source-list-map@^2.0.1: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: +"source-map-js@>=0.6.2 <2.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-js@^1.0.2, source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -3480,7 +3244,7 @@ strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -3568,11 +3332,6 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -3592,10 +3351,15 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== unicode-match-property-ecmascript@^2.0.0: version "2.0.0" @@ -3605,23 +3369,31 @@ unicode-match-property-ecmascript@^2.0.0: unicode-canonical-property-names-ecmascript "^2.0.0" unicode-property-aliases-ecmascript "^2.0.0" -unicode-match-property-value-ecmascript@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" - integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== +unicode-match-property-value-ecmascript@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz#65a7adfad8574c219890e219285ce4c64ed67eaa" + integrity sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg== unicode-property-aliases-ecmascript@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" - integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz#301d4f8a43d2b75c97adfad87c9dd5350c9475d1" + integrity sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ== update-browserslist-db@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" - integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== dependencies: - escalade "^3.1.2" - picocolors "^1.0.1" + escalade "^3.2.0" + picocolors "^1.1.1" + +update-browserslist-db@^1.1.1, update-browserslist-db@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz#7802aa2ae91477f255b86e0e46dbc787a206ad4a" + integrity sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" uri-js@^4.2.2: version "4.4.1" @@ -3732,6 +3504,11 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
' @@ -70,30 +67,30 @@ function admin_user() . '
${element.dataset.confirm_submit_text ?? ''}
{{ __('schedule.import.text') }}
{{ __('user.goodie_score', [score]) }}
{{ __('angeltypes.about.text') }}
{{ __('angeltypes.about.text', [url('/faq')])|raw }}
+ + {{ __('angeltypes.qr.expires') }} + +
{{ __('registration.jobs', [url('/angeltypes/about')])|raw }}
<b>bold</b>
bold
Test
https://example.com/link
<i>Lorem</i> "Ipsum"
Lorem "Ipsum"