Failed Conditions
Pull Request — master (#5)
by Michael
02:22
created

PoshmarkService::getJsonData()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 28.2838

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 22
c 1
b 0
f 0
dl 0
loc 39
ccs 7
cts 22
cp 0.3182
rs 8.4444
cc 8
nc 7
nop 1
crap 28.2838
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 12
    public function __construct($cookieCode, array $config = [])
102
    {
103 12
        $config = array_merge($config, static::DEFAULT_OPTIONS);
104 12
        $this->setGuzzleClient(new Client($config));
105 12
        $this->cookies = $this->parseCookiesFromString($cookieCode);
106 12
        $this->setupUserFromCookies($this->cookies);
107 12
    }
108
109
    /**
110
     * @return $this
111
     */
112 12
    public function setGuzzleClient(Client $client): self
113
    {
114 12
        $this->guzzleClient = $client;
115
116 12
        return $this;
117
    }
118
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 2
    public function getItems(string $usernameUuid = '', string $username = ''): array
132
    {
133 2
        if (!$usernameUuid) {
134 2
            $usernameUuid = $this->pmUserId;
135
        }
136 2
        if (!$username) {
137 2
            $username = $this->username;
138
        }
139 2
        $dataParser = $this->getDataParser();
140
141
        // Set a sane upper bound; 250 * 20 = 5000 max items to get
142 2
        $iterations = 250;
143 2
        $maxId = null;
144 2
        $items = [];
145 2
        while ($iterations > 0) {
146
            try {
147 2
                $loopItems = $this->getItemsByMaxId($usernameUuid, $username, $maxId);
148
            } catch (DataException $e) {
149
                if ($items === []) {
150
                    // If we got this exception on the very first try, we should re-throw it
151
                    throw $e;
152
                }
153
                $loopItems = [];
154
            }
155 2
            if (!$loopItems || empty($loopItems['data'])) {
156 1
                break;
157
            }
158 1
            foreach ($loopItems['data'] as $item) {
159
                // Convert each raw json to an Item object
160 1
                $items[] = $dataParser->parseOneItemResponseJson($item);
161
            }
162 1
            $maxId = ($loopItems['more']['next_max_id'] ?? null);
163 1
            if ($maxId <= 0) {
164
                // No next id signifies finished listing
165 1
                break;
166
            }
167 1
            $maxId = (string) $maxId;
168 1
            --$iterations;
169
170
            // Sleep 100ms
171 1
            usleep(100000);
172
173 1
            if ($iterations % 10) {
174
                // Every 10th iteration sleep an additional amount
175 1
                usleep(200000);
176
            }
177
        }
178
179
        usort($items, static function ($a, $b) {
180
            // sort array items by their item ids
181 1
            return strcmp($a->getId(), $b->getId());
182 2
        });
183
184 2
        return $items;
185
    }
186
187
    /**
188
     * Get a single item on Poshmark by its identifier, full details.
189
     * You either get the Item or an exception is thrown.
190
     *
191
     * @param string $poshmarkItemId Poshmark Item Id
192
     *
193
     * @throws DataException             If a problem occurred while trying to get the Item
194
     * @throws ItemNotFoundException     If the item was not found (e.g. 404)
195
     * @throws \InvalidArgumentException If you passed in an invalid id (request isn't even attempted)
196
     */
197 7
    public function getItem(string $poshmarkItemId): Item
198
    {
199 7
        if (!$poshmarkItemId) {
200 1
            throw new \InvalidArgumentException('$poshmarkItemId must be non-empty');
201
        }
202 6
        $parser = $this->getDataParser();
203 6
        $headers = static::DEFAULT_HEADERS;
204 6
        $headers['Referer'] = static::DEFAULT_REFERRER;
205 6
        $headers['Cookie'] = $this->getCookieHeader();
206
207 6
        $url = '/vm-rest/posts/%s?app_version=2.55&_=%s';
208 6
        $url = sprintf($url, rawurlencode($poshmarkItemId), (string) microtime(true));
209
210
        try {
211 6
            $response = $this->makeRequest('get', $url, [
212 6
                'headers' => $headers,
213
            ]);
214 4
            $data = $this->getJsonData($response);
215 2
        } catch (DataException $e) {
216 2
            if (404 === (int) $e->getCode()) {
217 2
                throw new ItemNotFoundException("Item {$poshmarkItemId} not found");
218
            }
219
220
            throw $e;
221
        }
222
223 4
        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
     *                               Only these fields currently supported:
235
     *                               [
236
     *                               'title' => 'New title',
237
     *                               'description' => 'New description',
238
     *                               'price' => '4.95 USD', // Price, with currency code (will default to USD)
239
     *                               'brand' => 'Nike', // brand name
240
     *                               ]
241
     *
242
     * @throws DataException         update failed
243
     * @throws ItemNotFoundException when the item you're trying to update wasn't found
244
     *
245
     * @return bool returns true on success, throws exception on failure
246
     */
247 4
    public function updateItemRequest(string $poshmarkItemId, array $itemFields): bool
248
    {
249 4
        if (!$poshmarkItemId) {
250 1
            throw new \InvalidArgumentException('$poshmarkItemId must be non-empty');
251
        }
252 3
        $itemObj = $this->getItem($poshmarkItemId);
253
254 2
        $newItemData = Helper::createItemDataForUpdate($itemFields, $itemObj->getRawData());
255
256
        $postBody = [
257 2
            'post' => $newItemData,
258
        ];
259 2
        $postBody = json_encode($postBody);
260
261 2
        $headers = static::DEFAULT_HEADERS;
262 2
        $headers['Referer'] = self::BASE_URL . '/edit-listing/' . $poshmarkItemId;
263 2
        $headers['Cookie'] = $this->getCookieHeader();
264 2
        $headers['Content-Type'] = 'application/json';
265
266 2
        $headers['X-XSRF-TOKEN'] = $this->getXsrfTokenForEditItem($poshmarkItemId);
267 2
        usleep(200000);
268
269 2
        $url = '/vm-rest/posts/%s';
270 2
        $url = sprintf($url, rawurlencode($poshmarkItemId));
271
272 2
        $response = $this->makeRequest('post', $url, [
273 2
            'body' => $postBody,
274 2
            'headers' => $headers,
275
        ]);
276
277
        // Check response code
278 1
        $this->getHtmlData($response);
279
280 1
        return true;
281
    }
282
283
    /**
284
     * Get full details on an order, by parsing the item details page.
285
     *
286
     * @param string $orderId Poshmark OrderID
287
     *
288
     * @throws DataException
289
     * @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
    {
293 1
        if ('' === $orderId) {
294
            throw new \InvalidArgumentException("Invalid \$orderId: {$orderId}");
295
        }
296 1
        $headers = static::DEFAULT_HEADERS;
297 1
        $headers['Referer'] = static::DEFAULT_REFERRER;
298 1
        $headers['Cookie'] = $this->getCookieHeader();
299 1
        $headers['Accept'] = 'text/html';
300
301 1
        $dataParser = $this->getDataParser();
302
303 1
        $url = '/order/sales/%s?_=%s';
304 1
        $url = sprintf($url, $orderId, (string) microtime(true));
305
306
        try {
307 1
            $response = $this->makeRequest('get', $url, [
308 1
                'headers' => $headers,
309
            ]);
310
311 1
            $html = $this->getHtmlData($response);
312 1
            $items = $this->getOrderItems($html);
313
314 1
            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
                "Order {$orderId} was not found.",
319
                $e->getCode(),
320
                $e
321
            );
322
        }
323
    }
324
325
    /**
326
     * Get a list of order summaries. These won't have full details populated. Sorted by newest first.
327
     *
328
     * @param int $limit Max number of orders to get. Maximum allowed: 10000
329
     *
330
     * @throws DataException If we couldn't get any order summaries (e.g. not logged in)
331
     *
332
     * @return Order[]
333
     */
334 1
    public function getOrderSummaries(int $limit = 100): array
335
    {
336 1
        if ($limit < 0 || $limit > 10000) {
337
            throw new \InvalidArgumentException('Limit must be between 1 and 10,000 orders');
338
        }
339 1
        $orders = [];
340 1
        $numOrders = 0;
341 1
        $maxId = '';
342 1
        $iterations = 0;
343 1
        while ($iterations++ < 100) {  // Safe guard to limit infinite loops
344 1
            [$loopOrders, $maxId] = $this->getOrdersLoop($maxId);
345 1
            if ($loopOrders && is_array($loopOrders)) {
346 1
                $orders[] = $loopOrders;
347 1
                $numOrders += count($loopOrders);
348
            }
349 1
            if (!$loopOrders || $maxId < 0 || $numOrders >= $limit) {
350 1
                break;
351
            }
352
        }
353
354 1
        if ($orders !== []) {
355 1
            $orders = array_merge(...$orders);
356
        }
357
358 1
        if ($numOrders > $limit) {
359 1
            $orders = array_slice($orders, 0, $limit);
360
        }
361
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
    }
364
365
    /**
366
     * Makes multiple web requests to get each individual item from an order page.
367
     */
368 1
    protected function getOrderItems(string $html): array
369
    {
370 1
        $crawler = new Crawler($html);
371 1
        $contentNode = $crawler->filter('.order-main-con');
372 1
        $itemNodes = $contentNode->filter('.listing-details .rw');
373
374
        $itemUrls = $itemNodes->each(static function (Crawler $node, $i) {
375 1
            return $node->filter('a')->first()->attr('href');
376 1
        });
377 1
        $items = [];
378 1
        foreach ($itemUrls as $url) {
379 1
            $id = DataParser::parseItemIdFromUrl($url);
380
381
            try {
382 1
                $items[] = $this->getItem($id);
383
            } catch (ItemNotFoundException $e) {
384
                $items[] = (new Item())
385
                    ->setId($id)
386
                    ->setTitle('Unknown')
387
                    ->setDescription('Unknown')
388
                ;
389
            }
390
        }
391
392 1
        return $items;
393
    }
394
395
    /**
396
     * Get a page of closet items by using max_id. Not publicly accessible, use getItems() instead.
397
     *
398
     * @param string $usernameUuid Obfuscated user id
399
     * @param string $username     Human readable username
400
     * @param mixed  $max_id       Max ID param for pagination. If null, get first page.
401
     *
402
     * @throws DataException on an unexpected HTTP response or transfer failure
403
     *
404
     * @return array ['data' => [...], 'more' => [...]]
405
     */
406 2
    protected function getItemsByMaxId(string $usernameUuid, string $username, $max_id = null): array
407
    {
408 2
        $headers = static::DEFAULT_HEADERS;
409 2
        $headers['Referer'] = static::DEFAULT_REFERRER;
410 2
        $headers['Cookie'] = $this->getCookieHeader();
411
412 2
        $url = '/vm-rest/users/%s/posts?app_version=2.55&format=json&username=%s&nm=cl_all&summarize=true&_=%s';
413 2
        if ($max_id) {
414 1
            $url .= '&max_id=' . $max_id;
415
        }
416 2
        $url = sprintf(
417 2
            $url,
418 2
            rawurlencode($usernameUuid),
419 2
            rawurlencode($username),
420 2
            (string) microtime(true)
421
        );
422
423 2
        $response = $this->makeRequest('get', $url, [
424 2
            'headers' => $headers,
425
        ]);
426
427 2
        return $this->getJsonData($response) ?: [];
428
    }
429
430
    /**
431
     * Returns the cookie array.
432
     */
433 10
    protected function getCookies(): array
434
    {
435 10
        return $this->cookies;
436
    }
437
438
    /**
439
     * Convert back the internal cookie map to a string for use in a Cookie: HTTP header.
440
     */
441 9
    protected function getCookieHeader(): string
442
    {
443
        // TODO: Memoize this
444 9
        $cookiesToSend = [];
445 9
        foreach ($this->getCookies() as $name => $value) {
446 9
            if (isset(static::COOKIE_WHITELIST[$name])) {
447 9
                $cookiesToSend[$name] = $value;
448
            }
449
        }
450
451 9
        return http_build_query($cookiesToSend, '', '; ', PHP_QUERY_RFC3986);
452
    }
453
454
    /**
455
     * Get a CSRF token (sometimes called XSRF token) for the user, necessary for updates.
456
     *
457
     * @param string $poshmarkItemId Item id
458
     *
459
     * @throws DataException
460
     */
461 2
    protected function getXsrfTokenForEditItem(string $poshmarkItemId): string
462
    {
463 2
        $headers = static::DEFAULT_HEADERS;
464 2
        $headers['Referer'] = static::DEFAULT_REFERRER;
465 2
        $headers['Cookie'] = $this->getCookieHeader();
466 2
        $headers['Accept'] = 'text/html';
467
468 2
        $url = '/edit-listing/%s?_=%s';
469 2
        $url = sprintf($url, rawurlencode($poshmarkItemId), (string) microtime(true));
470
471 2
        $response = $this->makeRequest('get', $url, [
472 2
            'headers' => $headers,
473
        ]);
474
475 2
        $html = $this->getHtmlData($response);
476
477 2
        $crawler = new Crawler($html);
478 2
        $node = $crawler->filter('#csrftoken')->eq(0);
479 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...
480
            throw new DataException('Failed to find a CSRF token on the page');
481
        }
482
483 2
        return (string) $node->attr('content');
484
    }
485
486
    /**
487
     * @param string $maxId Max ID for pagination
488
     *
489
     * @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 1
        $headers['Cookie'] = $this->getCookieHeader();
498 1
        $headers['Accept'] = 'application/json';
499 1
        $headers['X-Requested-With'] = 'XMLHttpRequest';
500
501 1
        $dataParser = $this->getDataParser();
502
503 1
        $url = '/order/sales?_=%s';
504 1
        $url = sprintf($url, (string) microtime(true));
505
506 1
        if ('' !== $maxId) {
507
            $url .= '&max_id=' . $maxId;
508
        }
509
510
        try {
511 1
            $response = $this->makeRequest('get', $url, [
512 1
                'headers' => $headers,
513
            ]);
514
515 1
            $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
                // Otherwise we'll capture it, and at least return a partial order set.
520
                throw $e;
521
            }
522
523
            return [null, null];
524
        }
525
526 1
        $nextMaxId = $json['max_id'] ?? -1;
527 1
        if (!$nextMaxId) {
528
            $nextMaxId = -1;
529
        }
530
531 1
        $html = $json['html'];
532
533
        return [
534 1
            $html ? $dataParser->parseOrdersPagePartialResponse($html) : [],
535 1
            $nextMaxId,
536
        ];
537
    }
538
539 9
    protected function getDataParser(): DataParser
540
    {
541 9
        return new DataParser();
542
    }
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
     * @param string|UriInterface $url
549
     *
550
     * @throws DataException on an unexpected HTTP Response or failure
551
     */
552 9
    private function makeRequest(string $method, $url, array $guzzleOptions): ResponseInterface
553
    {
554 9
        $method = strtolower($method);
555 9
        if (!in_array($method, ['get', 'post'], true)) {
556
            throw new \InvalidArgumentException('$method must be one of: get, post');
557
        }
558
559
        try {
560
            /** @see Client::__call */
561 9
            $response = $this->guzzleClient->{$method}($url, $guzzleOptions);
562 3
        } catch (\Exception $e) {
563 3
            $code = 101;
564 3
            if ($e instanceof RequestException && $e->getResponse()) {
565 3
                $code = $e->getResponse()->getStatusCode();
566
            }
567
568 3
            throw new DataException(
569 3
                sprintf(
570 3
                    'Exception occurred whilst making %s request to %s: %s',
571 3
                    strtoupper($method),
572
                    $url,
573 3
                    $e->getMessage()
574
                ),
575
                $code,
576
                $e
577
            );
578
        }
579
580 7
        return $response;
581
    }
582
583
    /**
584
     * @throws DataException On invalid data
585
     */
586 7
    private function getJsonData(ResponseInterface $response): array
587
    {
588 7
        $content = trim($response->getBody()->getContents());
589
590 7
        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 7
            $data = json_decode($content, true);
599
        } catch (\Exception $e) {
600
            $data = null;
601
        }
602 7
        if (!$data || !\is_array($data)) {
603
            throw new DataException(
604
                'Poshmark: Unexpected json body, Resp. code: ' . $response->getStatusCode(),
605
                500
606
            );
607
        }
608
609 7
        if (isset($data['error']['statusCode']) && $data['error']['statusCode'] >= 400) {
610
            // Poshmark will return an error response as an actual HTTP 200.
611
            // The http code will be in the `error` array of the data response body,
612
            // so we convert this to a more HTTP-like exception.
613
            throw new DataException(
614
                sprintf(
615
                    'Poshmark: Received %s error (%s) Response code: %s',
616
                    $data['error']['errorType'] ?? 'unknownType',
617
                    $data['error']['errorMessage'] ?? 'emptyMsg',
618
                    (string) $data['error']['statusCode']
619
                ),
620
                $data['error']['statusCode']
621
            );
622
        }
623
624 7
        return $data;
625
    }
626
627
    /**
628
     * @param string $cookieCode Raw cookie string such as "a=1; b=foo; _c=hello world;"
629
     *
630
     * @return array Map of cookie name => cookie value (already decoded)
631
     */
632 12
    private function parseCookiesFromString(string $cookieCode): array
633
    {
634 12
        $cookieCode = trim($cookieCode);
635 12
        if (Str::beginsWith($cookieCode, '"')) {
636
            // remove double quotes
637
            $cookieCode = trim($cookieCode, '"');
638 12
        } elseif (Str::beginsWith($cookieCode, "'")) {
639
            $cookieCode = trim($cookieCode, "'");
640
        }
641 12
        $cookies = [];
642 12
        parse_str(
643 12
            strtr($cookieCode, ['&' => '%26', '+' => '%2B', ';' => '&']),
644
            $cookies
645
        );
646
647 12
        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 12
    private function setupUserFromCookies(array $cookies): void
658
    {
659 12
        foreach (static::COOKIE_WHITELIST as $cookieKey => $bool) {
660 12
            if (!isset($cookies[$cookieKey]) || '' === $cookies[$cookieKey]) {
661
                throw new CookieException(
662
                    sprintf('Required cookie %s was not supplied', $cookieKey)
663
                );
664
            }
665
        }
666
667 12
        $ui = $cookies['ui'];
668
669 12
        $userData = json_decode($ui, true);
670 12
        $this->username = $userData['dh'];
671 12
        $this->email = $userData['em'];
672 12
        $this->pmUserId = $userData['uid'];
673 12
        $this->fullname = urldecode($userData['fn']);
674 12
        $this->cookieTimestamp = time();
675 12
    }
676
677
    /**
678
     * Get HTML body, and do some basic error checking.
679
     *
680
     * @throws DataException
681
     */
682 3
    private function getHtmlData(ResponseInterface $response): string
683
    {
684 3
        $content = trim($response->getBody()->getContents());
685 3
        if ('' === $content) {
686
            throw new DataException(
687
                'Poshmark: Unexpected HTML body',
688
                500
689
            );
690
        }
691
692 3
        return $content;
693
    }
694
}
695