diff --git a/Authentication/RememberMe/TokenProviderInterface.php b/Authentication/RememberMe/TokenProviderInterface.php index bfe49015..00e8bac5 100644 --- a/Authentication/RememberMe/TokenProviderInterface.php +++ b/Authentication/RememberMe/TokenProviderInterface.php @@ -23,32 +23,24 @@ interface TokenProviderInterface /** * Loads the active token for the given series. * - * @return PersistentTokenInterface - * * @throws TokenNotFoundException if the token is not found */ - public function loadTokenBySeries(string $series); + public function loadTokenBySeries(string $series): PersistentTokenInterface; /** * Deletes all tokens belonging to series. - * - * @return void */ - public function deleteTokenBySeries(string $series); + public function deleteTokenBySeries(string $series): void; /** * Updates the token according to this data. * - * @return void - * * @throws TokenNotFoundException if the token is not found */ - public function updateToken(string $series, #[\SensitiveParameter] string $tokenValue, \DateTimeInterface $lastUsed); + public function updateToken(string $series, #[\SensitiveParameter] string $tokenValue, \DateTimeInterface $lastUsed): void; /** * Creates a new token. - * - * @return void */ - public function createNewToken(PersistentTokenInterface $token); + public function createNewToken(PersistentTokenInterface $token): void; } diff --git a/Authentication/Token/AbstractToken.php b/Authentication/Token/AbstractToken.php index d730c111..37b7cd69 100644 --- a/Authentication/Token/AbstractToken.php +++ b/Authentication/Token/AbstractToken.php @@ -54,20 +54,6 @@ public function setUser(UserInterface $user): void $this->user = $user; } - /** - * Removes sensitive information from the token. - * - * @deprecated since Symfony 7.3, erase credentials using the "__serialize()" method instead - */ - public function eraseCredentials(): void - { - trigger_deprecation('symfony/security-core', '7.3', \sprintf('The "%s::eraseCredentials()" method is deprecated and will be removed in 8.0, erase credentials using the "__serialize()" method instead.', TokenInterface::class)); - - if ($this->getUser() instanceof UserInterface) { - $this->getUser()->eraseCredentials(); - } - } - /** * Returns all the necessary state of the object for serialization purposes. * diff --git a/Authentication/Token/NullToken.php b/Authentication/Token/NullToken.php index cb2bc0fd..2e35f0ef 100644 --- a/Authentication/Token/NullToken.php +++ b/Authentication/Token/NullToken.php @@ -43,17 +43,6 @@ public function getUserIdentifier(): string return ''; } - /** - * @deprecated since Symfony 7.3 - */ - #[\Deprecated(since: 'symfony/security-core 7.3')] - public function eraseCredentials(): void - { - if (\PHP_VERSION_ID < 80400) { - @trigger_error(\sprintf('Method %s::eraseCredentials() is deprecated since symfony/security-core 7.3', self::class), \E_USER_DEPRECATED); - } - } - public function getAttributes(): array { return []; diff --git a/Authentication/Token/RememberMeToken.php b/Authentication/Token/RememberMeToken.php index dfbe20ec..e9bca720 100644 --- a/Authentication/Token/RememberMeToken.php +++ b/Authentication/Token/RememberMeToken.php @@ -21,8 +21,6 @@ */ class RememberMeToken extends AbstractToken { - private ?string $secret = null; - /** * @throws \InvalidArgumentException */ @@ -32,11 +30,6 @@ public function __construct( ) { parent::__construct($user->getRoles()); - if (\func_num_args() > 2) { - trigger_deprecation('symfony/security-core', '7.2', 'The "$secret" argument of "%s()" is deprecated.', __METHOD__); - $this->secret = func_get_arg(2); - } - if (!$firewallName) { throw new InvalidArgumentException('$firewallName must not be empty.'); } @@ -49,25 +42,14 @@ public function getFirewallName(): string return $this->firewallName; } - /** - * @deprecated since Symfony 7.2 - */ - public function getSecret(): string - { - trigger_deprecation('symfony/security-core', '7.2', 'The "%s()" method is deprecated.', __METHOD__); - - return $this->secret ??= base64_encode(random_bytes(8)); - } - public function __serialize(): array { - // $this->firewallName should be kept at index 1 for compatibility with payloads generated before Symfony 8 - return [$this->secret, $this->firewallName, parent::__serialize()]; + return [null, $this->firewallName, parent::__serialize()]; } public function __unserialize(array $data): void { - [$this->secret, $this->firewallName, $parentData] = $data; + [, $this->firewallName, $parentData] = $data; $parentData = \is_array($parentData) ? $parentData : unserialize($parentData); parent::__unserialize($parentData); } diff --git a/Authentication/Token/TokenInterface.php b/Authentication/Token/TokenInterface.php index c658e38b..80423774 100644 --- a/Authentication/Token/TokenInterface.php +++ b/Authentication/Token/TokenInterface.php @@ -57,13 +57,6 @@ public function getUser(): ?UserInterface; */ public function setUser(UserInterface $user): void; - /** - * Removes sensitive information from the token. - * - * @deprecated since Symfony 7.3; erase credentials using the "__serialize()" method instead - */ - public function eraseCredentials(): void; - public function getAttributes(): array; /** diff --git a/Authorization/AccessDecisionManagerInterface.php b/Authorization/AccessDecisionManagerInterface.php index cb4a3310..c5e737b4 100644 --- a/Authorization/AccessDecisionManagerInterface.php +++ b/Authorization/AccessDecisionManagerInterface.php @@ -27,5 +27,5 @@ interface AccessDecisionManagerInterface * @param mixed $object The object to secure * @param AccessDecision|null $accessDecision Should be used to explain the decision */ - public function decide(TokenInterface $token, array $attributes, mixed $object = null/* , ?AccessDecision $accessDecision = null */): bool; + public function decide(TokenInterface $token, array $attributes, mixed $object = null, ?AccessDecision $accessDecision = null): bool; } diff --git a/Authorization/AuthorizationCheckerInterface.php b/Authorization/AuthorizationCheckerInterface.php index 848b17ee..a8fab50f 100644 --- a/Authorization/AuthorizationCheckerInterface.php +++ b/Authorization/AuthorizationCheckerInterface.php @@ -24,5 +24,5 @@ interface AuthorizationCheckerInterface * @param mixed $attribute A single attribute to vote on (can be of any type; strings, Expression and Closure instances are supported by the core) * @param AccessDecision|null $accessDecision Should be used to explain the decision */ - public function isGranted(mixed $attribute, mixed $subject = null/* , ?AccessDecision $accessDecision = null */): bool; + public function isGranted(mixed $attribute, mixed $subject = null, ?AccessDecision $accessDecision = null): bool; } diff --git a/Authorization/Voter/AuthenticatedVoter.php b/Authorization/Voter/AuthenticatedVoter.php index 3ab6b92c..6a7c2c2f 100644 --- a/Authorization/Voter/AuthenticatedVoter.php +++ b/Authorization/Voter/AuthenticatedVoter.php @@ -40,13 +40,8 @@ public function __construct( ) { } - /** - * @param Vote|null $vote Should be used to explain the vote - */ - public function vote(TokenInterface $token, mixed $subject, array $attributes/* , ?Vote $vote = null */): int + public function vote(TokenInterface $token, mixed $subject, array $attributes, ?Vote $vote = null): int { - $vote = 3 < \func_num_args() ? func_get_arg(3) : null; - if ($attributes === [self::PUBLIC_ACCESS]) { $vote?->addReason('Access is public.'); diff --git a/Authorization/Voter/ExpressionVoter.php b/Authorization/Voter/ExpressionVoter.php index 719aae7d..0a32751b 100644 --- a/Authorization/Voter/ExpressionVoter.php +++ b/Authorization/Voter/ExpressionVoter.php @@ -44,12 +44,8 @@ public function supportsType(string $subjectType): bool return true; } - /** - * @param Vote|null $vote Should be used to explain the vote - */ - public function vote(TokenInterface $token, mixed $subject, array $attributes/* , ?Vote $vote = null */): int + public function vote(TokenInterface $token, mixed $subject, array $attributes, ?Vote $vote = null): int { - $vote = 3 < \func_num_args() ? func_get_arg(3) : null; $result = VoterInterface::ACCESS_ABSTAIN; $variables = null; $failingExpressions = []; diff --git a/Authorization/Voter/RoleVoter.php b/Authorization/Voter/RoleVoter.php index 2225e8d4..b46ef9e7 100644 --- a/Authorization/Voter/RoleVoter.php +++ b/Authorization/Voter/RoleVoter.php @@ -25,12 +25,8 @@ public function __construct( ) { } - /** - * @param Vote|null $vote Should be used to explain the vote - */ - public function vote(TokenInterface $token, mixed $subject, array $attributes/* , ?Vote $vote = null */): int + public function vote(TokenInterface $token, mixed $subject, array $attributes, ?Vote $vote = null): int { - $vote = 3 < \func_num_args() ? func_get_arg(3) : null; $result = VoterInterface::ACCESS_ABSTAIN; $roles = $this->extractRoles($token); $missingRoles = []; diff --git a/Authorization/Voter/Voter.php b/Authorization/Voter/Voter.php index 55930def..3a561070 100644 --- a/Authorization/Voter/Voter.php +++ b/Authorization/Voter/Voter.php @@ -24,12 +24,8 @@ */ abstract class Voter implements VoterInterface, CacheableVoterInterface { - /** - * @param Vote|null $vote Should be used to explain the vote - */ - public function vote(TokenInterface $token, mixed $subject, array $attributes/* , ?Vote $vote = null */): int + public function vote(TokenInterface $token, mixed $subject, array $attributes, ?Vote $vote = null): int { - $vote = 3 < \func_num_args() ? func_get_arg(3) : null; // abstain vote by default in case none of the attributes are supported $voteResult = self::ACCESS_ABSTAIN; @@ -108,5 +104,5 @@ abstract protected function supports(string $attribute, mixed $subject): bool; * @param TSubject $subject * @param Vote|null $vote Should be used to explain the vote */ - abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token/* , ?Vote $vote = null */): bool; + abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool; } diff --git a/Authorization/Voter/VoterInterface.php b/Authorization/Voter/VoterInterface.php index 0902a94b..0dd7fa63 100644 --- a/Authorization/Voter/VoterInterface.php +++ b/Authorization/Voter/VoterInterface.php @@ -36,5 +36,5 @@ interface VoterInterface * * @return self::ACCESS_* */ - public function vote(TokenInterface $token, mixed $subject, array $attributes/* , ?Vote $vote = null */): int; + public function vote(TokenInterface $token, mixed $subject, array $attributes, ?Vote $vote = null): int; } diff --git a/CHANGELOG.md b/CHANGELOG.md index 12806416..6d21eb74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ CHANGELOG ========= +8.0 +--- + + * Remove `RememberMeToken::getSecret()` + * Remove `UserInterface::eraseCredentials()` and `TokenInterface::eraseCredentials()`, + erase credentials e.g. using `__serialize()` instead + * Add argument `$accessDecision` to `AccessDecisionManagerInterface::decide()` and `AuthorizationCheckerInterface::isGranted()` + * Add argument `$vote` to `VoterInterface::vote()` and `Voter::voteOnAttribute()` + * Add argument `$token` to `UserCheckerInterface::checkPostAuth()` + 7.3 --- diff --git a/Tests/Authentication/AuthenticationTrustResolverTest.php b/Tests/Authentication/AuthenticationTrustResolverTest.php index c657b31e..74d1cac9 100644 --- a/Tests/Authentication/AuthenticationTrustResolverTest.php +++ b/Tests/Authentication/AuthenticationTrustResolverTest.php @@ -119,11 +119,6 @@ public function getUserIdentifier(): string { } - #[\Deprecated] - public function eraseCredentials(): void - { - } - public function getAttributes(): array { } diff --git a/Tests/Authentication/Token/AbstractTokenTest.php b/Tests/Authentication/Token/AbstractTokenTest.php index 3972b1cd..e59401d2 100644 --- a/Tests/Authentication/Token/AbstractTokenTest.php +++ b/Tests/Authentication/Token/AbstractTokenTest.php @@ -12,16 +12,12 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Token; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\UserInterface; class AbstractTokenTest extends TestCase { - use ExpectUserDeprecationMessageTrait; - /** * @dataProvider provideUsers */ @@ -37,22 +33,6 @@ public static function provideUsers() yield [new InMemoryUser('fabien', null), 'fabien']; } - /** - * @group legacy - */ - public function testEraseCredentials() - { - $token = new ConcreteToken(['ROLE_FOO']); - - $user = $this->createMock(UserInterface::class); - $user->expects($this->once())->method('eraseCredentials'); - $token->setUser($user); - - $this->expectUserDeprecationMessage(\sprintf('Since symfony/security-core 7.3: The "%s::eraseCredentials()" method is deprecated and will be removed in 8.0, erase credentials using the "__serialize()" method instead.', TokenInterface::class)); - - $token->eraseCredentials(); - } - public function testSerialize() { $token = new ConcreteToken(['ROLE_FOO', 'ROLE_BAR']); diff --git a/Tests/Authentication/Token/Fixtures/CustomUser.php b/Tests/Authentication/Token/Fixtures/CustomUser.php index d4f91de1..388e38c6 100644 --- a/Tests/Authentication/Token/Fixtures/CustomUser.php +++ b/Tests/Authentication/Token/Fixtures/CustomUser.php @@ -35,9 +35,4 @@ public function getPassword(): ?string { return null; } - - #[\Deprecated] - public function eraseCredentials(): void - { - } } diff --git a/Tests/Authentication/Token/RememberMeTokenTest.php b/Tests/Authentication/Token/RememberMeTokenTest.php index b0cdbaf1..07f7674e 100644 --- a/Tests/Authentication/Token/RememberMeTokenTest.php +++ b/Tests/Authentication/Token/RememberMeTokenTest.php @@ -27,17 +27,6 @@ public function testConstructor() $this->assertSame($user, $token->getUser()); } - /** - * @group legacy - */ - public function testSecret() - { - $user = $this->getUser(); - $token = new RememberMeToken($user, 'fookey', 'foo'); - - $this->assertEquals('foo', $token->getSecret()); - } - protected function getUser($roles = ['ROLE_FOO']) { $user = $this->createMock(UserInterface::class); diff --git a/Tests/Authorization/Voter/VoterTest.php b/Tests/Authorization/Voter/VoterTest.php index eaada306..e5d22b6e 100644 --- a/Tests/Authorization/Voter/VoterTest.php +++ b/Tests/Authorization/Voter/VoterTest.php @@ -97,7 +97,7 @@ protected function voteOnAttribute(string $attribute, $object, TokenInterface $t protected function supports(string $attribute, $object): bool { - return $object instanceof \stdClass && \in_array($attribute, ['EDIT', 'CREATE']); + return $object instanceof \stdClass && \in_array($attribute, ['EDIT', 'CREATE'], true); } } diff --git a/Tests/User/InMemoryUserTest.php b/Tests/User/InMemoryUserTest.php index f06e98c3..3f16b91a 100644 --- a/Tests/User/InMemoryUserTest.php +++ b/Tests/User/InMemoryUserTest.php @@ -12,14 +12,11 @@ namespace Symfony\Component\Security\Core\Tests\User; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait; use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\UserInterface; class InMemoryUserTest extends TestCase { - use ExpectUserDeprecationMessageTrait; - public function testConstructorException() { $this->expectException(\InvalidArgumentException::class); @@ -56,17 +53,6 @@ public function testIsEnabled() $this->assertFalse($user->isEnabled()); } - /** - * @group legacy - */ - public function testEraseCredentials() - { - $user = new InMemoryUser('fabien', 'superpass'); - $this->expectUserDeprecationMessage(\sprintf('%sMethod %s::eraseCredentials() is deprecated since symfony/security-core 7.3', \PHP_VERSION_ID >= 80400 ? 'Unsilenced deprecation: ' : '', InMemoryUser::class)); - $user->eraseCredentials(); - $this->assertEquals('superpass', $user->getPassword()); - } - public function testToString() { $user = new InMemoryUser('fabien', 'superpass'); diff --git a/Tests/Validator/Constraints/UserPasswordTest.php b/Tests/Validator/Constraints/UserPasswordTest.php index ed4ca442..2c990808 100644 --- a/Tests/Validator/Constraints/UserPasswordTest.php +++ b/Tests/Validator/Constraints/UserPasswordTest.php @@ -35,8 +35,6 @@ public function testValidatedByService(UserPassword $constraint) public static function provideServiceValidatedConstraints(): iterable { - yield 'Doctrine style' => [new UserPassword(['service' => 'my_service'])]; - yield 'named arguments' => [new UserPassword(service: 'my_service')]; $metadata = new ClassMetadata(UserPasswordDummy::class); @@ -45,6 +43,14 @@ public static function provideServiceValidatedConstraints(): iterable yield 'attribute' => [$metadata->properties['b']->constraints[0]]; } + /** + * @group legacy + */ + public function testValidatedByServiceDoctrineStyle() + { + self::assertSame('my_service', (new UserPassword(['service' => 'my_service']))->validatedBy()); + } + public function testAttributes() { $metadata = new ClassMetadata(UserPasswordDummy::class); diff --git a/User/ChainUserChecker.php b/User/ChainUserChecker.php index 37ce0f04..f1ed0b10 100644 --- a/User/ChainUserChecker.php +++ b/User/ChainUserChecker.php @@ -29,13 +29,8 @@ public function checkPreAuth(UserInterface $user): void } } - /** - * @param ?TokenInterface $token - */ - public function checkPostAuth(UserInterface $user /* , ?TokenInterface $token = null */): void + public function checkPostAuth(UserInterface $user, ?TokenInterface $token = null): void { - $token = 1 < \func_num_args() ? func_get_arg(1) : null; - foreach ($this->checkers as $checker) { $checker->checkPostAuth($user, $token); } diff --git a/User/InMemoryUser.php b/User/InMemoryUser.php index 7bed183a..85f4aa5c 100644 --- a/User/InMemoryUser.php +++ b/User/InMemoryUser.php @@ -74,17 +74,6 @@ public function isEnabled(): bool return $this->enabled; } - /** - * @deprecated since Symfony 7.3 - */ - #[\Deprecated(since: 'symfony/security-core 7.3')] - public function eraseCredentials(): void - { - if (\PHP_VERSION_ID < 80400) { - @trigger_error(\sprintf('Method %s::eraseCredentials() is deprecated since symfony/security-core 7.3', self::class), \E_USER_DEPRECATED); - } - } - public function isEqualTo(UserInterface $user): bool { if (!$user instanceof self) { diff --git a/User/InMemoryUserChecker.php b/User/InMemoryUserChecker.php index 4d93d08c..c5b8bf0f 100644 --- a/User/InMemoryUserChecker.php +++ b/User/InMemoryUserChecker.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Security\Core\User; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\DisabledException; /** @@ -33,10 +34,7 @@ public function checkPreAuth(UserInterface $user): void } } - /** - * @param ?TokenInterface $token - */ - public function checkPostAuth(UserInterface $user /* , ?TokenInterface $token = null */): void + public function checkPostAuth(UserInterface $user, ?TokenInterface $token = null): void { } } diff --git a/User/OAuth2User.php b/User/OAuth2User.php index 42c0550a..8ee5e931 100644 --- a/User/OAuth2User.php +++ b/User/OAuth2User.php @@ -63,8 +63,4 @@ public function getUserIdentifier(): string { return (string) ($this->sub ?? $this->username); } - - public function eraseCredentials(): void - { - } } diff --git a/User/OidcUser.php b/User/OidcUser.php index df59c5f7..d61cb943 100644 --- a/User/OidcUser.php +++ b/User/OidcUser.php @@ -71,17 +71,6 @@ public function getUserIdentifier(): string return (string) ($this->userIdentifier ?? $this->getSub()); } - /** - * @deprecated since Symfony 7.3 - */ - #[\Deprecated(since: 'symfony/security-core 7.3')] - public function eraseCredentials(): void - { - if (\PHP_VERSION_ID < 80400) { - @trigger_error(\sprintf('Method %s::eraseCredentials() is deprecated since symfony/security-core 7.3', self::class), \E_USER_DEPRECATED); - } - } - public function getSub(): ?string { return $this->sub; diff --git a/User/UserCheckerInterface.php b/User/UserCheckerInterface.php index aea958fc..83851f08 100644 --- a/User/UserCheckerInterface.php +++ b/User/UserCheckerInterface.php @@ -34,9 +34,7 @@ public function checkPreAuth(UserInterface $user): void; /** * Checks the user account after authentication. * - * @param ?TokenInterface $token - * * @throws AccountStatusException */ - public function checkPostAuth(UserInterface $user /* , ?TokenInterface $token = null */): void; + public function checkPostAuth(UserInterface $user, ?TokenInterface $token = null): void; } diff --git a/User/UserInterface.php b/User/UserInterface.php index 24c0581f..3d01475e 100644 --- a/User/UserInterface.php +++ b/User/UserInterface.php @@ -47,16 +47,6 @@ interface UserInterface */ public function getRoles(): array; - /** - * Removes sensitive data from the user. - * - * This is important if, at any given point, sensitive information like - * the plain-text password is stored on this object. - * - * @deprecated since Symfony 7.3, erase credentials using the "__serialize()" method instead - */ - public function eraseCredentials(): void; - /** * Returns the identifier for this user (e.g. username or email address). * diff --git a/composer.json b/composer.json index 0aaff1e3..fdee3efa 100644 --- a/composer.json +++ b/composer.json @@ -16,33 +16,24 @@ } ], "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.4", "symfony/event-dispatcher-contracts": "^2.5|^3", - "symfony/service-contracts": "^2.5|^3", - "symfony/password-hasher": "^6.4|^7.0" + "symfony/password-hasher": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" }, "require-dev": { "psr/container": "^1.1|^2.0", "psr/cache": "^1.0|^2.0|^3.0", - "symfony/cache": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/ldap": "^6.4|^7.0", - "symfony/string": "^6.4|^7.0", - "symfony/translation": "^6.4.3|^7.0.3", - "symfony/validator": "^6.4|^7.0", - "psr/log": "^1|^2|^3" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/http-foundation": "<6.4", - "symfony/ldap": "<6.4", - "symfony/translation": "<6.4.3|>=7.0,<7.0.3", - "symfony/validator": "<6.4" + "psr/log": "^1|^2|^3", + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/ldap": "^7.4|^8.0", + "symfony/string": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0" }, "autoload": { "psr-4": { "Symfony\\Component\\Security\\Core\\": "" }, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy