Skip to content

Commit 044082f

Browse files
committed
feature #61789 [Security] deprecate extending RememberMeDetails using legacy constructor signature (xabbuh)
This PR was merged into the 7.4 branch. Discussion ---------- [Security] deprecate extending `RememberMeDetails` using legacy constructor signature | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | | License | MIT Commits ------- fda7c31 deprecate extending RememberMeDetails using legacy constructor signature
2 parents 0c7ba55 + fda7c31 commit 044082f

File tree

4 files changed

+287
-9
lines changed

4 files changed

+287
-9
lines changed

UPGRADE-7.4.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,31 @@ Routing
9090
Security
9191
--------
9292

93+
* Deprecate extending the `RememberMeDetails` class with a constructor expecting the user FQCN
94+
95+
Before:
96+
97+
```php
98+
class CustomRememberMeDetails extends RememberMeDetails
99+
{
100+
public function __construct(string $userFqcn, string $userIdentifier, int $expires, string $value)
101+
{
102+
parent::__construct($userFqcn, $userIdentifier, $expires, $value);
103+
}
104+
}
105+
```
106+
107+
After:
108+
109+
```php
110+
class CustomRememberMeDetails extends RememberMeDetails
111+
{
112+
public function __construct(string $userIdentifier, int $expires, string $value)
113+
{
114+
parent::__construct($userIdentifier, $expires, $value);
115+
}
116+
}
117+
```
93118
* Deprecate callable firewall listeners, extend `AbstractListener` or implement `FirewallListenerInterface` instead
94119
* Deprecate `AbstractListener::__invoke`
95120
* Deprecate `LazyFirewallContext::__invoke()`

src/Symfony/Component/Security/Http/CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@ CHANGELOG
44
7.4
55
---
66

7+
* Deprecate extending the `RememberMeDetails` class with a constructor expecting the user FQCN
8+
9+
Before:
10+
11+
```php
12+
class CustomRememberMeDetails extends RememberMeDetails
13+
{
14+
public function __construct(string $userFqcn, string $userIdentifier, int $expires, string $value)
15+
{
16+
parent::__construct($userFqcn, $userIdentifier, $expires, $value);
17+
}
18+
}
19+
```
20+
21+
After:
22+
23+
```php
24+
class CustomRememberMeDetails extends RememberMeDetails
25+
{
26+
public function __construct(string $userIdentifier, int $expires, string $value)
27+
{
28+
parent::__construct($userIdentifier, $expires, $value);
29+
}
30+
}
31+
```
732
* Add support for union types with `#[CurrentUser]`
833
* Deprecate callable firewall listeners, extend `AbstractListener` or implement `FirewallListenerInterface` instead
934
* Deprecate `AbstractListener::__invoke`

src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,12 @@ public static function fromRawCookie(string $rawCookie): self
8989
throw new AuthenticationException('The user identifier contains a character from outside the base64 alphabet.');
9090
}
9191

92-
if ('' === $cookieParts[0]) {
93-
unset($cookieParts[0]);
94-
} else {
95-
$cookieParts[0] = strtr($cookieParts[0], '.', '\\');
96-
$cookieParts[4] = false;
97-
}
98-
99-
return new static(...$cookieParts);
92+
return new static(...self::collectConstructorArguments(strtr($cookieParts[0], '.', '\\'), $cookieParts[1], $cookieParts[2], $cookieParts[3]));
10093
}
10194

10295
public static function fromPersistentToken(PersistentToken $token, int $expires): self
10396
{
104-
return new static(method_exists($token, 'getClass') ? $token->getClass(false) : '', $token->getUserIdentifier(), $expires, $token->getSeries().':'.$token->getTokenValue(), false);
97+
return new static(...self::collectConstructorArguments(method_exists($token, 'getClass') ? $token->getClass(false) : '', $token->getUserIdentifier(), $expires, $token->getSeries().':'.$token->getTokenValue()));
10598
}
10699

107100
public function withValue(string $value): self
@@ -142,4 +135,21 @@ public function toString(): string
142135
// $userIdentifier is encoded because it might contain COOKIE_DELIMITER, we assume other values don't
143136
return implode(self::COOKIE_DELIMITER, [strtr($this->userFqcn ?? '', '\\', '.'), strtr(base64_encode($this->userIdentifier), '+/=', '-_~'), $this->expires, $this->value]);
144137
}
138+
139+
private static function collectConstructorArguments(string $userFqcn, string $userIdentifier, int $expires, string $value): array
140+
{
141+
$constructor = new \ReflectionMethod(static::class, '__construct');
142+
143+
if (self::class === $constructor->class) {
144+
return [$userFqcn, $userIdentifier, $expires, $value, false];
145+
}
146+
147+
if (3 < $constructor->getNumberOfRequiredParameters()) {
148+
trigger_deprecation('symfony/security-http', '7.4', 'Extending the "%s" class and overriding the constructor with four required arguments is deprecated. Change the constructor signature to __construct(string $userIdentifier, int $expires, string $value).', self::class);
149+
150+
return [$userFqcn, $userIdentifier, $expires, $value];
151+
}
152+
153+
return [$userIdentifier, $expires, $value];
154+
}
145155
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\Tests\RememberMe;
13+
14+
use PHPUnit\Framework\Attributes\Group;
15+
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken;
18+
use Symfony\Component\Security\Core\User\InMemoryUser;
19+
use Symfony\Component\Security\Http\RememberMe\RememberMeDetails;
20+
21+
class RememberMeDetailsTest extends TestCase
22+
{
23+
public function testFromRawCookie()
24+
{
25+
$rememberMeDetails = RememberMeDetails::fromRawCookie(self::getRememberMeCookieValue());
26+
27+
$this->assertSame(RememberMeDetails::class, $rememberMeDetails::class);
28+
$this->assertSame('wouter', $rememberMeDetails->getUserIdentifier());
29+
$this->assertSame(360, $rememberMeDetails->getExpires());
30+
$this->assertSame('series1:token_value', $rememberMeDetails->getValue());
31+
}
32+
33+
public function testFromRawCookieChildClassWithNewConstructorSignature()
34+
{
35+
$rememberMeDetails = RememberMeDetailsChild::fromRawCookie(self::getRememberMeCookieValue());
36+
37+
$this->assertSame(RememberMeDetailsChild::class, $rememberMeDetails::class);
38+
$this->assertSame('wouter', $rememberMeDetails->getUserIdentifier());
39+
$this->assertSame(360, $rememberMeDetails->getExpires());
40+
$this->assertSame('series1:token_value', $rememberMeDetails->getValue());
41+
}
42+
43+
#[Group('legacy')]
44+
#[IgnoreDeprecations]
45+
public function testFromRawCookieChildClassWithLegacyConstructorSignature()
46+
{
47+
$this->expectUserDeprecationMessage(\sprintf('Since symfony/security-http 7.4: Extending the "%s" class and overriding the constructor with four required arguments is deprecated. Change the constructor signature to __construct(string $userIdentifier, int $expires, string $value).', RememberMeDetails::class));
48+
49+
$rememberMeDetails = RememberMeDetailsChildLegacyConstructorSignature::fromRawCookie(self::getRememberMeCookieValue());
50+
51+
$this->assertSame(RememberMeDetailsChildLegacyConstructorSignature::class, $rememberMeDetails::class);
52+
$this->assertSame('wouter', $rememberMeDetails->getUserIdentifier());
53+
$this->assertSame(360, $rememberMeDetails->getExpires());
54+
$this->assertSame('series1:token_value', $rememberMeDetails->getValue());
55+
}
56+
57+
public function testFromRawCookieChildClassWithoutConstructor()
58+
{
59+
$rememberMeDetails = RememberMeDetailsChildWithoutConstructor::fromRawCookie(self::getRememberMeCookieValue());
60+
61+
$this->assertSame(RememberMeDetailsChildWithoutConstructor::class, $rememberMeDetails::class);
62+
$this->assertSame('wouter', $rememberMeDetails->getUserIdentifier());
63+
$this->assertSame(360, $rememberMeDetails->getExpires());
64+
$this->assertSame('series1:token_value', $rememberMeDetails->getValue());
65+
}
66+
67+
#[Group('legacy')]
68+
#[IgnoreDeprecations]
69+
public function testFromLegacyRawCookie()
70+
{
71+
$rememberMeDetails = RememberMeDetails::fromRawCookie(self::getLegacyRememberMeCookieValue());
72+
73+
$this->assertSame(RememberMeDetails::class, $rememberMeDetails::class);
74+
$this->assertSame(InMemoryUser::class, $rememberMeDetails->getUserFqcn());
75+
$this->assertSame('wouter', $rememberMeDetails->getUserIdentifier());
76+
$this->assertSame(360, $rememberMeDetails->getExpires());
77+
$this->assertSame('series1:token_value', $rememberMeDetails->getValue());
78+
}
79+
80+
#[Group('legacy')]
81+
#[IgnoreDeprecations]
82+
public function testFromLegacyRawCookieChildClassWithNewConstructorSignature()
83+
{
84+
$rememberMeDetails = RememberMeDetailsChild::fromRawCookie(self::getLegacyRememberMeCookieValue());
85+
86+
$this->assertSame(RememberMeDetailsChild::class, $rememberMeDetails::class);
87+
$this->assertSame('', $rememberMeDetails->getUserFqcn());
88+
$this->assertSame('wouter', $rememberMeDetails->getUserIdentifier());
89+
$this->assertSame(360, $rememberMeDetails->getExpires());
90+
$this->assertSame('series1:token_value', $rememberMeDetails->getValue());
91+
}
92+
93+
#[Group('legacy')]
94+
#[IgnoreDeprecations]
95+
public function testFromLegacyRawCookieChildClassWithLegacyConstructorSignature()
96+
{
97+
$this->expectUserDeprecationMessage(\sprintf('Since symfony/security-http 7.4: Extending the "%s" class and overriding the constructor with four required arguments is deprecated. Change the constructor signature to __construct(string $userIdentifier, int $expires, string $value).', RememberMeDetails::class));
98+
99+
$rememberMeDetails = RememberMeDetailsChildLegacyConstructorSignature::fromRawCookie(self::getLegacyRememberMeCookieValue());
100+
101+
$this->assertSame(RememberMeDetailsChildLegacyConstructorSignature::class, $rememberMeDetails::class);
102+
$this->assertSame(InMemoryUser::class, $rememberMeDetails->getUserFqcn());
103+
$this->assertSame('wouter', $rememberMeDetails->getUserIdentifier());
104+
$this->assertSame(360, $rememberMeDetails->getExpires());
105+
$this->assertSame('series1:token_value', $rememberMeDetails->getValue());
106+
}
107+
108+
#[Group('legacy')]
109+
#[IgnoreDeprecations]
110+
public function testFromLegacyRawCookieChildClassWithoutConstructor()
111+
{
112+
$rememberMeDetails = RememberMeDetailsChildWithoutConstructor::fromRawCookie(self::getLegacyRememberMeCookieValue());
113+
114+
$this->assertSame(RememberMeDetailsChildWithoutConstructor::class, $rememberMeDetails::class);
115+
$this->assertSame(InMemoryUser::class, $rememberMeDetails->getUserFqcn());
116+
$this->assertSame('wouter', $rememberMeDetails->getUserIdentifier());
117+
$this->assertSame(360, $rememberMeDetails->getExpires());
118+
$this->assertSame('series1:token_value', $rememberMeDetails->getValue());
119+
}
120+
121+
public function testFromPersistentToken()
122+
{
123+
if (method_exists(PersistentToken::class, 'getClass')) {
124+
$token = new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'token_value', new \DateTimeImmutable(), false);
125+
} else {
126+
$token = new PersistentToken('wouter', 'series1', 'token_value', new \DateTimeImmutable());
127+
}
128+
129+
$rememberMeDetails = RememberMeDetails::fromPersistentToken($token, 360);
130+
131+
$this->assertSame(RememberMeDetails::class, $rememberMeDetails::class);
132+
$this->assertSame('wouter', $rememberMeDetails->getUserIdentifier());
133+
$this->assertSame(360, $rememberMeDetails->getExpires());
134+
$this->assertSame('series1:token_value', $rememberMeDetails->getValue());
135+
}
136+
137+
public function testFromPersistentTokenChildClassWithNewConstructorSignature()
138+
{
139+
if (method_exists(PersistentToken::class, 'getClass')) {
140+
$token = new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'token_value', new \DateTimeImmutable(), false);
141+
} else {
142+
$token = new PersistentToken('wouter', 'series1', 'token_value', new \DateTimeImmutable());
143+
}
144+
145+
$rememberMeDetails = RememberMeDetailsChild::fromPersistentToken($token, 360);
146+
147+
$this->assertSame(RememberMeDetailsChild::class, $rememberMeDetails::class);
148+
$this->assertSame('wouter', $rememberMeDetails->getUserIdentifier());
149+
$this->assertSame(360, $rememberMeDetails->getExpires());
150+
$this->assertSame('series1:token_value', $rememberMeDetails->getValue());
151+
}
152+
153+
#[Group('legacy')]
154+
#[IgnoreDeprecations]
155+
public function testFromPersistentTokenChildClassWithLegacyConstructorSignature()
156+
{
157+
$this->expectUserDeprecationMessage(\sprintf('Since symfony/security-http 7.4: Extending the "%s" class and overriding the constructor with four required arguments is deprecated. Change the constructor signature to __construct(string $userIdentifier, int $expires, string $value).', RememberMeDetails::class));
158+
159+
if (method_exists(PersistentToken::class, 'getClass')) {
160+
$token = new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'token_value', new \DateTimeImmutable(), false);
161+
} else {
162+
$token = new PersistentToken('wouter', 'series1', 'token_value', new \DateTimeImmutable());
163+
}
164+
165+
$rememberMeDetails = RememberMeDetailsChildLegacyConstructorSignature::fromPersistentToken($token, 360);
166+
167+
$this->assertSame(RememberMeDetailsChildLegacyConstructorSignature::class, $rememberMeDetails::class);
168+
$this->assertSame('wouter', $rememberMeDetails->getUserIdentifier());
169+
$this->assertSame(360, $rememberMeDetails->getExpires());
170+
$this->assertSame('series1:token_value', $rememberMeDetails->getValue());
171+
}
172+
173+
public function testFromPersistentTokenChildClassWithoutConstructor()
174+
{
175+
if (method_exists(PersistentToken::class, 'getClass')) {
176+
$token = new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'token_value', new \DateTimeImmutable(), false);
177+
} else {
178+
$token = new PersistentToken('wouter', 'series1', 'token_value', new \DateTimeImmutable());
179+
}
180+
181+
$rememberMeDetails = RememberMeDetailsChildWithoutConstructor::fromPersistentToken($token, 360);
182+
183+
$this->assertSame(RememberMeDetailsChildWithoutConstructor::class, $rememberMeDetails::class);
184+
$this->assertSame('wouter', $rememberMeDetails->getUserIdentifier());
185+
$this->assertSame(360, $rememberMeDetails->getExpires());
186+
$this->assertSame('series1:token_value', $rememberMeDetails->getValue());
187+
}
188+
189+
private static function getRememberMeCookieValue(): string
190+
{
191+
return base64_encode((new RememberMeDetails('wouter', 360, 'series1:token_value'))->toString());
192+
}
193+
194+
private static function getLegacyRememberMeCookieValue(): string
195+
{
196+
return base64_encode((new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'series1:token_value', false))->toString());
197+
}
198+
}
199+
200+
class RememberMeDetailsChild extends RememberMeDetails
201+
{
202+
public function __construct(string $userIdentifier, int $expires, string $value)
203+
{
204+
parent::__construct($userIdentifier, $expires, $value);
205+
}
206+
}
207+
208+
class RememberMeDetailsChildLegacyConstructorSignature extends RememberMeDetails
209+
{
210+
public function __construct(string $userFqcn, string $userIdentifier, int $expires, string $value)
211+
{
212+
parent::__construct($userFqcn, $userIdentifier, $expires, $value);
213+
}
214+
}
215+
216+
class RememberMeDetailsChildWithoutConstructor extends RememberMeDetails
217+
{
218+
}

0 commit comments

Comments
 (0)