CookieJarTest.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. <?php
  2. namespace GuzzleHttp\Tests\CookieJar;
  3. use DateInterval;
  4. use DateTime;
  5. use DateTimeImmutable;
  6. use GuzzleHttp\Cookie\CookieJar;
  7. use GuzzleHttp\Cookie\SetCookie;
  8. use GuzzleHttp\Psr7\Request;
  9. use GuzzleHttp\Psr7\Response;
  10. use PHPUnit\Framework\TestCase;
  11. /**
  12. * @covers \GuzzleHttp\Cookie\CookieJar
  13. */
  14. class CookieJarTest extends TestCase
  15. {
  16. /**
  17. * @var CookieJar
  18. */
  19. private $jar;
  20. public function setUp(): void
  21. {
  22. $this->jar = new CookieJar();
  23. }
  24. protected function getTestCookies()
  25. {
  26. return [
  27. new SetCookie(['Name' => 'foo', 'Value' => 'bar', 'Domain' => 'foo.com', 'Path' => '/', 'Discard' => true]),
  28. new SetCookie(['Name' => 'test', 'Value' => '123', 'Domain' => 'baz.com', 'Path' => '/foo', 'Expires' => 2]),
  29. new SetCookie(['Name' => 'you', 'Value' => '123', 'Domain' => 'bar.com', 'Path' => '/boo', 'Expires' => \time() + 1000]),
  30. ];
  31. }
  32. public function testCreatesFromArray()
  33. {
  34. $jar = CookieJar::fromArray([
  35. 'foo' => 'bar',
  36. 'baz' => 'bam',
  37. ], 'example.com');
  38. self::assertCount(2, $jar);
  39. }
  40. public function testEmptyJarIsCountable()
  41. {
  42. self::assertCount(0, new CookieJar());
  43. }
  44. public function testGetsCookiesByName()
  45. {
  46. $cookies = $this->getTestCookies();
  47. foreach ($this->getTestCookies() as $cookie) {
  48. $this->jar->setCookie($cookie);
  49. }
  50. $testCookie = $cookies[0];
  51. self::assertEquals($testCookie, $this->jar->getCookieByName($testCookie->getName()));
  52. self::assertNull($this->jar->getCookieByName('doesnotexist'));
  53. self::assertNull($this->jar->getCookieByName(''));
  54. }
  55. /**
  56. * Provides test data for cookie cookieJar retrieval
  57. */
  58. public function getCookiesDataProvider()
  59. {
  60. return [
  61. [['foo', 'baz', 'test', 'muppet', 'googoo'], '', '', '', false],
  62. [['foo', 'baz', 'muppet', 'googoo'], '', '', '', true],
  63. [['googoo'], 'www.example.com', '', '', false],
  64. [['muppet', 'googoo'], 'test.y.example.com', '', '', false],
  65. [['foo', 'baz'], 'example.com', '', '', false],
  66. [['muppet'], 'x.y.example.com', '/acme/', '', false],
  67. [['muppet'], 'x.y.example.com', '/acme/test/', '', false],
  68. [['googoo'], 'x.y.example.com', '/test/acme/test/', '', false],
  69. [['foo', 'baz'], 'example.com', '', '', false],
  70. [['baz'], 'example.com', '', 'baz', false],
  71. ];
  72. }
  73. public function testStoresAndRetrievesCookies()
  74. {
  75. $cookies = $this->getTestCookies();
  76. foreach ($cookies as $cookie) {
  77. self::assertTrue($this->jar->setCookie($cookie));
  78. }
  79. self::assertCount(3, $this->jar);
  80. self::assertCount(3, $this->jar->getIterator());
  81. self::assertEquals($cookies, $this->jar->getIterator()->getArrayCopy());
  82. }
  83. public function testRemovesTemporaryCookies()
  84. {
  85. $cookies = $this->getTestCookies();
  86. foreach ($this->getTestCookies() as $cookie) {
  87. $this->jar->setCookie($cookie);
  88. }
  89. $this->jar->clearSessionCookies();
  90. self::assertEquals(
  91. [$cookies[1], $cookies[2]],
  92. $this->jar->getIterator()->getArrayCopy()
  93. );
  94. }
  95. public function testRemovesSelectively()
  96. {
  97. foreach ($this->getTestCookies() as $cookie) {
  98. $this->jar->setCookie($cookie);
  99. }
  100. // Remove foo.com cookies
  101. $this->jar->clear('foo.com');
  102. self::assertCount(2, $this->jar);
  103. // Try again, removing no further cookies
  104. $this->jar->clear('foo.com');
  105. self::assertCount(2, $this->jar);
  106. // Remove bar.com cookies with path of /boo
  107. $this->jar->clear('bar.com', '/boo');
  108. self::assertCount(1, $this->jar);
  109. // Remove cookie by name
  110. $this->jar->clear(null, null, 'test');
  111. self::assertCount(0, $this->jar);
  112. }
  113. public static function providesIncompleteCookies(): array
  114. {
  115. return [
  116. [
  117. [],
  118. ],
  119. [
  120. [
  121. 'Name' => 'foo',
  122. ],
  123. ],
  124. [
  125. [
  126. 'Name' => false,
  127. ],
  128. ],
  129. [
  130. [
  131. 'Name' => true,
  132. ],
  133. ],
  134. [
  135. [
  136. 'Name' => 'foo',
  137. 'Domain' => 'foo.com',
  138. ],
  139. ],
  140. ];
  141. }
  142. /**
  143. * @dataProvider providesIncompleteCookies
  144. */
  145. public function testDoesNotAddIncompleteCookies(array $cookie)
  146. {
  147. self::assertFalse($this->jar->setCookie(new SetCookie($cookie)));
  148. }
  149. public static function providesEmptyCookies(): array
  150. {
  151. return [
  152. [
  153. [
  154. 'Name' => '',
  155. 'Domain' => 'foo.com',
  156. 'Value' => 0,
  157. ],
  158. ],
  159. [
  160. [
  161. 'Name' => null,
  162. 'Domain' => 'foo.com',
  163. 'Value' => 0,
  164. ],
  165. ],
  166. ];
  167. }
  168. /**
  169. * @dataProvider providesEmptyCookies
  170. */
  171. public function testDoesNotAddEmptyCookies(array $cookie)
  172. {
  173. self::assertFalse($this->jar->setCookie(new SetCookie($cookie)));
  174. }
  175. public static function providesValidCookies(): array
  176. {
  177. return [
  178. [
  179. [
  180. 'Name' => '0',
  181. 'Domain' => 'foo.com',
  182. 'Value' => 0,
  183. ],
  184. ],
  185. [
  186. [
  187. 'Name' => 'foo',
  188. 'Domain' => 'foo.com',
  189. 'Value' => 0,
  190. ],
  191. ],
  192. [
  193. [
  194. 'Name' => 'foo',
  195. 'Domain' => 'foo.com',
  196. 'Value' => 0.0,
  197. ],
  198. ],
  199. [
  200. [
  201. 'Name' => 'foo',
  202. 'Domain' => 'foo.com',
  203. 'Value' => '0',
  204. ],
  205. ],
  206. ];
  207. }
  208. /**
  209. * @dataProvider providesValidCookies
  210. */
  211. public function testDoesAddValidCookies(array $cookie)
  212. {
  213. self::assertTrue($this->jar->setCookie(new SetCookie($cookie)));
  214. }
  215. public function testOverwritesCookiesThatAreOlderOrDiscardable()
  216. {
  217. $t = \time() + 1000;
  218. $data = [
  219. 'Name' => 'foo',
  220. 'Value' => 'bar',
  221. 'Domain' => '.example.com',
  222. 'Path' => '/',
  223. 'Max-Age' => '86400',
  224. 'Secure' => true,
  225. 'Discard' => true,
  226. 'Expires' => $t,
  227. ];
  228. // Make sure that the discard cookie is overridden with the non-discard
  229. self::assertTrue($this->jar->setCookie(new SetCookie($data)));
  230. self::assertCount(1, $this->jar);
  231. $data['Discard'] = false;
  232. self::assertTrue($this->jar->setCookie(new SetCookie($data)));
  233. self::assertCount(1, $this->jar);
  234. $c = $this->jar->getIterator()->getArrayCopy();
  235. self::assertFalse($c[0]->getDiscard());
  236. // Make sure it doesn't duplicate the cookie
  237. $this->jar->setCookie(new SetCookie($data));
  238. self::assertCount(1, $this->jar);
  239. // Make sure the more future-ful expiration date supersede the other
  240. $data['Expires'] = \time() + 2000;
  241. self::assertTrue($this->jar->setCookie(new SetCookie($data)));
  242. self::assertCount(1, $this->jar);
  243. $c = $this->jar->getIterator()->getArrayCopy();
  244. self::assertNotEquals($t, $c[0]->getExpires());
  245. }
  246. public function testOverwritesCookiesThatHaveChanged()
  247. {
  248. $t = \time() + 1000;
  249. $data = [
  250. 'Name' => 'foo',
  251. 'Value' => 'bar',
  252. 'Domain' => '.example.com',
  253. 'Path' => '/',
  254. 'Max-Age' => '86400',
  255. 'Secure' => true,
  256. 'Discard' => true,
  257. 'Expires' => $t,
  258. ];
  259. // Make sure that the discard cookie is overridden with the non-discard
  260. self::assertTrue($this->jar->setCookie(new SetCookie($data)));
  261. $data['Value'] = 'boo';
  262. self::assertTrue($this->jar->setCookie(new SetCookie($data)));
  263. self::assertCount(1, $this->jar);
  264. // Changing the value plus a parameter also must overwrite the existing one
  265. $data['Value'] = 'zoo';
  266. $data['Secure'] = false;
  267. self::assertTrue($this->jar->setCookie(new SetCookie($data)));
  268. self::assertCount(1, $this->jar);
  269. $c = $this->jar->getIterator()->getArrayCopy();
  270. self::assertSame('zoo', $c[0]->getValue());
  271. }
  272. public function testAddsCookiesFromResponseWithRequest()
  273. {
  274. $response = new Response(200, [
  275. 'Set-Cookie' => 'fpc=d=.Hm.yh4.1XmJWjJfs4orLQzKzPImxklQoxXSHOZATHUSEFciRueW_7704iYUtsXNEXq0M92Px2glMdWypmJ7HIQl6XIUvrZimWjQ3vIdeuRbI.FNQMAfcxu_XN1zSx7l.AcPdKL6guHc2V7hIQFhnjRW0rxm2oHY1P4bGQxFNz7f.tHm12ZD3DbdMDiDy7TBXsuP4DM-&v=2; expires=Fri, 02-Mar-2019 02:17:40 GMT;',
  276. ]);
  277. $request = new Request('GET', 'http://www.example.com');
  278. $this->jar->extractCookies($request, $response);
  279. self::assertCount(1, $this->jar);
  280. }
  281. public function getMatchingCookiesDataProvider()
  282. {
  283. return [
  284. ['https://example.com', 'foo=bar; baz=foobar'],
  285. ['http://example.com', ''],
  286. ['https://example.com:8912', 'foo=bar; baz=foobar'],
  287. ['https://foo.example.com', 'foo=bar; baz=foobar'],
  288. ['http://foo.example.com/test/acme/', 'googoo=gaga'],
  289. ];
  290. }
  291. /**
  292. * @dataProvider getMatchingCookiesDataProvider
  293. */
  294. public function testReturnsCookiesMatchingRequests(string $url, string $cookies)
  295. {
  296. $bag = [
  297. new SetCookie([
  298. 'Name' => 'foo',
  299. 'Value' => 'bar',
  300. 'Domain' => 'example.com',
  301. 'Path' => '/',
  302. 'Max-Age' => '86400',
  303. 'Secure' => true,
  304. ]),
  305. new SetCookie([
  306. 'Name' => 'baz',
  307. 'Value' => 'foobar',
  308. 'Domain' => 'example.com',
  309. 'Path' => '/',
  310. 'Max-Age' => '86400',
  311. 'Secure' => true,
  312. ]),
  313. new SetCookie([
  314. 'Name' => 'test',
  315. 'Value' => '123',
  316. 'Domain' => 'www.foobar.com',
  317. 'Path' => '/path/',
  318. 'Discard' => true,
  319. ]),
  320. new SetCookie([
  321. 'Name' => 'muppet',
  322. 'Value' => 'cookie_monster',
  323. 'Domain' => '.y.example.com',
  324. 'Path' => '/acme/',
  325. 'Expires' => \time() + 86400,
  326. ]),
  327. new SetCookie([
  328. 'Name' => 'googoo',
  329. 'Value' => 'gaga',
  330. 'Domain' => '.example.com',
  331. 'Path' => '/test/acme/',
  332. 'Max-Age' => 1500,
  333. ]),
  334. ];
  335. foreach ($bag as $cookie) {
  336. $this->jar->setCookie($cookie);
  337. }
  338. $request = new Request('GET', $url);
  339. $request = $this->jar->withCookieHeader($request);
  340. self::assertSame($cookies, $request->getHeaderLine('Cookie'));
  341. }
  342. public function testThrowsExceptionWithStrictMode()
  343. {
  344. $a = new CookieJar(true);
  345. $this->expectException(\RuntimeException::class);
  346. $this->expectExceptionMessage('Invalid cookie: Cookie name must not contain invalid characters: ASCII Control characters (0-31;127), space, tab and the following characters: ()<>@,;:\\"/?={}');
  347. $a->setCookie(new SetCookie(['Name' => "abc\n", 'Value' => 'foo', 'Domain' => 'bar']));
  348. }
  349. public function testDeletesCookiesByName()
  350. {
  351. $cookies = $this->getTestCookies();
  352. $cookies[] = new SetCookie([
  353. 'Name' => 'other',
  354. 'Value' => '123',
  355. 'Domain' => 'bar.com',
  356. 'Path' => '/boo',
  357. 'Expires' => \time() + 1000,
  358. ]);
  359. $jar = new CookieJar();
  360. foreach ($cookies as $cookie) {
  361. $jar->setCookie($cookie);
  362. }
  363. self::assertCount(4, $jar);
  364. $jar->clear('bar.com', '/boo', 'other');
  365. self::assertCount(3, $jar);
  366. $names = \array_map(static function (SetCookie $c) {
  367. return $c->getName();
  368. }, $jar->getIterator()->getArrayCopy());
  369. self::assertSame(['foo', 'test', 'you'], $names);
  370. }
  371. public function testCanConvertToAndLoadFromArray()
  372. {
  373. $jar = new CookieJar(true);
  374. foreach ($this->getTestCookies() as $cookie) {
  375. $jar->setCookie($cookie);
  376. }
  377. self::assertCount(3, $jar);
  378. $arr = $jar->toArray();
  379. self::assertCount(3, $arr);
  380. $newCookieJar = new CookieJar(false, $arr);
  381. self::assertCount(3, $newCookieJar);
  382. self::assertSame($jar->toArray(), $newCookieJar->toArray());
  383. }
  384. public function testAddsCookiesWithEmptyPathFromResponse()
  385. {
  386. $response = new Response(200, [
  387. 'Set-Cookie' => "fpc=foobar; expires={$this->futureExpirationDate()}; path=;",
  388. ]);
  389. $request = new Request('GET', 'http://www.example.com');
  390. $this->jar->extractCookies($request, $response);
  391. $newRequest = $this->jar->withCookieHeader(new Request('GET', 'http://www.example.com/foo'));
  392. self::assertTrue($newRequest->hasHeader('Cookie'));
  393. }
  394. public function getCookiePathsDataProvider()
  395. {
  396. return [
  397. ['', '/'],
  398. ['/', '/'],
  399. ['/foo', '/'],
  400. ['/foo/bar', '/foo'],
  401. ['/foo/bar/', '/foo/bar'],
  402. ];
  403. }
  404. /**
  405. * @dataProvider getCookiePathsDataProvider
  406. */
  407. public function testCookiePathWithEmptySetCookiePath(string $uriPath, string $cookiePath)
  408. {
  409. $response = (new Response(200))
  410. ->withAddedHeader(
  411. 'Set-Cookie',
  412. "foo=bar; expires={$this->futureExpirationDate()}; domain=www.example.com; path=;"
  413. )
  414. ->withAddedHeader(
  415. 'Set-Cookie',
  416. "bar=foo; expires={$this->futureExpirationDate()}; domain=www.example.com; path=foobar;"
  417. )
  418. ;
  419. $request = (new Request('GET', "https://www.example.com{$uriPath}"));
  420. $this->jar->extractCookies($request, $response);
  421. self::assertSame($cookiePath, $this->jar->toArray()[0]['Path']);
  422. self::assertSame($cookiePath, $this->jar->toArray()[1]['Path']);
  423. }
  424. public function getDomainMatchesProvider()
  425. {
  426. return [
  427. ['www.example.com', 'www.example.com', true],
  428. ['www.example.com', 'www.EXAMPLE.com', true],
  429. ['www.example.com', 'www.example.net', false],
  430. ['www.example.com', 'ftp.example.com', false],
  431. ['www.example.com', 'example.com', true],
  432. ['www.example.com', 'EXAMPLE.com', true],
  433. ['fra.de.example.com', 'EXAMPLE.com', true],
  434. ['www.EXAMPLE.com', 'www.example.com', true],
  435. ['www.EXAMPLE.com', 'www.example.COM', true],
  436. ];
  437. }
  438. /**
  439. * @dataProvider getDomainMatchesProvider
  440. */
  441. public function testIgnoresCookiesForMismatchingDomains(string $requestHost, string $domainAttribute, bool $matches)
  442. {
  443. $response = (new Response(200))
  444. ->withAddedHeader(
  445. 'Set-Cookie',
  446. "foo=bar; expires={$this->futureExpirationDate()}; domain={$domainAttribute}; path=/;"
  447. )
  448. ;
  449. $request = (new Request('GET', "https://{$requestHost}/"));
  450. $this->jar->extractCookies($request, $response);
  451. self::assertCount($matches ? 1 : 0, $this->jar->toArray());
  452. }
  453. private function futureExpirationDate()
  454. {
  455. return (new DateTimeImmutable())->add(new DateInterval('P1D'))->format(DateTime::COOKIE);
  456. }
  457. }