Completed
Push — master ( 74754e...8047a2 )
by Michael
02:17
created

PoshmarkService::getOrdersLoop()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 43
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6.5625

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 25
c 1
b 0
f 0
dl 0
loc 43
ccs 18
cts 24
cp 0.75
rs 8.8977
cc 6
nc 16
nop 1
crap 6.5625
1
<?php
2
3
/*
4
 * This file is part of michaelbutler/phposh.
5
 * Source: https://github.com/michaelbutler/phposh
6
 *
7
 * (c) Michael Butler <[email protected]>
8
 *
9
 * This source file is subject to the MIT license that is bundled
10
 * with this source code in the file named LICENSE.
11
 */
12
13
namespace PHPosh\Provider\Poshmark;
14
15
use GuzzleHttp\Client;
16
use GuzzleHttp\Exception\RequestException;
17
use PHPosh\Exception\CookieException;
18
use PHPosh\Exception\DataException;
19
use PHPosh\Exception\ItemNotFoundException;
20
use PHPosh\Exception\OrderNotFoundException;
21
use PHPosh\Shared\Provider;
22
use Psr\Http\Message\ResponseInterface;
23
use Psr\Http\Message\UriInterface;
24
use Safe\Exceptions\SafeExceptionInterface;
25
use sndsgd\Str;
26
use Symfony\Component\DomCrawler\Crawler;
27
28
/**
29
 * Browser-Cookie based PoshmarkProvider.
30
 * The way this works is, it needs the cookie data from your logged-in Poshmark browser session.
31
 * Simple way to do this is:
32
 * - Log in to www.poshmark.com
33
 * - Press Ctrl/Command + Shift + K (Firefox) or Ctrl/Command + Shift + J (Chrome)
34
 * - Type document.cookie and press Enter
35
 * - Copy and Save that entire value shown between the quotes
36
 * - $pmProvider = new PoshmarkProvider("<paste the cookie data here>");
37
 * - $items = $pmProvider->getItems()
38
 * - If & when you get an error, repeat the steps above to get the latest cookie data.
39
 */
40
class PoshmarkService implements Provider
41
{
42
    /** @var string URL upon which all requests are based */
43
    public const BASE_URL = 'https://poshmark.com';
44
45
    /** @var array Standard options for the Guzzle client */
46
    private const DEFAULT_OPTIONS = [
47
        'timeout' => 5,
48
        'base_uri' => self::BASE_URL,
49
    ];
50
51
    /** @var array Standard headers to send on each request */
52
    private const DEFAULT_HEADERS = [
53
        'Accept' => 'application/json, text/javascript, */*; q=0.01',
54
        'Accept-Language' => 'en-US,en;q=0.5',
55
        'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) snap Chromium/81.0.4044.138 Chrome/81.0.4044.138 Safari/537.36',
56
        'Accept-Encoding' => 'gzip',
57
        'Referer' => '', // Replace with actual
58
        'Cookie' => '', // Replace with actual
59
    ];
60
61
    /** @var string An HTTP referrer (referer) to use if not specified otherwise */
62
    private const DEFAULT_REFERRER = 'https://poshmark.com/feed';
63
64
    /** @var array Map of cookies to send. Used to exclude unnecessary cruft */
65
    private const COOKIE_WHITELIST = [
66
        '_csrf' => true,
67
        '__ssid' => true,
68
        'exp' => true,
69
        'ui' => true,
70
        '_uetsid' => true,
71
        '_web_session' => true,
72
        'jwt' => true,
73
    ];
74
75
    /** @var Client */
76
    private $guzzleClient;
77
78
    /** @var array Map of cookies (name => value) to use */
79
    private $cookies = [];
80
81
    /** @var string Human readable username. Auto populated from cookie data */
82
    private $username;
83
84
    /** @var string Email address (from cookie) */
85
    private $email;
86
87
    /** @var string Full name of user (from cookie) */
88
    private $fullname;
89
90
    /** @var string Poshmark user id of user (from cookie) */
91
    private $pmUserId;
92
93
    /** @var string Timestamp when the cookie was pasted in to this system. If too old, it might not work... */
94
    private $cookieTimestamp;
95
96
    /**
97
     * Client constructor. Same options as Guzzle.
98
     *
99
     * @param string $cookieCode Copy+Pasted version of document.cookie on https://poshmark.com
100
     * @param array  $config     Optional Guzzle config overrides (See Guzzle docs for Client constructor)
101
     */
102 24
    public function __construct($cookieCode, array $config = [])
103
    {
104 24
        $config = array_merge($config, static::DEFAULT_OPTIONS);
105 24
        $this->setGuzzleClient(new Client($config));
106 24
        $this->cookies = $this->parseCookiesFromString($cookieCode);
107 24
        $this->setupUserFromCookies($this->cookies);
108 24
    }
109
110
    /**
111
     * @return $this
112
     */
113 24
    public function setGuzzleClient(Client $client): self
114
    {
115 24
        $this->guzzleClient = $client;
116
117 24
        return $this;
118
    }
119
120
    /**
121
     * Get All closet items of a user. Returned items will be sorted by item id. This needs to make multiple HTTP
122
     * requests to Poshmark, not in parallel. Only 20 per page is currently supported, so this will take about 3.5
123
     * seconds for every 100 items there are, or about 30 seconds for every 1000.
124
     *
125
     * @param string $usernameUuid Uuid of user. If empty, will use yourself (from cookie).
126
     * @param string $username     Display username of user. If empty, will use yourself (from cookie).
127
     *
128
     * @throws DataException if something unexpected happened, like the user doesn't exist or couldn't fetch items
129
     *
130
     * @return Item[]
131
     */
132 4
    public function getItems(string $usernameUuid = '', string $username = ''): array
133
    {
134 4
        if (!$usernameUuid) {
135 4
            $usernameUuid = $this->pmUserId;
136
        }
137 4
        if (!$username) {
138 4
            $username = $this->username;
139
        }
140 4
        $dataParser = $this->getDataParser();
141
142
        // Set a sane upper bound; 250 * 20 = 5000 max items to get
143 4
        $iterations = 250;
144 4
        $maxId = null;
145 4
        $items = [];
146 4
        while ($iterations > 0) {
147
            try {
148 4
                $loopItems = $this->getItemsByMaxId($usernameUuid, $username, $maxId);
149 2
            } catch (DataException $e) {
150 2
                if ($items === []) {
151
                    // If we got this exception on the very first try, we should re-throw it
152 1
                    throw $e;
153
                }
154 1
                $loopItems = [];
155
            }
156 3
            if (!$loopItems || empty($loopItems['data'])) {
157 2
                break;
158
            }
159 2
            foreach ($loopItems['data'] as $item) {
160
                // Convert each raw json to an Item object
161 2
                $items[] = $dataParser->parseOneItemResponseJson($item);
162
            }
163 2
            $maxId = ($loopItems['more']['next_max_id'] ?? null);
164 2
            if ($maxId <= 0) {
165
                // No next id signifies finished listing
166 1
                break;
167
            }
168 2
            $maxId = (string) $maxId;
169 2
            --$iterations;
170
171
            // Sleep 100ms
172 2
            usleep(100000);
173
174 2
            if ($iterations % 10) {
175
                // Every 10th iteration sleep an additional amount
176 2
                usleep(200000);
177
            }
178
        }
179
180
        usort($items, static function ($a, $b) {
181
            // sort array items by their item ids
182 2
            return strcmp($a->getId(), $b->getId());
183 3
        });
184
185 3
        return $items;
186
    }
187
188
    /**
189
     * Get a single item on Poshmark by its identifier, full details.
190
     * You either get the Item or an exception is thrown.
191
     *
192
     * @param string $poshmarkItemId Poshmark Item Id
193
     *
194
     * @throws DataException             If a problem occurred while trying to get the Item
195
     * @throws ItemNotFoundException     If the item was not found (e.g. 404)
196
     * @throws \InvalidArgumentException If you passed in an invalid id (request isn't even attempted)
197
     */
198 11
    public function getItem(string $poshmarkItemId): Item
199
    {
200 11
        if (!$poshmarkItemId) {
201 1
            throw new \InvalidArgumentException('$poshmarkItemId must be non-empty');
202
        }
203 10
        $parser = $this->getDataParser();
204 10
        $headers = static::DEFAULT_HEADERS;
205 10
        $headers['Referer'] = static::DEFAULT_REFERRER;
206 10
        $headers['Cookie'] = $this->getCookieHeader();
207
208 10
        $url = '/vm-rest/posts/%s?app_version=2.55&_=%s';
209 10
        $url = sprintf($url, rawurlencode($poshmarkItemId), (string) microtime(true));
210
211
        try {
212 10
            $response = $this->makeRequest('get', $url, [
213 10
                'headers' => $headers,
214
            ]);
215 6
            $data = $this->getJsonData($response);
216 6
        } catch (DataException $e) {
217 6
            if (404 === (int) $e->getCode()) {
218 3
                throw new ItemNotFoundException("Item {$poshmarkItemId} not found");
219
            }
220
221 3
            throw $e;
222
        }
223
224 4
        return $parser->parseOneItemResponseJson($data);
225
    }
226
227
    /**
228
     * Update data of a single item. Must provide title, description, price, and brand in $itemFields for this to work.
229
     *
230
     * Example:
231
     *
232
     * @param string $poshmarkItemId PoshmarkId for the item
233
     * @param array  $itemFields     New item data -- will replace the old data. All fields are optional but you must at least
234
     *                               provide one.
235
     *                               Only these fields currently supported:
236
     *                               [
237
     *                               'title' => 'New title',
238
     *                               'description' => 'New description',
239
     *                               'price' => '4.95 USD', // Price, with currency code (will default to USD)
240
     *                               'brand' => 'Nike', // brand name
241
     *                               ]
242
     *
243
     * @throws DataException         update failed
244
     * @throws ItemNotFoundException when the item you're trying to update wasn't found
245
     *
246
     * @return bool returns true on success, throws exception on failure
247
     */
248 4
    public function updateItemRequest(string $poshmarkItemId, array $itemFields): bool
249
    {
250 4
        if (!$poshmarkItemId) {
251 1
            throw new \InvalidArgumentException('$poshmarkItemId must be non-empty');
252
        }
253 3
        $itemObj = $this->getItem($poshmarkItemId);
254
255 2
        $newItemData = Helper::createItemDataForUpdate($itemFields, $itemObj->getRawData());
256
257
        $postBody = [
258 2
            'post' => $newItemData,
259
        ];
260 2
        $postBody = json_encode($postBody);
261
262 2
        $headers = static::DEFAULT_HEADERS;
263 2
        $headers['Referer'] = self::BASE_URL . '/edit-listing/' . $poshmarkItemId;
264 2
        $headers['Cookie'] = $this->getCookieHeader();
265 2
        $headers['Content-Type'] = 'application/json';
266
267 2
        $headers['X-XSRF-TOKEN'] = $this->getXsrfTokenForEditItem($poshmarkItemId);
268 2
        usleep(200000);
269
270 2
        $url = '/vm-rest/posts/%s';
271 2
        $url = sprintf($url, rawurlencode($poshmarkItemId));
272
273 2
        $response = $this->makeRequest('post', $url, [
274 2
            'body' => $postBody,
275 2
            'headers' => $headers,
276
        ]);
277
278
        // Check response code
279 1
        $this->getHtmlData($response);
280
281 1
        return true;
282
    }
283
284
    /**
285
     * Get full details on an order, by parsing the item details page.
286
     *
287
     * @param string $orderId Poshmark OrderID
288
     *
289
     * @throws DataException
290
     * @throws OrderNotFoundException if the order wasn't found on Poshmark, or you're not the seller
291
     */
292 4
    public function getOrderDetails(string $orderId): Order
293
    {
294 4
        if ('' === $orderId) {
295 1
            throw new \InvalidArgumentException("Invalid \$orderId: {$orderId}");
296
        }
297 3
        $headers = static::DEFAULT_HEADERS;
298 3
        $headers['Referer'] = static::DEFAULT_REFERRER;
299 3
        $headers['Cookie'] = $this->getCookieHeader();
300 3
        $headers['Accept'] = 'text/html';
301
302 3
        $dataParser = $this->getDataParser();
303
304 3
        $url = '/order/sales/%s?_=%s';
305 3
        $url = sprintf($url, $orderId, (string) microtime(true));
306
307
        try {
308 3
            $response = $this->makeRequest('get', $url, [
309 3
                'headers' => $headers,
310
            ]);
311
312 2
            $html = $this->getHtmlData($response);
313 2
            $items = $this->getOrderItems($html);
314
315 2
            return $dataParser->parseFullOrderResponseHtml($orderId, $html, $items);
316 1
        } catch (DataException $e) {
317
            // Of note: Poshmark throws a Server 500 on an invalid order id
318 1
            throw new OrderNotFoundException(
319 1
                "Order {$orderId} was not found.",
320 1
                $e->getCode(),
321
                $e
322
            );
323
        }
324
    }
325
326
    /**
327
     * Get a list of order summaries. These won't have full details populated. Sorted by newest first.
328
     *
329
     * @param int $limit Max number of orders to get. Maximum allowed: 10000
330
     *
331
     * @throws DataException If we couldn't get any order summaries (e.g. not logged in)
332
     *
333
     * @return Order[]
334
     */
335 3
    public function getOrderSummaries(int $limit = 100): array
336
    {
337 3
        if ($limit < 1 || $limit > 10000) {
338 2
            throw new \InvalidArgumentException('Limit must be between 1 and 10,000 orders');
339
        }
340 1
        $orders = [];
341 1
        $numOrders = 0;
342 1
        $maxId = '';
343 1
        $iterations = 0;
344 1
        while ($iterations++ < 100) {  // Safe guard to limit infinite loops
345 1
            [$loopOrders, $maxId] = $this->getOrdersLoop($maxId);
346 1
            if ($loopOrders && is_array($loopOrders)) {
347 1
                $orders[] = $loopOrders;
348 1
                $numOrders += count($loopOrders);
349
            }
350 1
            if (!$loopOrders || $maxId < 0 || $numOrders >= $limit) {
351 1
                break;
352
            }
353
        }
354
355 1
        if ($orders !== []) {
356 1
            $orders = array_merge(...$orders);
357
        }
358
359 1
        if ($numOrders > $limit) {
360 1
            $orders = array_slice($orders, 0, $limit);
361
        }
362
363 1
        return $orders;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $orders returns an array which contains values of type array which are incompatible with the documented value type PHPosh\Provider\Poshmark\Order.
Loading history...
364
    }
365
366
    /**
367
     * Makes multiple web requests to get each individual item from an order page.
368
     */
369 2
    protected function getOrderItems(string $html): array
370
    {
371 2
        $crawler = new Crawler($html);
372 2
        $contentNode = $crawler->filter('.order-main-con');
373 2
        $itemNodes = $contentNode->filter('.listing-details .rw');
374
375
        $itemUrls = $itemNodes->each(static function (Crawler $node, $i) {
376 2
            return $node->filter('a')->first()->attr('href');
377 2
        });
378 2
        $items = [];
379 2
        foreach ($itemUrls as $url) {
380 2
            $id = DataParser::parseItemIdFromUrl($url);
381
382
            try {
383 2
                $items[] = $this->getItem($id);
384 1
            } catch (ItemNotFoundException $e) {
385 1
                $items[] = (new Item())
386 1
                    ->setId($id)
387 1
                    ->setTitle('Unknown')
388 1
                    ->setDescription('Unknown')
389 1
                    ->setImageUrl('')
390
                ;
391
            }
392
        }
393
394 2
        return $items;
395
    }
396
397
    /**
398
     * Get a page of closet items by using max_id. Not publicly accessible, use getItems() instead.
399
     *
400
     * @param string $usernameUuid Obfuscated user id
401
     * @param string $username     Human readable username
402
     * @param mixed  $max_id       Max ID param for pagination. If null, get first page.
403
     *
404
     * @throws DataException on an unexpected HTTP response or transfer failure
405
     *
406
     * @return array ['data' => [...], 'more' => [...]]
407
     */
408 4
    protected function getItemsByMaxId(string $usernameUuid, string $username, $max_id = null): array
409
    {
410 4
        $headers = static::DEFAULT_HEADERS;
411 4
        $headers['Referer'] = static::DEFAULT_REFERRER;
412 4
        $headers['Cookie'] = $this->getCookieHeader();
413
414 4
        $url = '/vm-rest/users/%s/posts?app_version=2.55&format=json&username=%s&nm=cl_all&summarize=true&_=%s';
415 4
        if ($max_id) {
416 2
            $url .= '&max_id=' . $max_id;
417
        }
418 4
        $url = sprintf(
419 4
            $url,
420 4
            rawurlencode($usernameUuid),
421 4
            rawurlencode($username),
422 4
            (string) microtime(true)
423
        );
424
425 4
        $response = $this->makeRequest('get', $url, [
426 4
            'headers' => $headers,
427
        ]);
428
429 3
        return $this->getJsonData($response) ?: [];
430
    }
431
432
    /**
433
     * Returns the cookie array.
434
     */
435 19
    protected function getCookies(): array
436
    {
437 19
        return $this->cookies;
438
    }
439
440
    /**
441
     * Convert back the internal cookie map to a string for use in a Cookie: HTTP header.
442
     */
443 16
    protected function getCookieHeader(): string
444
    {
445
        // TODO: Memoize this
446 16
        $cookiesToSend = [];
447 16
        foreach ($this->getCookies() as $name => $value) {
448 16
            if (isset(static::COOKIE_WHITELIST[$name])) {
449 16
                $cookiesToSend[$name] = $value;
450
            }
451
        }
452
453 16
        return http_build_query($cookiesToSend, '', '; ', PHP_QUERY_RFC3986);
454
    }
455
456
    /**
457
     * Get a CSRF token (sometimes called XSRF token) for the user, necessary for updates.
458
     *
459
     * @param string $poshmarkItemId Item id
460
     *
461
     * @throws DataException
462
     */
463 2
    protected function getXsrfTokenForEditItem(string $poshmarkItemId): string
464
    {
465 2
        $headers = static::DEFAULT_HEADERS;
466 2
        $headers['Referer'] = static::DEFAULT_REFERRER;
467 2
        $headers['Cookie'] = $this->getCookieHeader();
468 2
        $headers['Accept'] = 'text/html';
469
470 2
        $url = '/edit-listing/%s?_=%s';
471 2
        $url = sprintf($url, rawurlencode($poshmarkItemId), (string) microtime(true));
472
473 2
        $response = $this->makeRequest('get', $url, [
474 2
            'headers' => $headers,
475
        ]);
476
477 2
        $html = $this->getHtmlData($response);
478
479 2
        $crawler = new Crawler($html);
480 2
        $node = $crawler->filter('#csrftoken')->eq(0);
481 2
        if (!$node) {
0 ignored issues
show
introduced by
$node is of type Symfony\Component\DomCrawler\Crawler, thus it always evaluated to true.
Loading history...
482
            throw new DataException('Failed to find a CSRF token on the page');
483
        }
484
485 2
        return (string) $node->attr('content');
486
    }
487
488
    /**
489
     * @param string $maxId Max ID for pagination
490
     *
491
     * @throws DataException
492
     *
493
     * @return array [Order[], $nextMaxId]
494
     */
495 1
    protected function getOrdersLoop(string $maxId = ''): array
496
    {
497 1
        $headers = static::DEFAULT_HEADERS;
498 1
        $headers['Referer'] = static::DEFAULT_REFERRER;
499 1
        $headers['Cookie'] = $this->getCookieHeader();
500 1
        $headers['Accept'] = 'application/json';
501 1
        $headers['X-Requested-With'] = 'XMLHttpRequest';
502
503 1
        $dataParser = $this->getDataParser();
504
505 1
        $url = '/order/sales?_=%s';
506 1
        $url = sprintf($url, (string) microtime(true));
507
508 1
        if ('' !== $maxId) {
509
            $url .= '&max_id=' . $maxId;
510
        }
511
512
        try {
513 1
            $response = $this->makeRequest('get', $url, [
514 1
                'headers' => $headers,
515
            ]);
516
517 1
            $json = $this->getJsonData($response);
518
        } catch (DataException $e) {
519
            if ('' === $maxId) {
520
                // If this was the very first request, let's just throw exception.
521
                // Otherwise we'll capture it, and at least return a partial order set.
522
                throw $e;
523
            }
524
525
            return [null, null];
526
        }
527
528 1
        $nextMaxId = $json['max_id'] ?? -1;
529 1
        if (!$nextMaxId) {
530
            $nextMaxId = -1;
531
        }
532
533 1
        $html = $json['html'];
534
535
        return [
536 1
            $html ? $dataParser->parseOrdersPagePartialResponse($html) : [],
537 1
            $nextMaxId,
538
        ];
539
    }
540
541 16
    protected function getDataParser(): DataParser
542
    {
543 16
        return new DataParser();
544
    }
545
546
    /**
547
     * Wrapper function for making any HTTP requests. Any Guzzle or HTTP Exception will be wrapped with DataException,
548
     * and re-thrown.
549
     *
550
     * @param string|UriInterface $url
551
     *
552
     * @throws DataException on an unexpected HTTP Response or failure
553
     */
554 16
    private function makeRequest(string $method, $url, array $guzzleOptions): ResponseInterface
555
    {
556 16
        $method = strtolower($method);
557
558
        try {
559
            /** @see Client::__call */
560 16
            $response = $this->guzzleClient->{$method}($url, $guzzleOptions);
561 8
        } catch (\Exception $e) {
562 8
            $code = 101;
563 8
            if ($e instanceof RequestException && $e->getResponse()) {
564 5
                $code = $e->getResponse()->getStatusCode();
565
            }
566
567 8
            throw new DataException(
568 8
                sprintf(
569 8
                    'Exception occurred whilst making %s request to %s: %s',
570 8
                    strtoupper($method),
571
                    $url,
572 8
                    $e->getMessage()
573
                ),
574
                $code,
575
                $e
576
            );
577
        }
578
579 11
        return $response;
580
    }
581
582
    /**
583
     * @throws DataException On invalid data
584
     */
585 10
    private function getJsonData(ResponseInterface $response): array
586
    {
587 10
        $content = trim($response->getBody()->getContents());
588
589
        try {
590 10
            $data = \Safe\json_decode($content, true);
591 1
        } catch (SafeExceptionInterface $e) {
592 1
            $data = null;
593
        }
594 10
        if (null === $data || !\is_array($data)) {
595 1
            throw new DataException(
596 1
                'Poshmark: Unexpected json body, Resp. code: ' . $response->getStatusCode(),
597 1
                500
598
            );
599
        }
600
601 9
        if (isset($data['error']['statusCode']) && $data['error']['statusCode'] >= 400) {
602
            // Poshmark will return an error response as an actual HTTP 200.
603
            // The http code will be in the `error` array of the data response body,
604
            // so we convert this to a more HTTP-like exception.
605 1
            throw new DataException(
606 1
                sprintf(
607 1
                    'Poshmark: Received %s error (%s) Response code: %s',
608 1
                    $data['error']['errorType'] ?? 'unknownType',
609 1
                    $data['error']['errorMessage'] ?? 'emptyMsg',
610 1
                    (string) $data['error']['statusCode']
611
                ),
612 1
                $data['error']['statusCode']
613
            );
614
        }
615
616 8
        return $data;
617
    }
618
619
    /**
620
     * @param string $cookieCode Raw cookie string such as "a=1; b=foo; _c=hello world;"
621
     *
622
     * @return array Map of cookie name => cookie value (already decoded)
623
     */
624 24
    private function parseCookiesFromString(string $cookieCode): array
625
    {
626 24
        $cookieCode = trim($cookieCode);
627 24
        if (Str::beginsWith($cookieCode, '"')) {
628
            // remove double quotes
629 1
            $cookieCode = trim($cookieCode, '"');
630 23
        } elseif (Str::beginsWith($cookieCode, "'")) {
631 1
            $cookieCode = trim($cookieCode, "'");
632
        }
633 24
        $cookies = [];
634 24
        parse_str(
635 24
            strtr($cookieCode, ['&' => '%26', '+' => '%2B', ';' => '&']),
636
            $cookies
637
        );
638
639 24
        return $cookies;
640
    }
641
642
    /**
643
     * Sets interval variables (user, email, etc.) from the cookies array.
644
     *
645
     * @param array $cookies Cookie name=>value hashmap
646
     *
647
     * @throws CookieException When a required cookie is not provided
648
     */
649 24
    private function setupUserFromCookies(array $cookies): void
650
    {
651 24
        foreach (static::COOKIE_WHITELIST as $cookieKey => $bool) {
652 24
            if (!isset($cookies[$cookieKey]) || '' === $cookies[$cookieKey]) {
653
                throw new CookieException(
654
                    sprintf('Required cookie %s was not supplied', $cookieKey)
655
                );
656
            }
657
        }
658
659 24
        $ui = $cookies['ui'];
660
661 24
        $userData = json_decode($ui, true);
662 24
        $this->username = $userData['dh'];
663 24
        $this->email = $userData['em'];
664 24
        $this->pmUserId = $userData['uid'];
665 24
        $this->fullname = urldecode($userData['fn']);
666 24
        $this->cookieTimestamp = time();
667 24
    }
668
669
    /**
670
     * Get HTML body, and do some basic error checking.
671
     *
672
     * @throws DataException
673
     */
674 4
    private function getHtmlData(ResponseInterface $response): string
675
    {
676 4
        $content = trim($response->getBody()->getContents());
677 4
        if ('' === $content) {
678
            throw new DataException(
679
                'Poshmark: Unexpected HTML body',
680
                500
681
            );
682
        }
683
684 4
        return $content;
685
    }
686
}
687