diff --git a/app/AppKernel.php b/app/AppKernel.php index 216cacf29..d161a9b9f 100644 --- a/app/AppKernel.php +++ b/app/AppKernel.php @@ -30,7 +30,6 @@ class AppKernel extends Kernel new Craue\ConfigBundle\CraueConfigBundle(), new BabDev\PagerfantaBundle\BabDevPagerfantaBundle(), new FOS\JsRoutingBundle\FOSJsRoutingBundle(), - new BD\GuzzleSiteAuthenticatorBundle\BDGuzzleSiteAuthenticatorBundle(), new OldSound\RabbitMqBundle\OldSoundRabbitMqBundle(), new Http\HttplugBundle\HttplugBundle(), new Sentry\SentryBundle\SentryBundle(), diff --git a/app/config/services.yml b/app/config/services.yml index f1353c3b2..1e1cf6a23 100644 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -34,7 +34,7 @@ services: Wallabag\CoreBundle\: resource: '../../src/Wallabag/CoreBundle/*' - exclude: ['../../src/Wallabag/CoreBundle/{Consumer,Controller,Entity,DataFixtures,Redis}', '../../src/Wallabag/CoreBundle/Event/*Event.php'] + exclude: ['../../src/Wallabag/CoreBundle/{Consumer,Controller,Entity,ExpressionLanguage,DataFixtures,Redis}', '../../src/Wallabag/CoreBundle/Event/*Event.php'] # controllers are imported separately to make sure services can be injected # as action arguments even if you don't extend any base controller class @@ -191,20 +191,21 @@ services: wallabag_core.http_client: alias: 'httplug.client.wallabag_core' - Wallabag\CoreBundle\GuzzleSiteAuthenticator\GrabySiteConfigBuilder: + Wallabag\CoreBundle\SiteConfig\GrabySiteConfigBuilder: tags: - { name: monolog.logger, channel: graby } # service alias override - bd_guzzle_site_authenticator.site_config_builder: - alias: Wallabag\CoreBundle\GuzzleSiteAuthenticator\GrabySiteConfigBuilder + Wallabag\CoreBundle\SiteConfig\SiteConfigBuilder: + alias: Wallabag\CoreBundle\SiteConfig\GrabySiteConfigBuilder GuzzleHttp\Cookie\CookieJar: alias: 'Wallabag\CoreBundle\Helper\FileCookieJar' Wallabag\CoreBundle\Helper\HttpClientFactory: calls: - - ["addSubscriber", ["@bd_guzzle_site_authenticator.authenticator_subscriber"]] + - ['addSubscriber', ['@Wallabag\CoreBundle\Guzzle\AuthenticatorSubscriber']] + - ['addSubscriber', ['@Wallabag\CoreBundle\Guzzle\FixupMondeDiplomatiqueUriSubscriber']] RulerZ\RulerZ: alias: rulerz diff --git a/composer.json b/composer.json index 4df38480c..f1f31dd0d 100644 --- a/composer.json +++ b/composer.json @@ -58,7 +58,6 @@ "ext-tokenizer": "*", "ext-xml": "*", "babdev/pagerfanta-bundle": "^3.8", - "bdunogier/guzzle-site-authenticator": "^1.1.0", "craue/config-bundle": "^2.7.0", "defuse/php-encryption": "^2.4", "doctrine/collections": "^1.8", diff --git a/composer.lock b/composer.lock index c3e157f50..255066584 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": "3676d938a5f11d9556b4c3047f08bf86", + "content-hash": "4ade839f1958082269247bc7ef3ac845", "packages": [ { "name": "babdev/pagerfanta-bundle", @@ -143,63 +143,6 @@ }, "time": "2022-12-07T17:46:57+00:00" }, - { - "name": "bdunogier/guzzle-site-authenticator", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/wallabag/guzzle-site-authenticator.git", - "reference": "16a73a973000cde431f45deb6a45b4315fc4d391" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/wallabag/guzzle-site-authenticator/zipball/16a73a973000cde431f45deb6a45b4315fc4d391", - "reference": "16a73a973000cde431f45deb6a45b4315fc4d391", - "shasum": "" - }, - "require": { - "guzzlehttp/guzzle": "^5.3.1", - "psr/log": "^1.0.0", - "symfony/config": "^4.4|^5.4|^6.0", - "symfony/dependency-injection": "^4.4|^5.4|^6.0", - "symfony/expression-language": "^4.4|^5.4|^6.0", - "symfony/http-kernel": "^4.4|^5.4|^6.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.4.0", - "monolog/monolog": "^2.3", - "nyholm/symfony-bundle-test": "^2.0", - "symfony/phpunit-bridge": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "BD\\GuzzleSiteAuthenticator\\": "lib/", - "BD\\GuzzleSiteAuthenticatorBundle\\": "bundle/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bertrand Dunogier", - "email": "bertrand.dunogier@gmail.com" - } - ], - "description": "A guzzle plugin that adds, if necessary, authentication data to requests. Uses credentials and cookies, with login requests to the sites.", - "support": { - "issues": "https://github.com/wallabag/guzzle-site-authenticator/issues", - "source": "https://github.com/wallabag/guzzle-site-authenticator/tree/1.1.0" - }, - "time": "2023-08-21T14:33:48+00:00" - }, { "name": "beberlei/assert", "version": "v3.3.2", diff --git a/src/Wallabag/CoreBundle/ExpressionLanguage/AuthenticatorProvider.php b/src/Wallabag/CoreBundle/ExpressionLanguage/AuthenticatorProvider.php new file mode 100644 index 000000000..8e366b131 --- /dev/null +++ b/src/Wallabag/CoreBundle/ExpressionLanguage/AuthenticatorProvider.php @@ -0,0 +1,96 @@ +guzzle = $guzzle; + } + + public function getFunctions(): array + { + $result = [ + $this->getRequestHtmlFunction(), + $this->getXpathFunction(), + $this->getPregMatchFunction(), + ]; + + return $result; + } + + private function getRequestHtmlFunction() + { + return new ExpressionFunction( + 'request_html', + function () { + throw new \Exception('Not supported'); + }, + function (array $arguments, $uri, array $options = []) { + return $this->guzzle->get($uri, $options)->getBody(); + } + ); + } + + private function getPregMatchFunction() + { + return new ExpressionFunction( + 'preg_match', + function () { + throw new \Exception('Not supported'); + }, + function (array $arguments, $pattern, $html) { + preg_match($pattern, $html, $matches); + + if (2 !== \count($matches)) { + return ''; + } + + return $matches[1]; + } + ); + } + + private function getXpathFunction() + { + return new ExpressionFunction( + 'xpath', + function () { + throw new \Exception('Not supported'); + }, + function (array $arguments, $xpathQuery, $html) { + $useInternalErrors = libxml_use_internal_errors(true); + + $doc = new \DOMDocument(); + $doc->loadHTML((string) $html, \LIBXML_NOCDATA | \LIBXML_NOWARNING | \LIBXML_NOERROR); + + $xpath = new \DOMXPath($doc); + $domNodeList = $xpath->query($xpathQuery); + + if (0 === $domNodeList->length) { + return ''; + } + + $domNode = $domNodeList->item(0); + + libxml_use_internal_errors($useInternalErrors); + + if (null === $domNode || null === $domNode->attributes) { + return ''; + } + + return $domNode->attributes->getNamedItem('value')->nodeValue; + } + ); + } +} diff --git a/src/Wallabag/CoreBundle/Guzzle/AuthenticatorSubscriber.php b/src/Wallabag/CoreBundle/Guzzle/AuthenticatorSubscriber.php new file mode 100644 index 000000000..1c6156049 --- /dev/null +++ b/src/Wallabag/CoreBundle/Guzzle/AuthenticatorSubscriber.php @@ -0,0 +1,123 @@ +configBuilder = $configBuilder; + $this->authenticatorFactory = $authenticatorFactory; + $this->logger = new NullLogger(); + } + + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } + + public function getEvents(): array + { + return [ + 'before' => ['loginIfRequired'], + 'complete' => ['loginIfRequested'], + ]; + } + + public function loginIfRequired(BeforeEvent $event) + { + $config = $this->buildSiteConfig($event->getRequest()); + if (false === $config || !$config->requiresLogin()) { + $this->logger->debug('loginIfRequired> will not require login'); + + return; + } + + $client = $event->getClient(); + $authenticator = $this->authenticatorFactory->buildFromSiteConfig($config); + + if (!$authenticator->isLoggedIn($client)) { + $this->logger->debug('loginIfRequired> user is not logged in, attach authenticator'); + + $emitter = $client->getEmitter(); + $emitter->detach($this); + $authenticator->login($client); + $emitter->attach($this); + } + } + + public function loginIfRequested(CompleteEvent $event) + { + $config = $this->buildSiteConfig($event->getRequest()); + if (false === $config || !$config->requiresLogin()) { + $this->logger->debug('loginIfRequested> will not require login'); + + return; + } + + $body = $event->getResponse()->getBody(); + + if ( + null === $body + || '' === $body->getContents() + ) { + $this->logger->debug('loginIfRequested> empty body, ignoring'); + + return; + } + + $authenticator = $this->authenticatorFactory->buildFromSiteConfig($config); + $isLoginRequired = $authenticator->isLoginRequired($body); + + $this->logger->debug('loginIfRequested> retry #' . self::$retries . ' with login ' . ($isLoginRequired ? '' : 'not ') . 'required'); + + if ($isLoginRequired && self::$retries < self::MAX_RETRIES) { + $client = $event->getClient(); + + $emitter = $client->getEmitter(); + $emitter->detach($this); + $authenticator->login($client); + $emitter->attach($this); + + $event->retry(); + + ++self::$retries; + } + } + + /** + * @return SiteConfig|false + */ + private function buildSiteConfig(RequestInterface $request) + { + return $this->configBuilder->buildForHost($request->getHost()); + } +} diff --git a/src/Wallabag/CoreBundle/Guzzle/FixupMondeDiplomatiqueUriSubscriber.php b/src/Wallabag/CoreBundle/Guzzle/FixupMondeDiplomatiqueUriSubscriber.php new file mode 100644 index 000000000..5a7358574 --- /dev/null +++ b/src/Wallabag/CoreBundle/Guzzle/FixupMondeDiplomatiqueUriSubscriber.php @@ -0,0 +1,33 @@ + [['fixUri', 500]]]; + } + + public function fixUri(CompleteEvent $event) + { + $response = $event->getResponse(); + + if (!$response->hasHeader('Location')) { + return; + } + + $uri = $response->getHeader('Location'); + if (false === ($badParameter = strstr($uri, 'retour=http://'))) { + return; + } + + $response->setHeader('Location', str_replace($badParameter, urlencode($badParameter), $uri)); + } +} diff --git a/src/Wallabag/CoreBundle/SiteConfig/ArraySiteConfigBuilder.php b/src/Wallabag/CoreBundle/SiteConfig/ArraySiteConfigBuilder.php new file mode 100644 index 000000000..853ebf395 --- /dev/null +++ b/src/Wallabag/CoreBundle/SiteConfig/ArraySiteConfigBuilder.php @@ -0,0 +1,34 @@ + SiteConfig. + */ + private $configs = []; + + public function __construct(array $hostConfigMap = []) + { + foreach ($hostConfigMap as $host => $hostConfig) { + $hostConfig['host'] = $host; + $this->configs[$host] = new SiteConfig($hostConfig); + } + } + + public function buildForHost($host) + { + $host = strtolower($host); + + if ('www.' === substr($host, 0, 4)) { + $host = substr($host, 4); + } + + if (isset($this->configs[$host])) { + return $this->configs[$host]; + } + + return false; + } +} diff --git a/src/Wallabag/CoreBundle/SiteConfig/Authenticator/Authenticator.php b/src/Wallabag/CoreBundle/SiteConfig/Authenticator/Authenticator.php new file mode 100644 index 000000000..4481d2b95 --- /dev/null +++ b/src/Wallabag/CoreBundle/SiteConfig/Authenticator/Authenticator.php @@ -0,0 +1,31 @@ +siteConfig = $siteConfig; + } + + public function login(ClientInterface $guzzle) + { + $postFields = [ + $this->siteConfig->getUsernameField() => $this->siteConfig->getUsername(), + $this->siteConfig->getPasswordField() => $this->siteConfig->getPassword(), + ] + $this->getExtraFields($guzzle); + + $guzzle->post( + $this->siteConfig->getLoginUri(), + ['body' => $postFields, 'allow_redirects' => true, 'verify' => false] + ); + + return $this; + } + + public function isLoggedIn(ClientInterface $guzzle) + { + if (($cookieJar = $guzzle->getDefaultOption('cookies')) instanceof CookieJar) { + /** @var \GuzzleHttp\Cookie\SetCookie $cookie */ + foreach ($cookieJar as $cookie) { + // check required cookies + if ($cookie->getDomain() === $this->siteConfig->getHost()) { + return true; + } + } + } + + return false; + } + + public function isLoginRequired($html) + { + $useInternalErrors = libxml_use_internal_errors(true); + + // need to check for the login dom element ($options['not_logged_in_xpath']) in the HTML + $doc = new \DOMDocument(); + $doc->loadHTML($html); + + $xpath = new \DOMXPath($doc); + $loggedIn = $xpath->evaluate((string) $this->siteConfig->getNotLoggedInXpath()); + + if (false === $loggedIn) { + return false; + } + + libxml_use_internal_errors($useInternalErrors); + + return $loggedIn->length > 0; + } + + /** + * Returns extra fields from the configuration. + * Evaluates any field value that is an expression language string. + * + * @return array + */ + private function getExtraFields(ClientInterface $guzzle) + { + $extraFields = []; + + foreach ($this->siteConfig->getExtraFields() as $fieldName => $fieldValue) { + if ('@=' === substr($fieldValue, 0, 2)) { + $expressionLanguage = $this->getExpressionLanguage($guzzle); + $fieldValue = $expressionLanguage->evaluate( + substr($fieldValue, 2), + [ + 'config' => $this->siteConfig, + ] + ); + } + + $extraFields[$fieldName] = $fieldValue; + } + + return $extraFields; + } + + /** + * @return ExpressionLanguage + */ + private function getExpressionLanguage(ClientInterface $guzzle) + { + return new ExpressionLanguage( + null, + [new AuthenticatorProvider($guzzle)] + ); + } +} diff --git a/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php b/src/Wallabag/CoreBundle/SiteConfig/GrabySiteConfigBuilder.php similarity index 95% rename from src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php rename to src/Wallabag/CoreBundle/SiteConfig/GrabySiteConfigBuilder.php index edc7d8402..276582ed2 100644 --- a/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php +++ b/src/Wallabag/CoreBundle/SiteConfig/GrabySiteConfigBuilder.php @@ -1,9 +1,7 @@ $propertyValue) { + if (!property_exists($this, $propertyName)) { + throw new \InvalidArgumentException('Unknown property: "' . $propertyName . '"'); + } + + $this->$propertyName = $propertyValue; + } + } + + /** + * @return bool + */ + public function requiresLogin() + { + return $this->requiresLogin; + } + + /** + * @param bool $requiresLogin + * + * @return SiteConfig + */ + public function setRequiresLogin($requiresLogin) + { + $this->requiresLogin = $requiresLogin; + + return $this; + } + + /** + * @return string + */ + public function getNotLoggedInXpath() + { + return $this->notLoggedInXpath; + } + + /** + * @param string $notLoggedInXpath + * + * @return SiteConfig + */ + public function setNotLoggedInXpath($notLoggedInXpath) + { + $this->notLoggedInXpath = $notLoggedInXpath; + + return $this; + } + + /** + * @return string + */ + public function getLoginUri() + { + return $this->loginUri; + } + + /** + * @param string $loginUri + * + * @return SiteConfig + */ + public function setLoginUri($loginUri) + { + $this->loginUri = $loginUri; + + return $this; + } + + /** + * @return string + */ + public function getUsernameField() + { + return $this->usernameField; + } + + /** + * @param string $usernameField + * + * @return SiteConfig + */ + public function setUsernameField($usernameField) + { + $this->usernameField = $usernameField; + + return $this; + } + + /** + * @return string + */ + public function getPasswordField() + { + return $this->passwordField; + } + + /** + * @param string $passwordField + * + * @return SiteConfig + */ + public function setPasswordField($passwordField) + { + $this->passwordField = $passwordField; + + return $this; + } + + /** + * @return array + */ + public function getExtraFields() + { + return $this->extraFields; + } + + /** + * @param array $extraFields + * + * @return SiteConfig + */ + public function setExtraFields($extraFields) + { + $this->extraFields = $extraFields; + + return $this; + } + + /** + * @return string + */ + public function getHost() + { + return $this->host; + } + + /** + * @param string $host + * + * @return SiteConfig + */ + public function setHost($host) + { + $this->host = $host; + + return $this; + } + + public function getUsername() + { + return $this->username; + } + + /** + * @return SiteConfig + */ + public function setUsername($username) + { + $this->username = $username; + + return $this; + } + + /** + * @return string + */ + public function getPassword() + { + return $this->password; + } + + /** + * @param string $password + * + * @return SiteConfig + */ + public function setPassword($password) + { + $this->password = $password; + + return $this; + } +} diff --git a/src/Wallabag/CoreBundle/SiteConfig/SiteConfigBuilder.php b/src/Wallabag/CoreBundle/SiteConfig/SiteConfigBuilder.php new file mode 100644 index 000000000..cc5947e0a --- /dev/null +++ b/src/Wallabag/CoreBundle/SiteConfig/SiteConfigBuilder.php @@ -0,0 +1,17 @@ +getEvents(); + + $this->assertArrayHasKey('before', $events); + $this->assertArrayHasKey('complete', $events); + $this->assertSame('loginIfRequired', $events['before'][0]); + $this->assertSame('loginIfRequested', $events['complete'][0]); + } + + public function testLoginIfRequiredNotRequired() + { + $builder = new ArraySiteConfigBuilder(['example.com' => []]); + $subscriber = new AuthenticatorSubscriber($builder, new Factory()); + + $logger = new Logger('foo'); + $handler = new TestHandler(); + $logger->pushHandler($handler); + + $subscriber->setLogger($logger); + + $request = new Request('GET', 'http://www.example.com'); + + $event = $this->getMockBuilder(BeforeEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + $event->expects($this->once()) + ->method('getRequest') + ->willReturn($request); + + $subscriber->loginIfRequired($event); + + $records = $handler->getRecords(); + + $this->assertCount(1, $records); + $this->assertSame('loginIfRequired> will not require login', $records[0]['message']); + } + + public function testLoginIfRequiredWithNotLoggedInUser() + { + $authenticator = $this->getMockBuilder(Authenticator::class) + ->disableOriginalConstructor() + ->getMock(); + + $authenticator->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + + $authenticator->expects($this->once()) + ->method('login'); + + $factory = $this->getMockBuilder(Factory::class) + ->disableOriginalConstructor() + ->getMock(); + + $factory->expects($this->once()) + ->method('buildFromSiteConfig') + ->willReturn($authenticator); + + $builder = new ArraySiteConfigBuilder(['example.com' => ['requiresLogin' => true]]); + $subscriber = new AuthenticatorSubscriber($builder, $factory); + + $logger = new Logger('foo'); + $handler = new TestHandler(); + $logger->pushHandler($handler); + + $subscriber->setLogger($logger); + + $response = new Response( + 200, + ['content-type' => 'text/html'], + Stream::factory('') + ); + $guzzle = new Client(); + $guzzle->getEmitter()->attach(new Mock([$response])); + + $request = new Request('GET', 'http://www.example.com'); + + $event = $this->getMockBuilder(BeforeEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + $event->expects($this->once()) + ->method('getRequest') + ->willReturn($request); + + $event->expects($this->once()) + ->method('getClient') + ->willReturn($guzzle); + + $subscriber->loginIfRequired($event); + + $records = $handler->getRecords(); + + $this->assertCount(1, $records); + $this->assertSame('loginIfRequired> user is not logged in, attach authenticator', $records[0]['message']); + } + + public function testLoginIfRequestedNotRequired() + { + $builder = new ArraySiteConfigBuilder(['example.com' => []]); + $subscriber = new AuthenticatorSubscriber($builder, new Factory()); + + $logger = new Logger('foo'); + $handler = new TestHandler(); + $logger->pushHandler($handler); + + $subscriber->setLogger($logger); + + $request = new Request('GET', 'http://www.example.com'); + + $event = $this->getMockBuilder(CompleteEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + $event->expects($this->once()) + ->method('getRequest') + ->willReturn($request); + + $subscriber->loginIfRequested($event); + + $records = $handler->getRecords(); + + $this->assertCount(1, $records); + $this->assertSame('loginIfRequested> will not require login', $records[0]['message']); + } + + public function testLoginIfRequestedNotRequested() + { + $authenticator = $this->getMockBuilder(Authenticator::class) + ->disableOriginalConstructor() + ->getMock(); + + $authenticator->expects($this->once()) + ->method('isLoginRequired') + ->willReturn(false); + + $factory = $this->getMockBuilder(Factory::class) + ->disableOriginalConstructor() + ->getMock(); + + $factory->expects($this->once()) + ->method('buildFromSiteConfig') + ->willReturn($authenticator); + + $builder = new ArraySiteConfigBuilder(['example.com' => [ + 'requiresLogin' => true, + 'notLoggedInXpath' => '//html', + ]]); + $subscriber = new AuthenticatorSubscriber($builder, $factory); + + $logger = new Logger('foo'); + $handler = new TestHandler(); + $logger->pushHandler($handler); + + $subscriber->setLogger($logger); + + $response = new Response( + 200, + ['content-type' => 'text/html'], + Stream::factory('
+ + +') + ); + $request = new Request('GET', 'http://www.example.com'); + + $event = $this->getMockBuilder(CompleteEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + $event->expects($this->once()) + ->method('getResponse') + ->willReturn($response); + + $event->expects($this->once()) + ->method('getRequest') + ->willReturn($request); + + $subscriber->loginIfRequested($event); + + $records = $handler->getRecords(); + + $this->assertCount(1, $records); + $this->assertSame('loginIfRequested> retry #0 with login not required', $records[0]['message']); + } + + public function testLoginIfRequestedRequested() + { + $authenticator = $this->getMockBuilder(Authenticator::class) + ->disableOriginalConstructor() + ->getMock(); + + $authenticator->expects($this->once()) + ->method('isLoginRequired') + ->willReturn(true); + + $authenticator->expects($this->once()) + ->method('login'); + + $factory = $this->getMockBuilder(Factory::class) + ->disableOriginalConstructor() + ->getMock(); + + $factory->expects($this->once()) + ->method('buildFromSiteConfig') + ->willReturn($authenticator); + + $builder = new ArraySiteConfigBuilder(['example.com' => [ + 'requiresLogin' => true, + 'notLoggedInXpath' => '//html', + ]]); + $subscriber = new AuthenticatorSubscriber($builder, $factory); + + $logger = new Logger('foo'); + $handler = new TestHandler(); + $logger->pushHandler($handler); + + $subscriber->setLogger($logger); + + $response = new Response( + 200, + ['content-type' => 'text/html'], + Stream::factory('
') + ); + $guzzle = new Client(); + $guzzle->getEmitter()->attach(new Mock([$response])); + $request = new Request('GET', 'http://www.example.com'); + + $event = $this->getMockBuilder(CompleteEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + $event->expects($this->once()) + ->method('getResponse') + ->willReturn($response); + + $event->expects($this->once()) + ->method('getRequest') + ->willReturn($request); + + $event->expects($this->any()) + ->method('getClient') + ->willReturn($guzzle); + + $subscriber->loginIfRequested($event); + + $records = $handler->getRecords(); + + $this->assertCount(1, $records); + $this->assertSame('loginIfRequested> retry #0 with login required', $records[0]['message']); + } + + public function testLoginIfRequestedRedirect() + { + $factory = $this->getMockBuilder(Factory::class) + ->disableOriginalConstructor() + ->getMock(); + + $builder = new ArraySiteConfigBuilder(['example.com' => [ + 'requiresLogin' => true, + 'notLoggedInXpath' => '//html', + ]]); + $subscriber = new AuthenticatorSubscriber($builder, $factory); + + $logger = new Logger('foo'); + $handler = new TestHandler(); + $logger->pushHandler($handler); + + $subscriber->setLogger($logger); + + $response = new Response( + 301, + [], + Stream::factory('') + ); + $guzzle = new Client(); + $guzzle->getEmitter()->attach(new Mock([$response])); + $request = new Request('GET', 'http://www.example.com'); + + $event = $this->getMockBuilder(CompleteEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + $event->expects($this->once()) + ->method('getResponse') + ->willReturn($response); + + $event->expects($this->once()) + ->method('getRequest') + ->willReturn($request); + + $event->expects($this->any()) + ->method('getClient') + ->willReturn($guzzle); + + $subscriber->loginIfRequested($event); + + $records = $handler->getRecords(); + + $this->assertCount(1, $records); + $this->assertSame('loginIfRequested> empty body, ignoring', $records[0]['message']); + } +} diff --git a/tests/Wallabag/CoreBundle/Guzzle/FixupMondeDiplomatiqueUriSubscriberTest.php b/tests/Wallabag/CoreBundle/Guzzle/FixupMondeDiplomatiqueUriSubscriberTest.php new file mode 100644 index 000000000..043154295 --- /dev/null +++ b/tests/Wallabag/CoreBundle/Guzzle/FixupMondeDiplomatiqueUriSubscriberTest.php @@ -0,0 +1,95 @@ +getEvents(); + + $this->assertArrayHasKey('complete', $events); + $this->assertCount(2, $events['complete'][0]); + } + + public function testGetEventsWithoutHeaderLocation() + { + $response = new Response( + 200, + [ + 'content-type' => 'text/html', + ], + Stream::factory('') + ); + + $event = $this->getMockBuilder(CompleteEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + $event->expects($this->once()) + ->method('getResponse') + ->willReturn($response); + + $subscriber = new FixupMondeDiplomatiqueUriSubscriber(); + $subscriber->fixUri($event); + + $this->assertFalse($response->hasHeader('Location')); + } + + public function testGetEventsWithNotMachingHeaderLocation() + { + $response = new Response( + 200, + [ + 'content-type' => 'text/html', + 'Location' => 'http://example.com', + ], + Stream::factory('') + ); + + $event = $this->getMockBuilder(CompleteEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + $event->expects($this->once()) + ->method('getResponse') + ->willReturn($response); + + $subscriber = new FixupMondeDiplomatiqueUriSubscriber(); + $subscriber->fixUri($event); + + $this->assertSame('http://example.com', $response->getHeader('Location')); + } + + public function testGetEventsWithMachingHeaderLocation() + { + $response = new Response( + 200, + [ + 'content-type' => 'text/html', + 'Location' => 'http://example.com/?foo=bar&retour=http://example.com', + ], + Stream::factory('') + ); + + $event = $this->getMockBuilder(CompleteEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + $event->expects($this->once()) + ->method('getResponse') + ->willReturn($response); + + $subscriber = new FixupMondeDiplomatiqueUriSubscriber(); + $subscriber->fixUri($event); + + $this->assertSame('http://example.com/?foo=bar&retour%3Dhttp%3A%2F%2Fexample.com', $response->getHeader('Location')); + } +} diff --git a/tests/Wallabag/CoreBundle/SiteConfig/ArraySiteConfigBuilderTest.php b/tests/Wallabag/CoreBundle/SiteConfig/ArraySiteConfigBuilderTest.php new file mode 100644 index 000000000..8ebb55c3b --- /dev/null +++ b/tests/Wallabag/CoreBundle/SiteConfig/ArraySiteConfigBuilderTest.php @@ -0,0 +1,26 @@ + []]); + $res = $builder->buildForHost('www.example.com'); + + $this->assertInstanceOf(SiteConfig::class, $res); + } + + public function testItReturnsFalseOnAHostThatDoesNotExist() + { + $builder = new ArraySiteConfigBuilder(['anotherexample.com' => []]); + $res = $builder->buildForHost('example.com'); + + $this->assertfalse($res); + } +} diff --git a/tests/Wallabag/CoreBundle/SiteConfig/Authenticator/LoginFormAuthenticatorTest.php b/tests/Wallabag/CoreBundle/SiteConfig/Authenticator/LoginFormAuthenticatorTest.php new file mode 100644 index 000000000..d0189c825 --- /dev/null +++ b/tests/Wallabag/CoreBundle/SiteConfig/Authenticator/LoginFormAuthenticatorTest.php @@ -0,0 +1,235 @@ + 'text/html'], + Stream::factory('') + ); + $guzzle = new Client(); + $guzzle->getEmitter()->attach(new Mock([$response])); + + $siteConfig = new SiteConfig([ + 'host' => 'example.com', + 'loginUri' => 'http://example.com/login', + 'usernameField' => 'username', + 'passwordField' => 'password', + 'extraFields' => [ + 'action' => 'login', + 'foo' => 'bar', + ], + 'username' => 'johndoe', + 'password' => 'unkn0wn', + ]); + + $auth = new LoginFormAuthenticator($siteConfig); + $res = $auth->login($guzzle); + + $this->assertInstanceOf(LoginFormAuthenticator::class, $res); + } + + public function testLoginPostWithExtraFieldsButEmptyHtml() + { + $response = new Response( + 200, + ['content-type' => 'text/html'], + Stream::factory('') + ); + $guzzle = new Client(); + $guzzle->getEmitter()->attach(new Mock([$response, $response])); + + $siteConfig = new SiteConfig([ + 'host' => 'example.com', + 'loginUri' => 'http://example.com/login', + 'usernameField' => 'username', + 'passwordField' => 'password', + 'extraFields' => [ + 'action' => 'login', + 'foo' => 'bar', + 'security' => '@=xpath(\'substring(//script[contains(text(), "security")]/text(), 112, 10)\', request_html(\'https://aoc.media/\'))', + ], + 'username' => 'johndoe', + 'password' => 'unkn0wn', + ]); + + $auth = new LoginFormAuthenticator($siteConfig); + $res = $auth->login($guzzle); + + $this->assertInstanceOf(LoginFormAuthenticator::class, $res); + } + + // testing preg_match + public function testLoginPostWithExtraFieldsWithRegex() + { + $response = $this->getMockBuilder(Response::class) + ->disableOriginalConstructor() + ->getMock(); + + $response->expects($this->any()) + ->method('getBody') + ->willReturn(file_get_contents(__DIR__ . '/../../fixtures/aoc.media.html')); + + $response->expects($this->any()) + ->method('getStatusCode') + ->willReturn(200); + + $client = $this->getMockBuilder(Client::class) + ->disableOriginalConstructor() + ->getMock(); + + $client->expects($this->any()) + ->method('post') + ->with( + $this->equalTo('https://aoc.media/wp-admin/admin-ajax.php'), + $this->equalTo([ + 'body' => [ + 'nom' => 'johndoe', + 'password' => 'unkn0wn', + 'security' => 'c506c1b8bc', + 'action' => 'login_user', + ], + 'allow_redirects' => true, + 'verify' => false, + ]) + ) + ->willReturn($response); + + $client->expects($this->any()) + ->method('get') + ->with( + $this->equalTo('https://aoc.media/'), + $this->equalTo([]) + ) + ->willReturn($response); + + $siteConfig = new SiteConfig([ + 'host' => 'aoc.media', + 'loginUri' => 'https://aoc.media/wp-admin/admin-ajax.php', + 'usernameField' => 'nom', + 'passwordField' => 'password', + 'extraFields' => [ + 'action' => 'login_user', + 'security' => '@=preg_match(\'/security\\\":\\\"([a-z0-9]+)\\\"/si\', request_html(\'https://aoc.media/\'))', + ], + 'username' => 'johndoe', + 'password' => 'unkn0wn', + ]); + + $auth = new LoginFormAuthenticator($siteConfig); + $res = $auth->login($client); + + $this->assertInstanceOf(LoginFormAuthenticator::class, $res); + } + + public function testLoginPostWithExtraFieldsWithData() + { + $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); + + $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); + + $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', + ]); + + $auth = new LoginFormAuthenticator($siteConfig); + $res = $auth->login($client); + + $this->assertInstanceOf(LoginFormAuthenticator::class, $res); + } + + public function testLoginWithBadSiteConfigNotLoggedInData() + { + $siteConfig = new SiteConfig([ + 'host' => 'nextinpact.com', + 'loginUri' => 'https://compte.nextinpact.com/Account/Login', + 'usernameField' => 'UserName', + 'username' => 'johndoe', + 'password' => 'unkn0wn', + ]); + + $auth = new LoginFormAuthenticator($siteConfig); + $loginRequired = $auth->isLoginRequired(file_get_contents(__DIR__ . '/../../fixtures/nextinpact-login.html')); + + $this->assertFalse($loginRequired); + } + + public function testLoginWithGoodSiteConfigNotLoggedInData() + { + $siteConfig = new SiteConfig([ + 'host' => 'nextinpact.com', + 'loginUri' => 'https://compte.nextinpact.com/Account/Login', + 'usernameField' => 'UserName', + 'username' => 'johndoe', + 'password' => 'unkn0wn', + 'notLoggedInXpath' => '//h2[@class="title_reserve_article"]', + ]); + + $auth = new LoginFormAuthenticator($siteConfig); + $loginRequired = $auth->isLoginRequired(file_get_contents(__DIR__ . '/../../fixtures/nextinpact-article.html')); + + $this->assertTrue($loginRequired); + } +} diff --git a/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php b/tests/Wallabag/CoreBundle/SiteConfig/GrabySiteConfigBuilderTest.php similarity index 98% rename from tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php rename to tests/Wallabag/CoreBundle/SiteConfig/GrabySiteConfigBuilderTest.php index 7a3a1c0cf..4b78b99da 100644 --- a/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php +++ b/tests/Wallabag/CoreBundle/SiteConfig/GrabySiteConfigBuilderTest.php @@ -1,6 +1,6 @@ assertInstanceOf(SiteConfig::class, $config); + } + + public function testUnknownProperty() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unknown property: "bad"'); + + new SiteConfig(['bad' => true]); + } + + public function testInitSiteConfigWillFullOptions() + { + $config = new SiteConfig([ + 'host' => 'example.com', + 'requiresLogin' => true, + 'notLoggedInXpath' => '//all', + 'loginUri' => 'https://example.com/login', + 'usernameField' => 'username', + 'passwordField' => 'password', + 'extraFields' => [ + 'action' => 'login', + 'foo' => 'bar', + ], + 'username' => 'johndoe', + 'password' => 'unkn0wn', + ]); + + $this->assertInstanceOf(SiteConfig::class, $config); + } +} diff --git a/tests/Wallabag/CoreBundle/fixtures/aoc.media.html b/tests/Wallabag/CoreBundle/fixtures/aoc.media.html new file mode 100644 index 000000000..9b5a3de86 --- /dev/null +++ b/tests/Wallabag/CoreBundle/fixtures/aoc.media.html @@ -0,0 +1,860 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +