Test Failed
Pull Request — master (#5)
by Michael
02:12
created

PoshmarkService::getJsonData()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 8.1515

Importance

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