diff --git a/composer.json b/composer.json index b2739e47db6..08408c747c2 100644 --- a/composer.json +++ b/composer.json @@ -62,13 +62,13 @@ "symfony/http-client": "^6.0.3|^7.0", "symfony/property-access": "^7.0", "symfony/property-info": "^7.0", - "symfony/serializer": "^6.4", + "symfony/serializer": "^6.4|^7.0", "symfony/var-dumper": "^5.0|^6.0|^7.0", "symfony/yaml": "^5.2.3|^6.0|^7.0", "theiconic/name-parser": "^1.2", "twig/twig": "~3.21.1", "voku/portable-ascii": "^2.0", - "web-auth/webauthn-lib": "~4.9.0", + "web-auth/webauthn-lib": "~5.2.4", "webonyx/graphql-php": "~14.11.10", "yiisoft/yii2": "~2.0.54.0", "yiisoft/yii2-debug": "~2.1.27.0", diff --git a/composer.lock b/composer.lock index 009d0f5e4c7..2a6eccdfef4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "672196a587361e988f30a5cf6e1d8350", + "content-hash": "d687c373f5825f70000cf0e083751595", "packages": [ { "name": "bacon/bacon-qr-code", @@ -1770,70 +1770,6 @@ }, "time": "2025-09-08T19:05:53+00:00" }, - { - "name": "lcobucci/clock", - "version": "3.3.1", - "source": { - "type": "git", - "url": "https://github.com/lcobucci/clock.git", - "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/lcobucci/clock/zipball/db3713a61addfffd615b79bf0bc22f0ccc61b86b", - "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b", - "shasum": "" - }, - "require": { - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "psr/clock": "^1.0" - }, - "provide": { - "psr/clock-implementation": "1.0" - }, - "require-dev": { - "infection/infection": "^0.29", - "lcobucci/coding-standard": "^11.1.0", - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.10.25", - "phpstan/phpstan-deprecation-rules": "^1.1.3", - "phpstan/phpstan-phpunit": "^1.3.13", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^11.3.6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Lcobucci\\Clock\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Luís Cobucci", - "email": "lcobucci@gmail.com" - } - ], - "description": "Yet another clock abstraction", - "support": { - "issues": "https://github.com/lcobucci/clock/issues", - "source": "https://github.com/lcobucci/clock/tree/3.3.1" - }, - "funding": [ - { - "url": "https://github.com/lcobucci", - "type": "github" - }, - { - "url": "https://www.patreon.com/lcobucci", - "type": "patreon" - } - ], - "time": "2024-09-24T20:45:14+00:00" - }, { "name": "league/uri", "version": "7.8.0", @@ -4015,6 +3951,84 @@ ], "time": "2025-12-20T12:57:40+00:00" }, + { + "name": "symfony/clock", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "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": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.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-11-12T15:39:26+00:00" + }, { "name": "symfony/css-selector", "version": "v7.4.0", @@ -6849,44 +6863,40 @@ }, { "name": "web-auth/webauthn-lib", - "version": "4.9.3", + "version": "5.2.4", "source": { "type": "git", "url": "https://github.com/web-auth/webauthn-lib.git", - "reference": "129fbaccd22163429a39bf85e320fb9eddad035c" + "reference": "c346c9812d4d4a641f5ff26cd5fa4d0bf2035eeb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/129fbaccd22163429a39bf85e320fb9eddad035c", - "reference": "129fbaccd22163429a39bf85e320fb9eddad035c", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/c346c9812d4d4a641f5ff26cd5fa4d0bf2035eeb", + "reference": "c346c9812d4d4a641f5ff26cd5fa4d0bf2035eeb", "shasum": "" }, "require": { "ext-json": "*", - "ext-mbstring": "*", "ext-openssl": "*", - "lcobucci/clock": "^2.2|^3.0", "paragonie/constant_time_encoding": "^2.6|^3.0", - "php": ">=8.1", + "php": ">=8.2", + "phpdocumentor/reflection-docblock": "^5.3", "psr/clock": "^1.0", "psr/event-dispatcher": "^1.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", "psr/log": "^1.0|^2.0|^3.0", "spomky-labs/cbor-php": "^3.0", "spomky-labs/pki-framework": "^1.0", + "symfony/clock": "^6.4|^7.0", "symfony/deprecation-contracts": "^3.2", - "symfony/uid": "^6.1|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", "web-auth/cose-lib": "^4.2.3" }, "suggest": { - "phpdocumentor/reflection-docblock": "As of 4.5.x, the phpdocumentor/reflection-docblock component will become mandatory for converting objects such as the Metadata Statement", - "psr/clock-implementation": "As of 4.5.x, the PSR Clock implementation will replace lcobucci/clock", "psr/log-implementation": "Recommended to receive logs from the library", "symfony/event-dispatcher": "Recommended to use dispatched events", - "symfony/property-access": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", - "symfony/property-info": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", - "symfony/serializer": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", "web-token/jwt-library": "Mandatory for fetching Metadata Statement from distant sources" }, "type": "library", @@ -6923,7 +6933,7 @@ "webauthn" ], "support": { - "source": "https://github.com/web-auth/webauthn-lib/tree/4.9.3" + "source": "https://github.com/web-auth/webauthn-lib/tree/5.2.4" }, "funding": [ { @@ -6935,7 +6945,7 @@ "type": "patreon" } ], - "time": "2026-02-05T12:48:16+00:00" + "time": "2026-03-08T17:01:15+00:00" }, { "name": "webmozart/assert", @@ -11304,9 +11314,9 @@ "ext-pdo": "*", "ext-zip": "*" }, - "platform-dev": {}, + "platform-dev": [], "platform-overrides": { "php": "8.2" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.3.0" } diff --git a/package-lock.json b/package-lock.json index fa1ee224078..a8256769d33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@craftcms/vue": "file:packages/craftcms-vue", "@floating-ui/dom": "^1.6.3", "@selectize/selectize": "selectize/selectize.js#master", - "@simplewebauthn/browser": "^7.1.0", + "@simplewebauthn/browser": "^13.2.0", "@types/jquery": "^3.5.7", "accounting": "^0.4.1", "axios": "^1.12.2", @@ -3391,17 +3391,10 @@ } }, "node_modules/@simplewebauthn/browser": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-7.4.0.tgz", - "integrity": "sha512-qqCZ99lFWjtyza8NCtCpRm3GU5u8/QFeBfMgW5+U/E8Qyc4lvUcuJ8JTbrhksVQLZWSY1c/6Xw11QZ5e+D1hNw==", - "dependencies": { - "@simplewebauthn/typescript-types": "^7.4.0" - } - }, - "node_modules/@simplewebauthn/typescript-types": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@simplewebauthn/typescript-types/-/typescript-types-7.4.0.tgz", - "integrity": "sha512-8/ZjHeUPe210Bt5oyaOIGx4h8lHdsQs19BiOT44gi/jBEgK7uBGA0Fy7NRsyh777al3m6WM0mBf0UR7xd4R7WQ==" + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", + "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", + "license": "MIT" }, "node_modules/@sinclair/typebox": { "version": "0.27.8", @@ -18206,17 +18199,9 @@ "requires": {} }, "@simplewebauthn/browser": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-7.4.0.tgz", - "integrity": "sha512-qqCZ99lFWjtyza8NCtCpRm3GU5u8/QFeBfMgW5+U/E8Qyc4lvUcuJ8JTbrhksVQLZWSY1c/6Xw11QZ5e+D1hNw==", - "requires": { - "@simplewebauthn/typescript-types": "^7.4.0" - } - }, - "@simplewebauthn/typescript-types": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@simplewebauthn/typescript-types/-/typescript-types-7.4.0.tgz", - "integrity": "sha512-8/ZjHeUPe210Bt5oyaOIGx4h8lHdsQs19BiOT44gi/jBEgK7uBGA0Fy7NRsyh777al3m6WM0mBf0UR7xd4R7WQ==" + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", + "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==" }, "@sinclair/typebox": { "version": "0.27.8", diff --git a/package.json b/package.json index 918cf86b8d4..567104c8db1 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@craftcms/vue": "file:packages/craftcms-vue", "@floating-ui/dom": "^1.6.3", "@selectize/selectize": "selectize/selectize.js#master", - "@simplewebauthn/browser": "^7.1.0", + "@simplewebauthn/browser": "^13.2.0", "@types/jquery": "^3.5.7", "accounting": "^0.4.1", "axios": "^1.12.2", diff --git a/src/auth/passkeys/CredentialRepository.php b/src/auth/passkeys/CredentialRepository.php index efdbe4887d6..f765f38d3a5 100644 --- a/src/auth/passkeys/CredentialRepository.php +++ b/src/auth/passkeys/CredentialRepository.php @@ -14,7 +14,6 @@ use craft\records\WebAuthn; use ParagonIE\ConstantTime\Base64UrlSafe; use Webauthn\PublicKeyCredentialSource; -use Webauthn\PublicKeyCredentialSourceRepository; use Webauthn\PublicKeyCredentialUserEntity; /** @@ -23,35 +22,56 @@ * @author Pixel & Tonic, Inc. * @since 5.0.0 */ -class CredentialRepository implements PublicKeyCredentialSourceRepository +class CredentialRepository { /** - * @inheritdoc + * Finds a webauthn record in the database for given id and returns the PublicKeyCredentialSource for its credential value. */ - public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource + public function findOneByCredentialId(string $publicKeyCredentialId, bool $checkOldUserHandle = false): ?PublicKeyCredentialSource { $record = $this->_findByCredentialId($publicKeyCredentialId); if ($record) { - return PublicKeyCredentialSource::createFromArray(Json::decodeIfJson($record->credential)); + $serializer = Craft::$app->getAuth()->webauthnServer()->getSerializer(); + + $publicKeyCredentialSource = $serializer->deserialize( + $record->credential, + PublicKeyCredentialSource::class, + 'json', + ); + + // if the record was created using webauthn v4 then the credential was run through Json::encode() before storing in the DB + // deserialising such value base64 decodes the userHandle too, and leads to user handle mismatch; + // so, if we failed to log user in based on the handle mismatch exception, we'll try again but using the encoded (old) handle + if ($checkOldUserHandle) { + $credential = Json::decodeIfJson($record->credential); + $publicKeyCredentialSource->userHandle = $credential['userHandle']; + } + + return $publicKeyCredentialSource; } return null; } /** - * @inheritdoc + * Finds all webauthn records for given user and returns an array of PublicKeyCredentialSources for their credential values. */ public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array { // Get the user ID by their UID. - $user = Craft::$app->getUsers()->getUserByUid($publicKeyCredentialUserEntity->getId()); + $user = Craft::$app->getUsers()->getUserByUid($publicKeyCredentialUserEntity->id); $keySources = []; if ($user && $user->id) { $records = WebAuthn::findAll(['userId' => $user->id]); + $serializer = Craft::$app->getAuth()->webauthnServer()->getSerializer(); foreach ($records as $record) { - $keySources[] = PublicKeyCredentialSource::createFromArray(Json::decodeIfJson($record->credential)); + $keySources[] = $serializer->deserialize( + $record->credential, + PublicKeyCredentialSource::class, + 'json', + ); } } @@ -66,7 +86,7 @@ public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCre */ public function savedNamedCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $credentialName = null): void { - $publicKeyCredentialId = $publicKeyCredentialSource->getPublicKeyCredentialId(); + $publicKeyCredentialId = $publicKeyCredentialSource->publicKeyCredentialId; $record = $this->_findByCredentialId($publicKeyCredentialId); if (!$record) { @@ -77,12 +97,12 @@ public function savedNamedCredentialSource(PublicKeyCredentialSource $publicKeyC } $record->dateLastUsed = Db::prepareDateForDb(DateTimeHelper::currentTimeStamp()); - $record->credential = Json::encode($publicKeyCredentialSource); + $record->credential = Craft::$app->getAuth()->webauthnServer()->getSerializer()->serialize($publicKeyCredentialSource, 'json'); $record->save(); } /** - * @inheritdoc + * Saves credential source in the database */ public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void { diff --git a/src/auth/passkeys/WebauthnServer.php b/src/auth/passkeys/WebauthnServer.php index e56eee5ff3e..677a4d17309 100644 --- a/src/auth/passkeys/WebauthnServer.php +++ b/src/auth/passkeys/WebauthnServer.php @@ -24,10 +24,9 @@ use Webauthn\AuthenticatorAssertionResponseValidator; use Webauthn\AuthenticatorAttestationResponseValidator; use Webauthn\AuthenticatorSelectionCriteria; +use Webauthn\CeremonyStep\CeremonyStepManagerFactory; use Webauthn\Denormalizer\WebauthnSerializerFactory; -use Webauthn\PublicKeyCredentialLoader; use Webauthn\PublicKeyCredentialParameters; -use Webauthn\TokenBinding\IgnoreTokenBindingHandler; /** * Webauthn server. @@ -38,17 +37,15 @@ */ class WebauthnServer { - /** - * Returns the token binding handler. - * - * > At the time of writing, we recommend to ignore this feature. - * - * @return IgnoreTokenBindingHandler - * @see https://webauthn-doc.spomky-labs.com/v/v4.5/pure-php/the-hard-way#token-binding-handler - */ - public function getTokenBindingHandler(): IgnoreTokenBindingHandler + private CeremonyStepManagerFactory $csmFactory; + private ?SerializerInterface $serializer = null; + private ?CredentialRepository $credentialRepository = null; + + public function __construct() { - return IgnoreTokenBindingHandler::create(); + $this->csmFactory = new CeremonyStepManagerFactory(); + $this->csmFactory->setAlgorithmManager($this->getAlgorithmManager()); + $this->csmFactory->setExtensionOutputCheckerHandler($this->getExtensionOutputCheckerHandler()); } /** @@ -73,25 +70,25 @@ public function getAttestationStatementManager(): AttestationStatementSupportMan * * @return AttestationObjectLoader * @see https://webauthn-doc.spomky-labs.com/pure-php/the-hard-way#attestation-object-loader + * @deprecated in 5.9.16 */ public function getAttestationObjectLoader(): AttestationObjectLoader { - return AttestationObjectLoader::create( - $this->getAttestationStatementManager() - ); + return AttestationObjectLoader::create($this->getAttestationStatementManager()); } /** - * Returns the object that will load the Public Key. + * Returns the credential repository. * - * @return PublicKeyCredentialLoader - * @see https://webauthn-doc.spomky-labs.com/pure-php/the-hard-way#public-key-credential-loader + * @return CredentialRepository */ - public function getPublicKeyCredentialLoader(): PublicKeyCredentialLoader + public function getCredentialRepository(): CredentialRepository { - return PublicKeyCredentialLoader::create( - $this->getAttestationObjectLoader() - ); + if ($this->credentialRepository === null) { + $this->credentialRepository = new CredentialRepository(); + } + + return $this->credentialRepository; } /** @@ -102,11 +99,14 @@ public function getPublicKeyCredentialLoader(): PublicKeyCredentialLoader */ public function getSerializer(): SerializerInterface { - $attestationStatementSupportManager = AttestationStatementSupportManager::create(); - $attestationStatementSupportManager->add(NoneAttestationStatementSupport::create()); - $factory = new WebauthnSerializerFactory($attestationStatementSupportManager); - - return $factory->create(); + if ($this->serializer == null) { + $attestationStatementSupportManager = AttestationStatementSupportManager::create(); + $attestationStatementSupportManager->add(NoneAttestationStatementSupport::create()); + $factory = new WebauthnSerializerFactory($attestationStatementSupportManager); + $this->serializer = $factory->create(); + } + + return $this->serializer; } /** @@ -156,12 +156,7 @@ public function getAlgorithmManager(): Manager */ public function getAuthenticatorAttestationResponseValidator(): AuthenticatorAttestationResponseValidator { - return AuthenticatorAttestationResponseValidator::create( - $this->getAttestationStatementManager(), - new CredentialRepository(), - $this->getTokenBindingHandler(), - $this->getExtensionOutputCheckerHandler(), - ); + return AuthenticatorAttestationResponseValidator::create(ceremonyStepManager: $this->csmFactory->creationCeremony()); } /** @@ -172,12 +167,7 @@ public function getAuthenticatorAttestationResponseValidator(): AuthenticatorAtt */ public function getAuthenticatorAssertionResponseValidator(): AuthenticatorAssertionResponseValidator { - return AuthenticatorAssertionResponseValidator::create( - new CredentialRepository(), - $this->getTokenBindingHandler(), - $this->getExtensionOutputCheckerHandler(), - $this->getAlgorithmManager(), - ); + return AuthenticatorAssertionResponseValidator::create(ceremonyStepManager: $this->csmFactory->requestCeremony()); } /** @@ -208,7 +198,6 @@ public function getPasskeyAuthenticatorSelectionCriteria(): AuthenticatorSelecti authenticatorAttachment: null, userVerification: AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED, residentKey: AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_REQUIRED, - requireResidentKey: true, ); } } diff --git a/src/controllers/AuthController.php b/src/controllers/AuthController.php index 36d238575fc..cdd586030ca 100644 --- a/src/controllers/AuthController.php +++ b/src/controllers/AuthController.php @@ -177,10 +177,14 @@ public function actionPasskeyCreationOptions(): Response $this->requirePostRequest(); $this->requireElevatedSession(); - $options = Craft::$app->getAuth()->getPasskeyCreationOptions(static::currentUser()); + $authService = Craft::$app->getAuth(); + $options = $authService->getPasskeyCreationOptions(static::currentUser()); + + $serializer = $authService->webauthnServer()->getSerializer(); + $serializedData = $serializer->serialize($options, 'json'); return $this->asJson([ - 'options' => $options, + 'options' => $serializedData, ]); } @@ -194,10 +198,13 @@ public function actionPasskeyRequestOptions(): Response $this->requirePostRequest(); $this->requireAcceptsJson(); - $options = Craft::$app->getAuth()->getPasskeyRequestOptions(); + $authService = Craft::$app->getAuth(); + $options = $authService->getPasskeyRequestOptions(); + $serializer = $authService->webauthnServer()->getSerializer(); + $serializedData = $serializer->serialize($options, 'json'); return $this->asJson([ - 'options' => $options, + 'options' => $serializedData, ]); } diff --git a/src/controllers/UsersController.php b/src/controllers/UsersController.php index d50e284df1d..8caabace58a 100644 --- a/src/controllers/UsersController.php +++ b/src/controllers/UsersController.php @@ -34,7 +34,6 @@ use craft\helpers\Html; use craft\helpers\Image; use craft\helpers\Json; -use craft\helpers\Session; use craft\helpers\StringHelper; use craft\helpers\UrlHelper; use craft\helpers\User as UserHelper; @@ -316,7 +315,9 @@ public function actionLoginWithPasskey(): ?Response $duration = Craft::$app->getConfig()->getGeneral()->userSessionDuration; + // PublicKeyCredentialRequestOptions $requestOptions = $this->request->getRequiredBodyParam('requestOptions'); + // PublicKeyCredential $response = $this->request->getRequiredBodyParam('response'); $credential = WebAuthnRecord::findOne(['credentialId' => Json::decode($response)['id']]); diff --git a/src/elements/User.php b/src/elements/User.php index 7976bada3b1..026135448a7 100644 --- a/src/elements/User.php +++ b/src/elements/User.php @@ -57,6 +57,7 @@ use DateTime; use DateTimeZone; use Throwable; +use Webauthn\Exception\InvalidUserHandleException; use Webauthn\PublicKeyCredentialRequestOptions; use yii\base\Exception; use yii\base\InvalidArgumentException; @@ -1408,6 +1409,8 @@ public function authenticate(string $password): bool * @param string $response The authentication response data * @return bool * @since 5.0.0 + * + * TODO: change the signature in v6 - $requestOptions only really accepts a string */ public function authenticateWithPasskey( PublicKeyCredentialRequestOptions|array|string $requestOptions, @@ -1439,6 +1442,8 @@ public function authenticateWithPasskey( // Validate the security key try { $keyValid = Craft::$app->getAuth()->verifyPasskey($this, $requestOptions, $response); + } catch (InvalidUserHandleException $e) { + $keyValid = Craft::$app->getAuth()->verifyPasskey($this, $requestOptions, $response, true); } catch (InvalidArgumentException) { $keyValid = false; } diff --git a/src/services/Auth.php b/src/services/Auth.php index 241fe32450d..a8e5384d606 100644 --- a/src/services/Auth.php +++ b/src/services/Auth.php @@ -11,7 +11,6 @@ use craft\auth\methods\AuthMethodInterface; use craft\auth\methods\RecoveryCodes; use craft\auth\methods\TOTP; -use craft\auth\passkeys\CredentialRepository; use craft\auth\passkeys\WebauthnServer; use craft\elements\User; use craft\enums\CmsEdition; @@ -19,7 +18,6 @@ use craft\helpers\ArrayHelper; use craft\helpers\Component as ComponentHelper; use craft\helpers\DateTimeHelper; -use craft\helpers\Json; use craft\helpers\Session as SessionHelper; use craft\helpers\User as UserHelper; use craft\models\UserGroup; @@ -27,12 +25,11 @@ use craft\web\Session; use craft\web\View; use DateTime; -use GuzzleHttp\Psr7\ServerRequest; use ParagonIE\ConstantTime\Base64UrlSafe; -use Psr\Http\Message\ServerRequestInterface; use Throwable; use Webauthn\AuthenticatorAssertionResponse; use Webauthn\AuthenticatorAttestationResponse; +use Webauthn\Exception\InvalidUserHandleException; use Webauthn\PublicKeyCredential; use Webauthn\PublicKeyCredentialCreationOptions; use Webauthn\PublicKeyCredentialOptions; @@ -467,7 +464,7 @@ public function getPasskeyCreationOptions(User $user): PublicKeyCredentialOption $excludeCredentials = array_map( fn(PublicKeyCredentialSource $credential) => $credential->getPublicKeyCredentialDescriptor(), - (new CredentialRepository())->findAllForUserEntity($userEntity), + $this->webauthnServer()->getCredentialRepository()->findAllForUserEntity($userEntity), ); $publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions::create( @@ -480,7 +477,9 @@ public function getPasskeyCreationOptions(User $user): PublicKeyCredentialOption excludeCredentials: $excludeCredentials ); - SessionHelper::set($this->passkeyCreationOptionsParam, Json::encode($publicKeyCredentialCreationOptions)); + $serializer = $this->webauthnServer()->getSerializer(); + $serialisedData = $serializer->serialize($publicKeyCredentialCreationOptions, 'json'); + SessionHelper::set($this->passkeyCreationOptionsParam, $serialisedData); return $publicKeyCredentialCreationOptions; } @@ -502,7 +501,11 @@ public function verifyPasskeyCreationResponse(string $credentials, ?string $cred $serializer = $this->webauthnServer()->getSerializer(); - $publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions::createFromArray(Json::decode($optionsJson)); + $publicKeyCredentialCreationOptions = $serializer->deserialize( + $optionsJson, + PublicKeyCredentialCreationOptions::class, + 'json', + ); $publicKeyCredential = $serializer->deserialize( $credentials, PublicKeyCredential::class, @@ -526,8 +529,7 @@ public function verifyPasskeyCreationResponse(string $credentials, ?string $cred return false; } - $credentialRepository = new CredentialRepository(); - $credentialRepository->savedNamedCredentialSource($publicKeyCredentialSource, $credentialName); + $this->webauthnServer()->getCredentialRepository()->savedNamedCredentialSource($publicKeyCredentialSource, $credentialName); return true; } @@ -552,20 +554,28 @@ public function getPasskeyRequestOptions(): PublicKeyCredentialRequestOptions * @param PublicKeyCredentialRequestOptions|array|string $requestOptions The public key credential request options * @param string $response The authentication response data * @return bool + * + * TODO: change the signature in v6 - $requestOptions only really accepts a string */ public function verifyPasskey( User $user, PublicKeyCredentialRequestOptions|array|string $requestOptions, string $response, + bool $checkOldUserHandle = false, ): bool { - if (is_array($requestOptions)) { - $requestOptions = PublicKeyCredentialRequestOptions::createFromArray($requestOptions); - } elseif (is_string($requestOptions)) { - $requestOptions = PublicKeyCredentialRequestOptions::createFromString($requestOptions); - } + $serializer = $this->webauthnServer()->getSerializer(); + $publicKeyCredentialRequestOptions = $serializer->deserialize( + $requestOptions, + PublicKeyCredentialRequestOptions::class, + 'json', + ); $userEntity = $this->passkeyUserEntity($user); - $publicKeyCredential = $this->webauthnServer()->getPublicKeyCredentialLoader()->load($response); + $publicKeyCredential = $serializer->deserialize( + $response, + PublicKeyCredential::class, + 'json', + ); $authenticatorAssertionResponse = $publicKeyCredential->response; if (!$authenticatorAssertionResponse instanceof AuthenticatorAssertionResponse) { @@ -573,15 +583,26 @@ public function verifyPasskey( return false; } - $serverRequest = $this->buildServerRequest(ServerRequest::fromGlobals()); + $publicKeyCredentialSource = $this->webauthnServer()->getCredentialRepository()->findOneByCredentialId( + $publicKeyCredential->rawId, + $checkOldUserHandle, + ); + + if ($publicKeyCredentialSource === null) { + Craft::warning('No publicKeyCredential source was found.'); + return false; + } + try { $this->webauthnServer()->getAuthenticatorAssertionResponseValidator()->check( - $publicKeyCredential->rawId, + $publicKeyCredentialSource, $authenticatorAssertionResponse, - $requestOptions, - $serverRequest, + $publicKeyCredentialRequestOptions, + Craft::$app->getRequest()->getHostName(), $userEntity->id, ); + } catch (InvalidUserHandleException $e) { + throw $e; } catch (Throwable $e) { Craft::warning('Authenticator Assertion Response Validation failed: ' . $e->getMessage()); return false; @@ -606,7 +627,7 @@ public function deletePasskey(User $user, string $uid): void * * @return WebauthnServer */ - private function webauthnServer(): WebauthnServer + public function webauthnServer(): WebauthnServer { if (!isset($this->_webauthnServer)) { $this->_webauthnServer = new WebauthnServer(); @@ -623,13 +644,11 @@ private function webauthnServer(): WebauthnServer */ private function passkeyUserEntity(User $user): PublicKeyCredentialUserEntity { - $data = [ - 'name' => $user->email, - 'id' => Base64UrlSafe::encodeUnpadded($user->uid), - 'displayName' => $user->getName(), - ]; - - return PublicKeyCredentialUserEntity::createFromArray($data); + return PublicKeyCredentialUserEntity::create( + $user->email, + Base64UrlSafe::encodeUnpadded($user->uid), + $user->getName(), + ); } /** @@ -639,38 +658,9 @@ private function passkeyUserEntity(User $user): PublicKeyCredentialUserEntity */ private function passkeyRpEntity(): PublicKeyCredentialRpEntity { - return PublicKeyCredentialRpEntity::createFromArray([ - 'name' => Craft::$app->getSystemName(), - 'id' => Craft::$app->getRequest()->getHostName(), - ]); - } - - /** - * Builds server request using the Craft-provided data, e.g. host name. - * - * - * @param ServerRequestInterface $defaultServerRequest - * @return ServerRequestInterface - */ - private function buildServerRequest(ServerRequestInterface $defaultServerRequest): ServerRequestInterface - { - $uri = $defaultServerRequest->getUri(); - $uri = $uri->withHost(Craft::$app->getRequest()->getHostName()); - - $serverRequest = new ServerRequest( - $defaultServerRequest->getMethod(), - $uri, - $defaultServerRequest->getHeaders(), - $defaultServerRequest->getBody(), - $defaultServerRequest->getProtocolVersion(), - $_SERVER + return PublicKeyCredentialRpEntity::create( + Craft::$app->getSystemName(), + Craft::$app->getRequest()->getHostName(), ); - - - return $serverRequest - ->withCookieParams($_COOKIE) - ->withQueryParams($_GET) - ->withParsedBody($_POST) - ->withUploadedFiles(ServerRequest::normalizeFiles($_FILES)); } } diff --git a/src/web/assets/cp/dist/cp.js b/src/web/assets/cp/dist/cp.js index 143ae0af6da..b64b2ea5922 100644 --- a/src/web/assets/cp/dist/cp.js +++ b/src/web/assets/cp/dist/cp.js @@ -1,3 +1,3 @@ /*! For license information please see cp.js.LICENSE.txt */ -(function(){var __webpack_modules__={0:function(){function t(t,n){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=n){var i,r,a,s,o=[],l=!0,c=!1;try{if(a=(n=n.call(t)).next,0===e){if(Object(n)!==n)return;l=!1}else for(;!(l=(i=a.call(n)).done)&&(o.push(i.value),o.length!==e);l=!0);}catch(t){c=!0,r=t}finally{try{if(!l&&null!=n.return&&(s=n.return(),Object(s)!==s))return}finally{if(c)throw r}}return o}}(t,n)||function(t,n){if(t){if("string"==typeof t)return e(t,n);var i={}.toString.call(t).slice(8,-1);return"Object"===i&&t.constructor&&(i=t.constructor.name),"Map"===i||"Set"===i?Array.from(t):"Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i)?e(t,n):void 0}}(t,n)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function e(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,i=Array(e);nn.settings.maxFileSize&&(n._rejectedFiles.size.push("“"+t.name+"”"),r=!1),r&&"function"==typeof n.settings.canAddMoreFiles&&!n.settings.canAddMoreFiles(n._validFileCounter)&&(n._rejectedFiles.limit.push("“"+t.name+"”"),r=!1),r&&(n._validFileCounter++,e.submit()),++n._totalFileCounter===e.originalFiles.length&&(n._totalFileCounter=0,n._validFileCounter=0,n.processErrorMessages())})),!0},destroy:function(){var e=this;this.uploader.fileupload("instance")&&this.uploader.fileupload("destroy"),this.$element.off("fileuploadadd",this._onFileAdd),Object.entries(this.events).forEach((function(n){var i=t(n,2),r=i[0],a=i[1];e.$element.off(r,a)}))}},{defaults:{autoUpload:!1,sequentialUploads:!0,maxFileSize:Craft.maxUploadSize,replaceFileInput:!1,createAction:"assets/upload",replaceAction:"assets/replace-file",deleteAction:"assets/delete-asset"}})},9:function(){Craft.Structure=Garnish.Base.extend({id:null,$container:null,state:null,structureDrag:null,init:function(t,e,n){this.id=t,this.$container=$(e),this.setSettings(n,Craft.Structure.defaults),this.$container.data("structure")&&(console.warn("Double-instantiating a structure on an element"),this.$container.data("structure").destroy()),this.$container.data("structure",this),this.state={},this.settings.storageKey&&$.extend(this.state,Craft.getLocalStorage(this.settings.storageKey,{})),void 0===this.state.collapsedElementIds&&(this.state.collapsedElementIds=[]);for(var i=this.$container.find("ul").prev(".row"),r=0;r').prependTo(a);-1!==$.inArray(a.children(".element").data("id"),this.state.collapsedElementIds)&&s.addClass("collapsed"),this.initToggle(o)}this.settings.sortable&&(this.structureDrag=new Craft.StructureDrag(this,this.settings.maxLevels)),this.settings.newChildUrl&&this.initNewChildMenus(this.$container.find(".add"))},initToggle:function(t){var e=this;t.on("click",(function(t){var n=$(t.currentTarget).closest("li"),i=n.children(".row").find(".element:first").data("id"),r=$.inArray(i,e.state.collapsedElementIds);n.hasClass("collapsed")?(n.removeClass("collapsed"),-1!==r&&e.state.collapsedElementIds.splice(r,1)):(n.addClass("collapsed"),-1===r&&e.state.collapsedElementIds.push(i)),e.settings.storageKey&&Craft.setLocalStorage(e.settings.storageKey,e.state)}))},initNewChildMenus:function(t){this.addListener(t,"click","onNewChildMenuClick")},onNewChildMenuClick:function(t){var e=$(t.currentTarget);if(!e.data("menubtn")){var n=e.parent().children(".element").data("id"),i=Craft.getUrl(this.settings.newChildUrl,"parentId="+n);$('").insertAfter(e),new Garnish.MenuBtn(e).showMenu()}},getIndent:function(t){return Craft.Structure.baseIndent+(t-1)*Craft.Structure.nestedIndent},addElement:function(t){var e=$('
  • ').appendTo(this.$container),n=$('
    ').appendTo(e);if(n.append(t),this.settings.sortable&&(n.append(''),this.structureDrag.addItems(e)),this.settings.newChildUrl){var i=$('').appendTo(n);this.initNewChildMenus(i)}n.css("margin-bottom",-30),n.velocity({"margin-bottom":0},"fast")},removeElement:function(t){var e,n=this,i=t.parent().parent();this.settings.sortable&&this.structureDrag.removeItems(i),i.siblings().length||(e=i.parent()),i.css("visibility","hidden").velocity({marginBottom:-i.height()},"fast",(function(){i.remove(),void 0!==e&&n._removeUl(e)}))},_removeUl:function(t){t.siblings(".row").children(".toggle").remove(),t.remove()},destroy:function(){this.$container.removeData("structure"),this.base()}},{baseIndent:8,nestedIndent:35,defaults:{storageKey:null,sortable:!1,newChildUrl:null,maxLevels:null}})},146:function(){function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}function e(){"use strict";e=function(){return i};var n,i={},r=Object.prototype,a=r.hasOwnProperty,s=Object.defineProperty||function(t,e,n){t[e]=n.value},o="function"==typeof Symbol?Symbol:{},l=o.iterator||"@@iterator",c=o.asyncIterator||"@@asyncIterator",u=o.toStringTag||"@@toStringTag";function h(t,e,n){return Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{h({},"")}catch(n){h=function(t,e,n){return t[e]=n}}function d(t,e,n,i){var r=e&&e.prototype instanceof b?e:b,a=Object.create(r.prototype),o=new A(i||[]);return s(a,"_invoke",{value:T(t,n,o)}),a}function f(t,e,n){try{return{type:"normal",arg:t.call(e,n)}}catch(t){return{type:"throw",arg:t}}}i.wrap=d;var p="suspendedStart",g="suspendedYield",m="executing",v="completed",y={};function b(){}function w(){}function C(){}var $={};h($,l,(function(){return this}));var _=Object.getPrototypeOf,S=_&&_(_(P([])));S&&S!==r&&a.call(S,l)&&($=S);var x=C.prototype=b.prototype=Object.create($);function I(t){["next","throw","return"].forEach((function(e){h(t,e,(function(t){return this._invoke(e,t)}))}))}function E(e,n){function i(r,s,o,l){var c=f(e[r],e,s);if("throw"!==c.type){var u=c.arg,h=u.value;return h&&"object"==t(h)&&a.call(h,"__await")?n.resolve(h.__await).then((function(t){i("next",t,o,l)}),(function(t){i("throw",t,o,l)})):n.resolve(h).then((function(t){u.value=t,o(u)}),(function(t){return i("throw",t,o,l)}))}l(c.arg)}var r;s(this,"_invoke",{value:function(t,e){function a(){return new n((function(n,r){i(t,e,n,r)}))}return r=r?r.then(a,a):a()}})}function T(t,e,i){var r=p;return function(a,s){if(r===m)throw Error("Generator is already running");if(r===v){if("throw"===a)throw s;return{value:n,done:!0}}for(i.method=a,i.arg=s;;){var o=i.delegate;if(o){var l=D(o,i);if(l){if(l===y)continue;return l}}if("next"===i.method)i.sent=i._sent=i.arg;else if("throw"===i.method){if(r===p)throw r=v,i.arg;i.dispatchException(i.arg)}else"return"===i.method&&i.abrupt("return",i.arg);r=m;var c=f(t,e,i);if("normal"===c.type){if(r=i.done?v:g,c.arg===y)continue;return{value:c.arg,done:i.done}}"throw"===c.type&&(r=v,i.method="throw",i.arg=c.arg)}}}function D(t,e){var i=e.method,r=t.iterator[i];if(r===n)return e.delegate=null,"throw"===i&&t.iterator.return&&(e.method="return",e.arg=n,D(t,e),"throw"===e.method)||"return"!==i&&(e.method="throw",e.arg=new TypeError("The iterator does not provide a '"+i+"' method")),y;var a=f(r,t.iterator,e.arg);if("throw"===a.type)return e.method="throw",e.arg=a.arg,e.delegate=null,y;var s=a.arg;return s?s.done?(e[t.resultName]=s.value,e.next=t.nextLoc,"return"!==e.method&&(e.method="next",e.arg=n),e.delegate=null,y):s:(e.method="throw",e.arg=new TypeError("iterator result is not an object"),e.delegate=null,y)}function L(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function k(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function A(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(L,this),this.reset(!0)}function P(e){if(e||""===e){var i=e[l];if(i)return i.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var r=-1,s=function t(){for(;++r=0;--r){var s=this.tryEntries[r],o=s.completion;if("root"===s.tryLoc)return i("end");if(s.tryLoc<=this.prev){var l=a.call(s,"catchLoc"),c=a.call(s,"finallyLoc");if(l&&c){if(this.prev=0;--n){var i=this.tryEntries[n];if(i.tryLoc<=this.prev&&a.call(i,"finallyLoc")&&this.prev=0;--e){var n=this.tryEntries[e];if(n.finallyLoc===t)return this.complete(n.completion,n.afterLoc),k(n),y}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.tryLoc===t){var i=n.completion;if("throw"===i.type){var r=i.arg;k(n)}return r}}throw Error("illegal catch attempt")},delegateYield:function(t,e,i){return this.delegate={iterator:P(t),resultName:e,nextLoc:i},"next"===this.method&&(this.arg=n),y}},i}function n(t,e,n,i,r,a,s){try{var o=t[a](s),l=o.value}catch(t){return void n(t)}o.done?e(l):Promise.resolve(l).then(i,r)}var i,r;Craft.EntrySelectInput=Craft.BaseElementSelectInput.extend({get section(){var t=this;return this.settings.sectionId?Craft.publishableSections.find((function(e){return e.id===t.settings.sectionId})):null},canCreateElements:function(){return!!this.section},createElement:(i=e().mark((function t(n){var i,r;return e().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,Craft.sendActionRequest("POST","entries/create",{data:{siteId:this.settings.criteria.siteId,section:this.section.handle,authorId:Craft.userId,title:n}});case 2:return i=t.sent,r=i.data.entry,t.prev=4,t.next=7,this.showElementEditor(r);case 7:t.next=12;break;case 9:return t.prev=9,t.t0=t.catch(4),t.abrupt("return",null);case 12:return t.abrupt("return",r.id);case 13:case"end":return t.stop()}}),t,this,[[4,9]])})),r=function(){var t=this,e=arguments;return new Promise((function(r,a){var s=i.apply(t,e);function o(t){n(s,r,a,o,l,"next",t)}function l(t){n(s,r,a,o,l,"throw",t)}o(void 0)}))},function(t){return r.apply(this,arguments)}),showElementEditor:function(t){var e=this;return new Promise((function(n,i){var r=Craft.createElementEditor("craft\\elements\\Entry",{siteId:e.settings.criteria.siteId,elementId:t.id,draftId:t.draftId,params:{fresh:1}}),a=!1;r.on("submit",(function(){a=!0,n()})),r.on("close",(function(){a||i()}))}))}})},258:function(){function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}function e(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,i)}return n}function n(t){for(var n=1;n').appendTo(Garnish.$bod),this.$body=$('
    ').appendTo(this.$container),this.$footer=$('
  • ").appendTo(i);var a=new Garnish.MenuBtn(this.$selectTransformBtn,{onOptionSelect:this.onSelectTransform.bind(this)});a.disable(),this.$selectTransformBtn.data("menuButton",a)}},onSelectionChange:function(t){var e=!1;this.elementIndex.getSelectedElements().length&&this.settings.transforms.length&&(e=!0);var n=null;this.$selectTransformBtn&&(n=this.$selectTransformBtn.data("menuButton")),e?(n&&n.enable(),this.$selectTransformBtn.removeClass("disabled")):this.$selectTransformBtn&&(n&&n.disable(),this.$selectTransformBtn.addClass("disabled")),this.base()},onSelectTransform:function(t){var e=$(t).data("transform");this.selectImagesWithTransform(e)},selectImagesWithTransform:function(t){var e=this;void 0===Craft.AssetSelectorModal.transformUrls[t]&&(Craft.AssetSelectorModal.transformUrls[t]={});for(var n=this.elementIndex.getSelectedElements(),i=[],r=0;r",{class:"modal elementselectormodal","aria-labelledby":n}).appendTo(Garnish.$bod),r=$("
    ",{class:this.settings.showTitle?"header":"visually-hidden"}).appendTo(i);$("

    ",{id:n,text:this.settings.modalTitle}).appendTo(r);var a=$("
    ",{class:"body"}).append($("
    ",{class:"spinner big"})).appendTo(i);this.$footer=$("
    ",{class:"footer"}).appendTo(i),this.settings.fullscreen&&(i.addClass("fullscreen"),this.settings.minGutter=0),this.base(i,this.settings),this.$secondaryButtons=$('
    ').appendTo(this.$footer),this.$primaryButtons=$('
    ').appendTo(this.$footer),this.$cancelBtn=$("
    ").appendTo(r),s=$('
    ').appendTo(a),o=$("
    ').appendTo(this.$container),this.$cursor=$('
    ').appendTo(this.$container),this.$graduations=$('
    ').appendTo(this.$container),this.$graduationsUl=$("
      ").attr({"aria-hidden":"true"}).appendTo(this.$graduations),this.$container.attr({role:"slider",tabindex:"0","aria-valuemin":this.slideMin,"aria-valuemax":this.slideMax,"aria-valuenow":"0","aria-valuetext":Craft.t("app","{num, number} {num, plural, =1{degree} other{degrees}}",{num:0})});for(var i=this.graduationsMin;i<=this.graduationsMax;i++){var r=$('
    • '+i+"
    • ").appendTo(this.$graduationsUl);i%5==0&&r.addClass("main-graduation"),0===i&&r.addClass("selected")}this.$options=this.$container.find(".graduation"),this.addListener(this.$container,"resize",this._handleResize.bind(this)),this.addListener(this.$container,"tapstart",this._handleTapStart.bind(this)),this.addListener(Garnish.$bod,"tapmove",this._handleTapMove.bind(this)),this.addListener(Garnish.$bod,"tapend",this._handleTapEnd.bind(this)),this.addListener(this.$container,"keydown",this._handleKeypress.bind(this)),setTimeout((function(){n.graduationsCalculatedWidth=10*(n.$options.length-1),n.$graduationsUl.css("left",-n.graduationsCalculatedWidth/2+n.$container.width()/2)}),50)},_handleResize:function(){var t=this.valueToPosition(this.value);this.$graduationsUl.css("left",t)},_handleKeypress:function(t){var e=parseInt(this.$container.attr("aria-valuenow"),10);switch(t.keyCode){case Garnish.UP_KEY:case Garnish.RIGHT_KEY:this.setValue(e+1);break;case Garnish.DOWN_KEY:case Garnish.LEFT_KEY:this.setValue(e-1);break;case Garnish.PAGE_UP_KEY:this.setValue(e+10);break;case Garnish.PAGE_DOWN_KEY:this.setValue(e-10);break;case Garnish.HOME_KEY:this.setValue(this.slideMin);break;case Garnish.END_KEY:this.setValue(this.slideMax)}this.onChange()},_handleTapStart:function(t,e){t.preventDefault(),this.rotateIntent=$(t.target).is(".graduations *"),this.rotateIntent&&(this.startPositionX=e.position.x,this.startLeft=this.$graduationsUl.position().left,this.onStart())},_handleTapMove:function(t,e){this.rotateIntent&&Math.abs(e.position.x-this.startPositionX)>this.sensitivity&&(this.dragging=!0,this.$container.addClass("dragging"),t.preventDefault(),this._setValueFromTouch(e),this.onChange())},_setValueFromTouch:function(t){var e,n=this.dragging?this.startPositionX:this.$cursor.offset().left+this.$cursor.outerWidth()/2;e=this.dragging?n-t.position.x:t.position.x-n;var i=this.startLeft-e,r=this.positionToValue(i);this.setValue(r)},setValue:function(t){var e=this.valueToPosition(t);tthis.slideMax&&(t=this.slideMax,e=this.valueToPosition(t)),this.$graduationsUl.css("left",e),t>=this.slideMin&&t<=this.slideMax&&(this.$options.removeClass("selected"),$.each(this.$options,(function(e,n){$(n).data("graduation")>0&&$(n).data("graduation")<=t&&$(n).addClass("selected"),$(n).data("graduation")<0&&$(n).data("graduation")>=t&&$(n).addClass("selected"),0==$(n).data("graduation")&&$(n).addClass("selected")}))),this.$container.attr({"aria-valuenow":t,"aria-valuetext":Craft.t("app","{num, number} {num, plural, =1{degree} other{degrees}}",{num:parseInt(t,10)})}),this.value=t},_handleTapEnd:function(t,e){this.rotateIntent&&(this.dragging?(t.preventDefault(),this.dragging=!1,this.$container.removeClass("dragging")):(this._setValueFromTouch(e),this.onChange()),this.onEnd(),this.startPositionX=null,this.rotateIntent=!1)},positionToValue:function(t){var e=-1*this.graduationsMin,n=-1*(this.graduationsMin-this.graduationsMax);return(this.$graduations.width()/2+-1*t)/this.graduationsCalculatedWidth*n-e},valueToPosition:function(t){var e=-1*this.graduationsMin,n=-1*(this.graduationsMin-this.graduationsMax);return-((t+e)*this.graduationsCalculatedWidth/n-this.$graduations.width()/2)},onStart:function(){"function"==typeof this.settings.onChange&&this.settings.onStart(this)},onChange:function(){"function"==typeof this.settings.onChange&&this.settings.onChange(this)},onEnd:function(){"function"==typeof this.settings.onChange&&this.settings.onEnd(this)},defaultSettings:{onStart:$.noop,onChange:$.noop,onEnd:$.noop}})},3254:function(){},3517:function(){Craft.BaseUploader=Garnish.Base.extend({allowedKinds:null,$element:null,$fileInput:null,settings:null,fsType:null,formData:{},events:{},_rejectedFiles:{},_extensionList:null,_inProgressCounter:0,init:function(t,e){this._rejectedFiles={size:[],type:[],limit:[]},this.$element=t,this.settings=$.extend({},Craft.BaseUploader.defaults,e),this.formData=this.settings.formData,this.$fileInput=this.settings.fileInput||t,this.events=this.settings.events,this.settings.url||(this.settings.url=this.settings.replace?Craft.getActionUrl(this.settings.replaceAction):Craft.getActionUrl(this.settings.createAction)),this.settings.allowedKinds&&this.settings.allowedKinds.length&&("string"==typeof this.settings.allowedKinds&&(this.settings.allowedKinds=[this.settings.allowedKinds]),this.allowedKinds=this.settings.allowedKinds,delete this.settings.allowedKinds)},setParams:function(t){void 0!==Craft.csrfTokenName&&void 0!==Craft.csrfTokenValue&&(t[Craft.csrfTokenName]=Craft.csrfTokenValue),this.formData=t},getInProgress:function(){return this._inProgressCounter},isLastUpload:function(){return this.getInProgress()<2},processErrorMessages:function(){var t;this._rejectedFiles.type.length&&(t=1===this._rejectedFiles.type.length?"The file {files} could not be uploaded. The allowed file kinds are: {kinds}.":"The files {files} could not be uploaded. The allowed file kinds are: {kinds}.",t=Craft.t("app",t,{files:this._rejectedFiles.type.join(", "),kinds:this.allowedKinds.join(", ")}),this._rejectedFiles.type=[],Craft.cp.displayError(t)),this._rejectedFiles.size.length&&(t=1===this._rejectedFiles.size.length?"The file {files} could not be uploaded, because it exceeds the maximum upload size of {size}.":"The files {files} could not be uploaded, because they exceeded the maximum upload size of {size}.",t=Craft.t("app",t,{files:this._rejectedFiles.size.join(", "),size:this.humanFileSize(this.settings.maxFileSize)}),this._rejectedFiles.size=[],Craft.cp.displayError(t)),this._rejectedFiles.limit.length&&(t=1===this._rejectedFiles.limit.length?"The file {files} could not be uploaded, because the field limit has been reached.":"The files {files} could not be uploaded, because the field limit has been reached.",t=Craft.t("app",t,{files:this._rejectedFiles.limit.join(", ")}),this._rejectedFiles.limit=[],Craft.cp.displayError(t))},humanFileSize:function(t){var e=1024;if(t=e);return t.toFixed(1)+" "+["kB","MB","GB","TB","PB","EB","ZB","YB"][n]},_createExtensionList:function(){this._extensionList=[];for(var t=0;t=0;--r){var s=this.tryEntries[r],o=s.completion;if("root"===s.tryLoc)return i("end");if(s.tryLoc<=this.prev){var l=a.call(s,"catchLoc"),c=a.call(s,"finallyLoc");if(l&&c){if(this.prev=0;--n){var i=this.tryEntries[n];if(i.tryLoc<=this.prev&&a.call(i,"finallyLoc")&&this.prev=0;--e){var n=this.tryEntries[e];if(n.finallyLoc===t)return this.complete(n.completion,n.afterLoc),k(n),y}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.tryLoc===t){var i=n.completion;if("throw"===i.type){var r=i.arg;k(n)}return r}}throw Error("illegal catch attempt")},delegateYield:function(t,e,i){return this.delegate={iterator:P(t),resultName:e,nextLoc:i},"next"===this.method&&(this.arg=n),y}},i}function n(t,e,n,i,r,a,s){try{var o=t[a](s),l=o.value}catch(t){return void n(t)}o.done?e(l):Promise.resolve(l).then(i,r)}Craft.AssetSelectInput=Craft.BaseElementSelectInput.extend({$uploadBtn:null,uploader:null,progressBar:null,openPreviewTimeout:null,init:function(){this.base.apply(this,arguments),this.settings.canUpload&&this._attachUploader(),this.updateAddElementsBtn(),this.addListener(this.$elementsContainer,"keydown",this._onKeyDown.bind(this))},elementSelectSettings:function(){return Object.assign(this.base(),{makeFocusable:!0})},_onKeyDown:function(t){if(t.keyCode===Garnish.SPACE_KEY&&t.shiftKey)return this.openPreview(),t.stopPropagation(),!1},clearOpenPreviewTimeout:function(){this.openPreviewTimeout&&(clearTimeout(this.openPreviewTimeout),this.openPreviewTimeout=null)},openPreview:function(t){Craft.PreviewFileModal.openInstance?Craft.PreviewFileModal.openInstance.hide():(t||(t=this.$elements.filter(":focus").add(this.$elements.has(":focus"))),t.length&&Craft.PreviewFileModal.showForAsset(t,this.elementSelect))},_attachUploader:function(){var t=this;this.progressBar=new Craft.ProgressBar($('
      ').appendTo(this.$container)),this.$addElementBtn&&(this.$uploadBtn=$("
      ').addClass().appendTo(Garnish.$bod)),this.$prompt=$('
      ').appendTo(this.$modalContainerDiv.empty()),this.$promptMessage=$('

      ').appendTo(this.$prompt),this.$promptChoices=$('

      ').appendTo(this.$prompt),this.$promptApplyToRemainingContainer=$('
      ",{class:"entry-type-override-settings-body"}),$("
      ",{class:"fields",html:t.settingsHtml}).appendTo(n),i=$("
      ",{class:"entry-type-override-settings-footer"}),$("
      ",{class:"flex-grow"}).appendTo(i),r=Craft.ui.createButton({label:Craft.t("app","Close"),spinner:!0}).appendTo(i),Craft.ui.createSubmitButton({class:"secondary",label:Craft.t("app","Apply"),spinner:!0}).appendTo(i),a=n.add(i),(o=new Craft.Slideout(a,{containerElement:"form",containerAttributes:{action:"",method:"post",novalidate:"",class:"entry-type-override-settings"}})).on("open",(function(){Garnish.requestAnimationFrame((function(){o.$container.find(".text:first").focus()}))})),r.on("click",(function(){o.close()})),!t.headHtml){e.next=13;break}return e.next=13,Craft.appendHeadHtml(t.headHtml);case 13:if(!t.bodyHtml){e.next=16;break}return e.next=16,Craft.appendBodyHtml(t.bodyHtml);case 16:return Craft.initUiElements(o.$container),e.abrupt("return",o);case 18:case"end":return e.stop()}}),e)})))()},applySettings:function(t,e,i){var a=this;return l(s().mark((function o(){var l,c,u,h,d,f,p,g,m,v;return s().wrap((function(s){for(;;)switch(s.prev=s.next){case 0:return l=e.$container.find("button[type=submit]").addClass("loading"),e.$container.find(".field.has-errors").each((function(t,e){var n=$(e);n.removeClass("has-errors"),n.children(".input").removeClass("errors prevalidate"),n.children("ul.errors").remove()})),s.prev=2,s.prev=3,s.next=6,Craft.sendActionRequest("POST","entry-types/apply-override-settings",{data:{id:t.data("id"),settingsNamespace:i,settings:e.$container.serialize()}});case 6:u=s.sent,c=u.data,s.next=16;break;case 10:throw s.prev=10,s.t0=s.catch(3),(f=null===s.t0||void 0===s.t0||null===(h=s.t0.response)||void 0===h||null===(h=h.data)||void 0===h?void 0:h.errors)&&Object.entries(f).forEach((function(t){var n=r(t,2),i=n[0],a=n[1],s=e.$container.find('[data-error-key="'.concat(i,'"]'));s.length&&Craft.ui.addErrorsToField(s,a)})),Craft.cp.displayError(null===s.t0||void 0===s.t0||null===(d=s.t0.response)||void 0===d||null===(d=d.data)||void 0===d?void 0:d.message),s.t0;case 16:p=$(c.chipHtml),t.find(".chip-label").replaceWith(p.find(".chip-label")),g=t.find("input"),m=n({},c.config),(v=JSON.parse(g.val()).group)&&(m.group=v),g.val(JSON.stringify(m)),Craft.initUiElements(t),a.trigger("applySettings"),e.close(),e.destroy();case 27:return s.prev=27,l.removeClass("loading"),s.finish(27);case 30:case"end":return s.stop()}}),o,null,[[2,,27,30],[3,10]])})))()},renderSettings:function(t){return Object.assign(this.base(),{inputValue:this.settings.allowOverrides?JSON.stringify({id:t}):null})}},{defaults:{allowOverrides:!1}})},4004:function(){Craft.ProgressBar=Garnish.Base.extend({$progressBar:null,$innerProgressBar:null,$progressBarStatus:null,intervalManager:null,_itemCount:0,_progressPercentage:null,_processedItemCount:0,_displaySteps:!1,init:function(t,e,n){var i=this;this.setSettings(n,Craft.ProgressBar.defaults),e&&(this._displaySteps=!0),this.$progressBar=$('