Skip to content

Commit a1f4b86

Browse files
committed
Introduce client param id_token_signed_response_alg
1 parent 5cc585a commit a1f4b86

File tree

13 files changed

+158
-16
lines changed

13 files changed

+158
-16
lines changed

docker/conformance.sql

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ INSERT INTO oidc_migration_versions VALUES('20210916153400');
1414
INSERT INTO oidc_migration_versions VALUES('20210916173400');
1515
INSERT INTO oidc_migration_versions VALUES('20240603141400');
1616
INSERT INTO oidc_migration_versions VALUES('20240605145700');
17+
INSERT INTO oidc_migration_versions VALUES('20240820132400');
18+
INSERT INTO oidc_migration_versions VALUES('20240828153300');
19+
INSERT INTO oidc_migration_versions VALUES('20240830153300');
20+
INSERT INTO oidc_migration_versions VALUES('20240902120000');
21+
INSERT INTO oidc_migration_versions VALUES('20240905120000');
22+
INSERT INTO oidc_migration_versions VALUES('20240906120000');
23+
INSERT INTO oidc_migration_versions VALUES('20250818163000');
24+
INSERT INTO oidc_migration_versions VALUES('20250908163000');
25+
INSERT INTO oidc_migration_versions VALUES('20250912163000');
26+
INSERT INTO oidc_migration_versions VALUES('20250913163000');
27+
INSERT INTO oidc_migration_versions VALUES('20250915163000');
28+
INSERT INTO oidc_migration_versions VALUES('20250916163000');
29+
INSERT INTO oidc_migration_versions VALUES('20250917163000');
30+
INSERT INTO oidc_migration_versions VALUES('20251021000001');
31+
INSERT INTO oidc_migration_versions VALUES('20251021000002');
32+
INSERT INTO oidc_migration_versions VALUES('20260109000001');
1733
CREATE TABLE oidc_user (
1834
id VARCHAR(191) PRIMARY KEY NOT NULL,
1935
claims TEXT,
@@ -44,15 +60,16 @@ CREATE TABLE oidc_client (
4460
created_at TIMESTAMP NULL DEFAULT NULL,
4561
expires_at TIMESTAMP NULL DEFAULT NULL,
4662
is_federated BOOLEAN NOT NULL DEFAULT false,
47-
is_generic BOOLEAN NOT NULL DEFAULT false
63+
is_generic BOOLEAN NOT NULL DEFAULT false,
64+
extra_metadata TEXT NULL
4865
);
4966
-- Used 'httpd' host for back-channel logout url (https://httpd:8443/test/a/simplesamlphp-module-oidc/backchannel_logout)
5067
-- since this is the hostname of conformance server while running in container environment
51-
INSERT INTO oidc_client VALUES('_55a99a1d298da921cb27d700d4604352e51171ebc4','_8967dd97d07cc59db7055e84ac00e79005157c1132','Conformance Client 1',replace('Client 1 for Conformance Testing https://openid.net/certification/connect_op_testing/\n','\n',char(10)),'example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","address","phone","offline_access"]',1,1,NULL,'["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/post_logout_redirect"]','https://httpd:8443/test/a/simplesamlphp-module-oidc/backchannel_logout',NULL,NULL, NULL, NULL, NULL, NULL, 'manual', NULL, NULL, NULL, false, false);
52-
INSERT INTO oidc_client VALUES('_34efb61060172a11d62101bc804db789f8f9100b0e','_91a4607a1c10ba801268929b961b3f6c067ff82d21','Conformance Client 2','','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","offline_access"]',1,1,NULL,NULL,NULL,NULL,NULL, NULL, NULL, NULL, NULL, 'manual', NULL, NULL, NULL, false, false);
53-
INSERT INTO oidc_client VALUES('_0afb7d18e54b2de8205a93e38ca119e62ee321d031','_944e73bbeec7850d32b68f1b5c780562c955967e4e','Conformance Client 3','Client for client_secret_post','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email"]',1,1,NULL,NULL,NULL,NULL,NULL, NULL, NULL, NULL, NULL, 'manual', NULL, NULL, NULL, false, false);
54-
INSERT INTO oidc_client VALUES('_8957eda35234902ba8343c0cdacac040310f17dfca','_322d16999f9da8b5abc9e9c0c08e853f60f4dc4804','RP-Initiated Logout Client','Client for testing RP-Initiated Logout','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","address","phone"]',1,1,NULL,'["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/post_logout_redirect"]',NULL,NULL,NULL, NULL, NULL, NULL, NULL, 'manual', NULL, NULL, NULL, false, false);
55-
INSERT INTO oidc_client VALUES('_9fe2f7589ece1b71f5ef75a91847d71bc5125ec2a6','_3c0beb20194179c01d7796c6836f62801e9ed4b368','Back-Channel Logout Client','Client for testing Back-Channel Logout','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","address","phone"]',1,1,NULL,'["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/post_logout_redirect"]','https://httpd:8443/test/a/simplesamlphp-module-oidc/backchannel_logout',NULL,NULL, NULL, NULL, NULL, NULL, 'manual', NULL, NULL, NULL, false, false);
68+
INSERT INTO oidc_client VALUES('_55a99a1d298da921cb27d700d4604352e51171ebc4','_8967dd97d07cc59db7055e84ac00e79005157c1132','Conformance Client 1',replace('Client 1 for Conformance Testing https://openid.net/certification/connect_op_testing/\n','\n',char(10)),'example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","address","phone","offline_access"]',1,1,NULL,'["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/post_logout_redirect"]','https://httpd:8443/test/a/simplesamlphp-module-oidc/backchannel_logout',NULL,NULL, NULL, NULL, NULL, NULL, 'manual', NULL, NULL, NULL, false, false, NULL);
69+
INSERT INTO oidc_client VALUES('_34efb61060172a11d62101bc804db789f8f9100b0e','_91a4607a1c10ba801268929b961b3f6c067ff82d21','Conformance Client 2','','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","offline_access"]',1,1,NULL,NULL,NULL,NULL,NULL, NULL, NULL, NULL, NULL, 'manual', NULL, NULL, NULL, false, false, NULL);
70+
INSERT INTO oidc_client VALUES('_0afb7d18e54b2de8205a93e38ca119e62ee321d031','_944e73bbeec7850d32b68f1b5c780562c955967e4e','Conformance Client 3','Client for client_secret_post','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email"]',1,1,NULL,NULL,NULL,NULL,NULL, NULL, NULL, NULL, NULL, 'manual', NULL, NULL, NULL, false, false, NULL);
71+
INSERT INTO oidc_client VALUES('_8957eda35234902ba8343c0cdacac040310f17dfca','_322d16999f9da8b5abc9e9c0c08e853f60f4dc4804','RP-Initiated Logout Client','Client for testing RP-Initiated Logout','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","address","phone"]',1,1,NULL,'["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/post_logout_redirect"]',NULL,NULL,NULL, NULL, NULL, NULL, NULL, 'manual', NULL, NULL, NULL, false, false, NULL);
72+
INSERT INTO oidc_client VALUES('_9fe2f7589ece1b71f5ef75a91847d71bc5125ec2a6','_3c0beb20194179c01d7796c6836f62801e9ed4b368','Back-Channel Logout Client','Client for testing Back-Channel Logout','example-userpass','["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/callback","https:\/\/www.certification.openid.net\/test\/a\/simplesamlphp-module-oidc\/callback"]','["openid","profile","email","address","phone"]',1,1,NULL,'["https:\/\/localhost.emobix.co.uk:8443\/test\/a\/simplesamlphp-module-oidc\/post_logout_redirect"]','https://httpd:8443/test/a/simplesamlphp-module-oidc/backchannel_logout',NULL,NULL, NULL, NULL, NULL, NULL, 'manual', NULL, NULL, NULL, false, false, NULL);
5673
CREATE TABLE oidc_access_token (
5774
id VARCHAR(191) PRIMARY KEY NOT NULL,
5875
scopes TEXT,

docs/6-oidc-upgrade.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ apply those relevant to your deployment.
77

88
New features:
99

10+
- Clients can now be configured with new properties:
11+
- ID Token Signing Algorithm (id_token_signed_response_alg)
1012
- Initial support for OpenID for Verifiable Credential Issuance
1113
(OpenID4VCI). Note that the implementation is experimental. You should not use
12-
it in production yet.
14+
it in production.
1315

1416
New configuration options:
1517

16-
- Several new options regarding support for OpenID4VCI.
18+
- Several new options are available in module config file regarding support for
19+
OpenID4VCI.
1720

1821
Major impact changes:
1922

src/Controllers/Admin/ClientController.php

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use SimpleSAML\Module\oidc\Services\LoggerService;
2626
use SimpleSAML\Module\oidc\Services\SessionMessagesService;
2727
use SimpleSAML\Module\oidc\Utils\Routes;
28+
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
2829
use Symfony\Component\HttpFoundation\Request;
2930
use Symfony\Component\HttpFoundation\Response;
3031

@@ -118,7 +119,7 @@ public function resetSecret(Request $request): Response
118119
$this->clientRepository->update($client, $authedUserId);
119120

120121
$message = Translate::noop('Client secret has been reset.');
121-
$this->logger->info($message, $client->getState());
122+
$this->logger->info($message, [ParametersEnum::ClientId->value => $client->getIdentifier()]);
122123
$this->sessionMessagesService->addMessage($message);
123124

124125
return $this->routes->newRedirectResponseToModuleUrl(
@@ -181,14 +182,14 @@ public function add(): Response
181182

182183
if ($this->clientRepository->findById($client->getIdentifier())) {
183184
$message = Translate::noop('Client with generated ID already exists.');
184-
$this->logger->warning($message, $client->getState());
185+
$this->logger->warning($message, [ParametersEnum::ClientId->value => $client->getIdentifier()]);
185186
$this->sessionMessagesService->addMessage($message);
186187
} elseif (
187188
($entityIdentifier = $client->getEntityIdentifier()) &&
188189
$this->clientRepository->findByEntityIdentifier($entityIdentifier)
189190
) {
190191
$message = Translate::noop('Client with given entity identifier already exists.');
191-
$this->logger->warning($message, $client->getState());
192+
$this->logger->warning($message, [ParametersEnum::ClientId->value => $client->getIdentifier()]);
192193
$this->sessionMessagesService->addMessage($message);
193194
} else {
194195
$this->clientRepository->add($client);
@@ -199,7 +200,7 @@ public function add(): Response
199200
/** @var string[] $allowedOrigins */
200201
$this->allowedOriginRepository->set($client->getIdentifier(), $allowedOrigins);
201202
$message = Translate::noop('Client has been added.');
202-
$this->logger->info($message, $client->getState());
203+
$this->logger->info($message, [ParametersEnum::ClientId->value => $client->getIdentifier()]);
203204
$this->sessionMessagesService->addMessage($message);
204205

205206
return $this->routes->newRedirectResponseToModuleUrl(
@@ -238,6 +239,9 @@ public function edit(Request $request): Response
238239

239240
$clientData = $originalClient->toArray();
240241
$clientData['allowed_origin'] = $clientAllowedOrigins;
242+
243+
// Handle extra metadata
244+
241245
$form->setDefaults($clientData);
242246

243247
if ($form->isSuccess()) {
@@ -252,6 +256,7 @@ public function edit(Request $request): Response
252256
$originalClient->getCreatedAt(),
253257
$originalClient->getExpiresAt(),
254258
$originalClient->getOwner(),
259+
$originalClient->isGeneric(),
255260
);
256261

257262
// We have to make sure that the Entity Identifier is unique.
@@ -311,6 +316,7 @@ protected function buildClientEntityFromFormData(
311316
?\DateTimeImmutable $createdAt = null,
312317
?\DateTimeImmutable $expiresAt = null,
313318
?string $owner = null,
319+
bool $isGeneric = false,
314320
): ClientEntityInterface {
315321
/** @var array $data */
316322
$data = $form->getValues('array');
@@ -344,6 +350,15 @@ protected function buildClientEntityFromFormData(
344350
null : (string)$data[ClientEntity::KEY_SIGNED_JWKS_URI];
345351
$isFederated = (bool)$data[ClientEntity::KEY_IS_FEDERATED];
346352

353+
$idTokenSignedResponseAlg = isset($data[ClaimsEnum::IdTokenSignedResponseAlg->value]) &&
354+
is_string($data[ClaimsEnum::IdTokenSignedResponseAlg->value]) ?
355+
$data[ClaimsEnum::IdTokenSignedResponseAlg->value] :
356+
null;
357+
358+
$extraMetadata = [
359+
ClaimsEnum::IdTokenSignedResponseAlg->value => $idTokenSignedResponseAlg,
360+
];
361+
347362
return $this->clientEntityFactory->fromData(
348363
$identifier,
349364
$secret,
@@ -368,6 +383,8 @@ protected function buildClientEntityFromFormData(
368383
$createdAt,
369384
$expiresAt,
370385
$isFederated,
386+
$isGeneric,
387+
$extraMetadata,
371388
);
372389
}
373390
}

src/Entities/ClientEntity.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use League\OAuth2\Server\Entities\Traits\EntityTrait;
2222
use SimpleSAML\Module\oidc\Codebooks\RegistrationTypeEnum;
2323
use SimpleSAML\Module\oidc\Entities\Interfaces\ClientEntityInterface;
24+
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
2425
use SimpleSAML\OpenID\Codebooks\ClientRegistrationTypesEnum;
2526

2627
class ClientEntity implements ClientEntityInterface
@@ -52,6 +53,7 @@ class ClientEntity implements ClientEntityInterface
5253
public const KEY_EXPIRES_AT = 'expires_at';
5354
public const KEY_IS_FEDERATED = 'is_federated';
5455
public const KEY_IS_GENERIC = 'is_generic';
56+
public const KEY_EXTRA_METADATA = 'extra_metadata';
5557

5658
private string $secret;
5759

@@ -95,6 +97,7 @@ class ClientEntity implements ClientEntityInterface
9597
private ?DateTimeImmutable $expiresAt;
9698
private bool $isFederated;
9799
private bool $isGeneric;
100+
private ?array $extraMetadata;
98101

99102
/**
100103
* @param string[] $redirectUri
@@ -129,6 +132,7 @@ public function __construct(
129132
?DateTimeImmutable $expiresAt = null,
130133
bool $isFederated = false,
131134
bool $isGeneric = false,
135+
?array $extraMetadata = null,
132136
) {
133137
$this->identifier = $identifier;
134138
$this->secret = $secret;
@@ -154,6 +158,7 @@ public function __construct(
154158
$this->expiresAt = $expiresAt;
155159
$this->isFederated = $isFederated;
156160
$this->isGeneric = $isGeneric;
161+
$this->extraMetadata = $extraMetadata;
157162
}
158163

159164
/**
@@ -193,6 +198,9 @@ public function getState(): array
193198
self::KEY_EXPIRES_AT => $this->getExpiresAt()?->format('Y-m-d H:i:s'),
194199
self::KEY_IS_FEDERATED => $this->isFederated(),
195200
self::KEY_IS_GENERIC => $this->isGeneric(),
201+
self::KEY_EXTRA_METADATA => is_null($this->extraMetadata) ?
202+
null :
203+
json_encode($this->extraMetadata, JSON_THROW_ON_ERROR),
196204
];
197205
}
198206

@@ -223,6 +231,9 @@ public function toArray(): array
223231
self::KEY_EXPIRES_AT => $this->expiresAt,
224232
self::KEY_IS_FEDERATED => $this->isFederated,
225233
self::KEY_IS_GENERIC => $this->isGeneric,
234+
235+
// Extra metadata
236+
ClaimsEnum::IdTokenSignedResponseAlg->value => $this->getIdTokenSignedResponseAlg(),
226237
];
227238
}
228239

@@ -366,4 +377,24 @@ public function isGeneric(): bool
366377
{
367378
return $this->isGeneric;
368379
}
380+
381+
public function getExtraMetadata(): array
382+
{
383+
return $this->extraMetadata ?? [];
384+
}
385+
386+
public function getIdTokenSignedResponseAlg(): ?string
387+
{
388+
if (!is_array($this->extraMetadata)) {
389+
return null;
390+
}
391+
392+
$idTokenSignedResponseAlg = $this->extraMetadata['id_token_signed_response_alg'] ?? null;
393+
394+
if (!is_string($idTokenSignedResponseAlg)) {
395+
return null;
396+
}
397+
398+
return $idTokenSignedResponseAlg;
399+
}
369400
}

src/Entities/Interfaces/ClientEntityInterface.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,7 @@ public function getExpiresAt(): ?DateTimeImmutable;
8080
public function isExpired(): bool;
8181
public function isFederated(): bool;
8282
public function isGeneric(): bool;
83+
84+
public function getExtraMetadata(): array;
85+
public function getIdTokenSignedResponseAlg(): ?string;
8386
}

src/Factories/Entities/ClientEntityFactory.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public function fromData(
6767
?DateTimeImmutable $expiresAt = null,
6868
bool $isFederated = false,
6969
bool $isGeneric = false,
70+
?array $extraMetadata = null,
7071
): ClientEntityInterface {
7172
return new ClientEntity(
7273
$id,
@@ -93,6 +94,7 @@ public function fromData(
9394
$expiresAt,
9495
$isFederated,
9596
$isGeneric,
97+
$extraMetadata,
9698
);
9799
}
98100

@@ -196,6 +198,20 @@ public function fromRegistrationData(
196198
$isFederated = $existingClient?->isFederated() ?? false;
197199
$isGeneric = $existingClient?->isGeneric() ?? false;
198200

201+
$extraMetadata = $existingClient?->getExtraMetadata() ?? [];
202+
203+
// Handle any other supported client metadata as extra metadata.
204+
// id_token_signed_response_alg
205+
$idTokenSignedResponseAlg = isset($metadata[ClaimsEnum::IdTokenSignedResponseAlg->value]) &&
206+
is_string($metadata[ClaimsEnum::IdTokenSignedResponseAlg->value]) ?
207+
$metadata[ClaimsEnum::IdTokenSignedResponseAlg->value] :
208+
$existingClient?->getIdTokenSignedResponseAlg();
209+
210+
// TODO mivanci Check if id_token_signed_response_alg is supported.
211+
212+
$extraMetadata[ClaimsEnum::IdTokenSignedResponseAlg->value] = $idTokenSignedResponseAlg;
213+
214+
199215
return $this->fromData(
200216
$id,
201217
$secret,
@@ -221,6 +237,7 @@ public function fromRegistrationData(
221237
$expiresAt,
222238
$isFederated,
223239
$isGeneric,
240+
$extraMetadata,
224241
);
225242
}
226243

@@ -361,6 +378,11 @@ public function fromState(array $state): ClientEntityInterface
361378
$isFederated = (bool)$state[ClientEntity::KEY_IS_FEDERATED];
362379
$isGeneric = (bool)$state[ClientEntity::KEY_IS_GENERIC];
363380

381+
/** @var ?mixed[] $extraMetadata */
382+
$extraMetadata = empty($state[ClientEntity::KEY_EXTRA_METADATA]) ?
383+
null :
384+
json_decode((string)$state[ClientEntity::KEY_EXTRA_METADATA], true, 512, JSON_THROW_ON_ERROR);
385+
364386
return $this->fromData(
365387
$id,
366388
$secret,
@@ -386,6 +408,7 @@ public function fromState(array $state): ClientEntityInterface
386408
$expiresAt,
387409
$isFederated,
388410
$isGeneric,
411+
$extraMetadata,
389412
);
390413
}
391414

src/Forms/ClientForm.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use SimpleSAML\Module\oidc\Forms\Controls\CsrfProtection;
2323
use SimpleSAML\Module\oidc\Helpers;
2424
use SimpleSAML\Module\oidc\ModuleConfig;
25+
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
2526
use SimpleSAML\OpenID\Codebooks\ClientRegistrationTypesEnum;
2627
use Traversable;
2728

@@ -278,6 +279,10 @@ public function getValues(string|object|bool|null $returnType = null, ?array $co
278279
$signedJwksUri = trim((string)$values['signed_jwks_uri']);
279280
$values['signed_jwks_uri'] = empty($signedJwksUri) ? null : $signedJwksUri;
280281

282+
$idTokenSignedResponseAlg = trim((string)$values[ClaimsEnum::IdTokenSignedResponseAlg->value]);
283+
$values[ClaimsEnum::IdTokenSignedResponseAlg->value] = empty($idTokenSignedResponseAlg) ?
284+
null : $idTokenSignedResponseAlg;
285+
281286
return $values;
282287
}
283288

@@ -414,6 +419,12 @@ protected function buildForm(): void
414419

415420
$this->addCheckbox('is_federated', '{oidc:client:is_federated}')
416421
->setHtmlAttribute('class', 'full-width');
422+
423+
// TODO mivanci Properly fetch the list of supported algos
424+
$this->addSelect('id_token_signed_response_alg', Translate::noop('ID Token Signing Algorithm'))
425+
->setHtmlAttribute('class', 'full-width')
426+
->setItems(['RS256'], false)
427+
->setPrompt(Translate::noop('-'));
417428
}
418429

419430
/**

0 commit comments

Comments
 (0)