Migrate from Guzzle to Symfony HttpClient - 1

This commit is contained in:
Yassine Guedidi 2025-01-09 01:14:27 +01:00
parent 9ff4cbee22
commit 6593f0c4df
5 changed files with 87 additions and 164 deletions

View file

@ -47,6 +47,10 @@ framework:
X-Accept: 'application/json' X-Accept: 'application/json'
request_html_function.client: request_html_function.client:
scope: '.*' scope: '.*'
browser.client:
scope: '.*'
verify_host: false
verify_peer: false
# Twig Configuration # Twig Configuration
twig: twig:

View file

@ -214,6 +214,10 @@ services:
GuzzleHttp\Cookie\CookieJar: ~ GuzzleHttp\Cookie\CookieJar: ~
Symfony\Component\BrowserKit\HttpBrowser:
arguments:
$client: '@browser.client'
Wallabag\Helper\HttpClientFactory: Wallabag\Helper\HttpClientFactory:
calls: calls:
- ['addSubscriber', ['@Wallabag\Guzzle\AuthenticatorSubscriber']] - ['addSubscriber', ['@Wallabag\Guzzle\AuthenticatorSubscriber']]

View file

@ -61,15 +61,10 @@ class AuthenticatorSubscriber implements SubscriberInterface, LoggerAwareInterfa
return; return;
} }
$client = $event->getClient(); if (!$this->authenticator->isLoggedIn($config)) {
if (!$this->authenticator->isLoggedIn($config, $client)) {
$this->logger->debug('loginIfRequired> user is not logged in, attach authenticator'); $this->logger->debug('loginIfRequired> user is not logged in, attach authenticator');
$emitter = $client->getEmitter(); $this->authenticator->login($config);
$emitter->detach($this);
$this->authenticator->login($config, $client);
$emitter->attach($this);
} }
} }
@ -98,12 +93,7 @@ class AuthenticatorSubscriber implements SubscriberInterface, LoggerAwareInterfa
$this->logger->debug('loginIfRequested> retry #' . $this->retries . ' with login ' . ($isLoginRequired ? '' : 'not ') . 'required'); $this->logger->debug('loginIfRequested> retry #' . $this->retries . ' with login ' . ($isLoginRequired ? '' : 'not ') . 'required');
if ($isLoginRequired && $this->retries < self::MAX_RETRIES) { if ($isLoginRequired && $this->retries < self::MAX_RETRIES) {
$client = $event->getClient(); $this->authenticator->login($config);
$emitter = $client->getEmitter();
$emitter->detach($this);
$this->authenticator->login($config, $client);
$emitter->attach($this);
$event->retry(); $event->retry();

View file

@ -2,18 +2,19 @@
namespace Wallabag\SiteConfig; namespace Wallabag\SiteConfig;
use GuzzleHttp\ClientInterface; use Symfony\Component\BrowserKit\HttpBrowser;
use GuzzleHttp\Cookie\CookieJar;
use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Wallabag\ExpressionLanguage\AuthenticatorProvider; use Wallabag\ExpressionLanguage\AuthenticatorProvider;
class LoginFormAuthenticator class LoginFormAuthenticator
{ {
private HttpBrowser $browser;
private ExpressionLanguage $expressionLanguage; private ExpressionLanguage $expressionLanguage;
public function __construct(AuthenticatorProvider $authenticatorProvider) public function __construct(HttpBrowser $browser, AuthenticatorProvider $authenticatorProvider)
{ {
$this->browser = $browser;
$this->expressionLanguage = new ExpressionLanguage(null, [$authenticatorProvider]); $this->expressionLanguage = new ExpressionLanguage(null, [$authenticatorProvider]);
} }
@ -22,17 +23,14 @@ class LoginFormAuthenticator
* *
* @return self * @return self
*/ */
public function login(SiteConfig $siteConfig, ClientInterface $guzzle) public function login(SiteConfig $siteConfig)
{ {
$postFields = [ $postFields = [
$siteConfig->getUsernameField() => $siteConfig->getUsername(), $siteConfig->getUsernameField() => $siteConfig->getUsername(),
$siteConfig->getPasswordField() => $siteConfig->getPassword(), $siteConfig->getPasswordField() => $siteConfig->getPassword(),
] + $this->getExtraFields($siteConfig); ] + $this->getExtraFields($siteConfig);
$guzzle->post( $this->browser->request('POST', $siteConfig->getLoginUri(), $postFields);
$siteConfig->getLoginUri(),
['body' => $postFields, 'allow_redirects' => true, 'verify' => false]
);
return $this; return $this;
} }
@ -42,15 +40,12 @@ class LoginFormAuthenticator
* *
* @return bool * @return bool
*/ */
public function isLoggedIn(SiteConfig $siteConfig, ClientInterface $guzzle) public function isLoggedIn(SiteConfig $siteConfig)
{ {
if (($cookieJar = $guzzle->getDefaultOption('cookies')) instanceof CookieJar) { foreach ($this->browser->getCookieJar()->all() as $cookie) {
/** @var \GuzzleHttp\Cookie\SetCookie $cookie */ // check required cookies
foreach ($cookieJar as $cookie) { if ($cookie->getDomain() === $siteConfig->getHost()) {
// check required cookies return true;
if ($cookie->getDomain() === $siteConfig->getHost()) {
return true;
}
} }
} }

View file

@ -2,13 +2,12 @@
namespace Tests\Wallabag\SiteConfig; namespace Tests\Wallabag\SiteConfig;
use GuzzleHttp\Client;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Subscriber\Mock;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\BrowserKit\HttpBrowser;
use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Wallabag\ExpressionLanguage\AuthenticatorProvider; use Wallabag\ExpressionLanguage\AuthenticatorProvider;
use Wallabag\SiteConfig\LoginFormAuthenticator; use Wallabag\SiteConfig\LoginFormAuthenticator;
use Wallabag\SiteConfig\SiteConfig; use Wallabag\SiteConfig\SiteConfig;
@ -30,19 +29,17 @@ class LoginFormAuthenticatorTest extends TestCase
'password' => 'unkn0wn', 'password' => 'unkn0wn',
]); ]);
$response = new Response( $browserResponse = new MockResponse('<html></html>', ['http_code' => 200, 'response_headers' => ['content-type' => 'text/html']]);
200, $browserClient = new MockHttpClient([$browserResponse]);
['content-type' => 'text/html'], $browser = new HttpBrowser($browserClient);
Stream::factory('')
);
$guzzle = new Client();
$guzzle->getEmitter()->attach(new Mock([$response]));
$mockHttpClient = new MockHttpClient([new MockResponse('', ['http_code' => 200, 'response_headers' => ['content-type' => 'text/html']])]); $requestHtmlFunctionResponse = new MockResponse('<html></html>', ['http_code' => 200, 'response_headers' => ['content-type' => 'text/html']]);
$requestHtmlFunctionClient = new MockHttpClient([$requestHtmlFunctionResponse]);
$authenticatorProvider = new AuthenticatorProvider($requestHtmlFunctionClient);
$authenticatorProvider = new AuthenticatorProvider($mockHttpClient); $auth = new LoginFormAuthenticator($browser, $authenticatorProvider);
$auth = new LoginFormAuthenticator($authenticatorProvider);
$res = $auth->login($siteConfig, $guzzle); $res = $auth->login($siteConfig);
$this->assertInstanceOf(LoginFormAuthenticator::class, $res); $this->assertInstanceOf(LoginFormAuthenticator::class, $res);
} }
@ -63,19 +60,17 @@ class LoginFormAuthenticatorTest extends TestCase
'password' => 'unkn0wn', 'password' => 'unkn0wn',
]); ]);
$response = new Response( $browserResponse = new MockResponse('<html></html>', ['http_code' => 200, 'response_headers' => ['content-type' => 'text/html']]);
200, $browserClient = new MockHttpClient([$browserResponse]);
['content-type' => 'text/html'], $browser = new HttpBrowser($browserClient);
Stream::factory('<html></html>')
);
$guzzle = new Client();
$guzzle->getEmitter()->attach(new Mock([$response, $response]));
$mockHttpClient = new MockHttpClient([new MockResponse('<html></html>', ['http_code' => 200, 'response_headers' => ['content-type' => 'text/html']])]); $requestHtmlFunctionResponse = new MockResponse('<html></html>', ['http_code' => 200, 'response_headers' => ['content-type' => 'text/html']]);
$requestHtmlFunctionClient = new MockHttpClient([$requestHtmlFunctionResponse]);
$authenticatorProvider = new AuthenticatorProvider($requestHtmlFunctionClient);
$authenticatorProvider = new AuthenticatorProvider($mockHttpClient); $auth = new LoginFormAuthenticator($browser, $authenticatorProvider);
$auth = new LoginFormAuthenticator($authenticatorProvider);
$res = $auth->login($siteConfig, $guzzle); $res = $auth->login($siteConfig);
$this->assertInstanceOf(LoginFormAuthenticator::class, $res); $this->assertInstanceOf(LoginFormAuthenticator::class, $res);
} }
@ -96,121 +91,44 @@ class LoginFormAuthenticatorTest extends TestCase
'password' => 'unkn0wn', 'password' => 'unkn0wn',
]); ]);
$response = $this->getMockBuilder(Response::class) $browserResponse = new MockResponse('<html></html>', ['http_code' => 200, 'response_headers' => ['content-type' => 'text/html']]);
->disableOriginalConstructor() $browserClient = new MockHttpClient([$browserResponse]);
$browser = $this->getMockBuilder(HttpBrowser::class)
->setConstructorArgs([$browserClient])
->getMock(); ->getMock();
$browser->expects($this->any())
$response->expects($this->any()) ->method('request')
->method('getBody')
->willReturn(file_get_contents(__DIR__ . '/../fixtures/aoc.media.html'));
$response->expects($this->any())
->method('getStatusCode')
->willReturn(200);
$mockHttpClient = new MockHttpClient([new MockResponse(file_get_contents(__DIR__ . '/../fixtures/aoc.media.html'), ['http_code' => 200, 'response_headers' => ['content-type' => 'text/html']])]);
$client = $this->getMockBuilder(Client::class)
->disableOriginalConstructor()
->getMock();
$client->expects($this->any())
->method('post')
->with( ->with(
$this->equalTo('POST'),
$this->equalTo('https://aoc.media/wp-admin/admin-ajax.php'), $this->equalTo('https://aoc.media/wp-admin/admin-ajax.php'),
$this->equalTo([ $this->equalTo([
'body' => [ 'nom' => 'johndoe',
'nom' => 'johndoe', 'password' => 'unkn0wn',
'password' => 'unkn0wn', 'security' => 'c506c1b8bc',
'security' => 'c506c1b8bc', 'action' => 'login_user',
'action' => 'login_user',
],
'allow_redirects' => true,
'verify' => false,
]) ])
) )
->willReturn($response); ;
$client->expects($this->any()) $requestHtmlFunctionResponse = $this->getMockBuilder(ResponseInterface::class)->getMock();
->method('get') $requestHtmlFunctionResponse->expects($this->any())
->method('getContent')
->willReturn(file_get_contents(__DIR__ . '/../fixtures/aoc.media.html'))
;
$requestHtmlFunctionClient = $this->getMockBuilder(HttpClientInterface::class)->getMock();
$requestHtmlFunctionClient->expects($this->any())
->method('request')
->with( ->with(
$this->equalTo('GET'),
$this->equalTo('https://aoc.media/'), $this->equalTo('https://aoc.media/'),
$this->equalTo([])
) )
->willReturn($response); ->willReturn($requestHtmlFunctionResponse)
;
$authenticatorProvider = new AuthenticatorProvider($requestHtmlFunctionClient);
$authenticatorProvider = new AuthenticatorProvider($mockHttpClient); $auth = new LoginFormAuthenticator($browser, $authenticatorProvider);
$auth = new LoginFormAuthenticator($authenticatorProvider);
$res = $auth->login($siteConfig, $client);
$this->assertInstanceOf(LoginFormAuthenticator::class, $res); $res = $auth->login($siteConfig);
}
public function testLoginPostWithExtraFieldsWithData()
{
$siteConfig = new SiteConfig([
'host' => 'nextinpact.com',
'loginUri' => 'https://compte.nextinpact.com/Account/Login',
'usernameField' => 'UserName',
'passwordField' => 'Password',
'extraFields' => [
'__RequestVerificationToken' => '@=xpath(\'//form[@action="/Account/Login"]/input[@name="__RequestVerificationToken"]\', request_html(\'https://compte.nextinpact.com/Account/Login?http://www.nextinpact.com/\', {\'headers\': {\'X-Requested-With\':\'XMLHttpRequest\'}}))',
'returnUrl' => 'https://www.nextinpact.com/news/102835-pour-cour-comptes-fonctionnement-actuel-vote-par-internet-nest-pas-satisfaisant.htm',
],
'username' => 'johndoe',
'password' => 'unkn0wn',
]);
$response = $this->getMockBuilder(Response::class)
->disableOriginalConstructor()
->getMock();
$response->expects($this->any())
->method('getBody')
->willReturn(file_get_contents(__DIR__ . '/../fixtures/nextinpact-login.html'));
$response->expects($this->any())
->method('getStatusCode')
->willReturn(200);
$mockHttpClient = new MockHttpClient([new MockResponse(file_get_contents(__DIR__ . '/../fixtures/nextinpact-login.html'), ['http_code' => 200, 'response_headers' => ['content-type' => 'text/html']])]);
$client = $this->getMockBuilder(Client::class)
->disableOriginalConstructor()
->getMock();
$client->expects($this->any())
->method('post')
->with(
$this->equalTo('https://compte.nextinpact.com/Account/Login'),
$this->equalTo([
'body' => [
'UserName' => 'johndoe',
'Password' => 'unkn0wn',
'__RequestVerificationToken' => 's6x2QcnQDUL92mkKSi_JuUBXcgUYx_Plf-KyQ2eJypKAjQZIeTvaFHOsfEdTrcSXt3dt2CW39V7r9V16LUtvjszodAU1',
'returnUrl' => 'https://www.nextinpact.com/news/102835-pour-cour-comptes-fonctionnement-actuel-vote-par-internet-nest-pas-satisfaisant.htm',
],
'allow_redirects' => true,
'verify' => false,
])
)
->willReturn($response);
$client->expects($this->any())
->method('get')
->with(
$this->equalTo('https://compte.nextinpact.com/Account/Login?http://www.nextinpact.com/'),
$this->equalTo([
'headers' => [
'X-Requested-With' => 'XMLHttpRequest',
],
])
)
->willReturn($response);
$authenticatorProvider = new AuthenticatorProvider($mockHttpClient);
$auth = new LoginFormAuthenticator($authenticatorProvider);
$res = $auth->login($siteConfig, $client);
$this->assertInstanceOf(LoginFormAuthenticator::class, $res); $this->assertInstanceOf(LoginFormAuthenticator::class, $res);
} }
@ -225,10 +143,16 @@ class LoginFormAuthenticatorTest extends TestCase
'password' => 'unkn0wn', 'password' => 'unkn0wn',
]); ]);
$mockHttpClient = new MockHttpClient(); $browserResponse = new MockResponse('<html></html>', ['http_code' => 200, 'response_headers' => ['content-type' => 'text/html']]);
$browserClient = new MockHttpClient([$browserResponse]);
$browser = new HttpBrowser($browserClient);
$requestHtmlFunctionResponse = new MockResponse('<html></html>', ['http_code' => 200, 'response_headers' => ['content-type' => 'text/html']]);
$requestHtmlFunctionClient = new MockHttpClient([$requestHtmlFunctionResponse]);
$authenticatorProvider = new AuthenticatorProvider($requestHtmlFunctionClient);
$auth = new LoginFormAuthenticator($browser, $authenticatorProvider);
$authenticatorProvider = new AuthenticatorProvider($mockHttpClient);
$auth = new LoginFormAuthenticator($authenticatorProvider);
$loginRequired = $auth->isLoginRequired($siteConfig, file_get_contents(__DIR__ . '/../fixtures/nextinpact-login.html')); $loginRequired = $auth->isLoginRequired($siteConfig, file_get_contents(__DIR__ . '/../fixtures/nextinpact-login.html'));
$this->assertFalse($loginRequired); $this->assertFalse($loginRequired);
@ -245,10 +169,16 @@ class LoginFormAuthenticatorTest extends TestCase
'notLoggedInXpath' => '//h2[@class="title_reserve_article"]', 'notLoggedInXpath' => '//h2[@class="title_reserve_article"]',
]); ]);
$mockHttpClient = new MockHttpClient(); $browserResponse = new MockResponse('<html></html>', ['http_code' => 200, 'response_headers' => ['content-type' => 'text/html']]);
$browserClient = new MockHttpClient([$browserResponse]);
$browser = new HttpBrowser($browserClient);
$requestHtmlFunctionResponse = new MockResponse('<html></html>', ['http_code' => 200, 'response_headers' => ['content-type' => 'text/html']]);
$requestHtmlFunctionClient = new MockHttpClient([$requestHtmlFunctionResponse]);
$authenticatorProvider = new AuthenticatorProvider($requestHtmlFunctionClient);
$auth = new LoginFormAuthenticator($browser, $authenticatorProvider);
$authenticatorProvider = new AuthenticatorProvider($mockHttpClient);
$auth = new LoginFormAuthenticator($authenticatorProvider);
$loginRequired = $auth->isLoginRequired($siteConfig, file_get_contents(__DIR__ . '/../fixtures/nextinpact-article.html')); $loginRequired = $auth->isLoginRequired($siteConfig, file_get_contents(__DIR__ . '/../fixtures/nextinpact-article.html'));
$this->assertTrue($loginRequired); $this->assertTrue($loginRequired);