Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.24% covered (success)
95.24%
60 / 63
83.33% covered (warning)
83.33%
15 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
Cart
95.24% covered (success)
95.24%
60 / 63
83.33% covered (warning)
83.33%
15 / 18
29
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getItems
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 contains
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 emptyCart
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addItem
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addItems
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 removeItems
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getMaxSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isFull
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isActive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isActiveInSearch
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 init
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 save
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 getCookieDomain
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCookiePath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCookieSameSite
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRecordDetails
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Cart Class
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  Cart
25 * @author   Tuan Nguyen <tuan@yorku.ca>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org/wiki/development Wiki
28 */
29
30namespace VuFind;
31
32use VuFind\Cookie\CookieManager;
33
34use function array_slice;
35use function chr;
36use function count;
37use function in_array;
38use function ord;
39
40/**
41 * Cart Class
42 *
43 * The data model object representing a user's book cart.
44 *
45 * @category VuFind
46 * @package  Cart
47 * @author   Tuan Nguyen <tuan@yorku.ca>
48 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
49 * @link     https://vufind.org/wiki/development Wiki
50 */
51class Cart
52{
53    /**
54     * Cart contents.
55     *
56     * @var array
57     */
58    protected $items;
59
60    /**
61     * Maximum number of items allowed in cart.
62     *
63     * @var int
64     */
65    protected $maxSize;
66
67    /**
68     * Is the cart currently activated?
69     *
70     * @var bool
71     */
72    protected $active;
73
74    /**
75     * Is cart configured to toggles in search results?
76     *
77     * @var bool
78     */
79    protected $showTogglesInSearch;
80
81    /**
82     * Record loader
83     *
84     * @var \VuFind\Record\Loader
85     */
86    protected $recordLoader;
87
88    /**
89     * Cookie manager
90     *
91     * @var CookieManager
92     */
93    protected $cookieManager;
94
95    public const CART_COOKIE = 'vufind_cart';
96    public const CART_COOKIE_SOURCES = 'vufind_cart_src';
97    public const CART_COOKIE_DELIM = "\t";
98
99    /**
100     * Constructor
101     *
102     * @param \VuFind\Record\Loader $loader          Object for loading records
103     * @param CookieManager         $cookieManager   Cookie manager
104     * @param int                   $maxSize         Maximum size of cart contents
105     * @param bool                  $active          Is cart enabled?
106     * @param bool                  $togglesInSearch Is cart configured to toggles
107     * in search results?
108     */
109    public function __construct(
110        \VuFind\Record\Loader $loader,
111        \VuFind\Cookie\CookieManager $cookieManager,
112        $maxSize = 100,
113        $active = true,
114        $togglesInSearch = true
115    ) {
116        $this->recordLoader = $loader;
117        $this->cookieManager = $cookieManager;
118        $this->maxSize = $maxSize;
119        $this->active = $active;
120        $this->showTogglesInSearch = $togglesInSearch;
121
122        // Initialize contents
123        $this->init($this->cookieManager->getCookies());
124    }
125
126    /**
127     * Return the contents of the cart.
128     *
129     * @return array     array of items in the cart
130     */
131    public function getItems()
132    {
133        return $this->items;
134    }
135
136    /**
137     * Does the cart contain the specified item?
138     *
139     * @param string $item ID of item to check
140     *
141     * @return bool
142     */
143    public function contains($item)
144    {
145        return in_array($item, $this->items);
146    }
147
148    /**
149     * Empty the cart.
150     *
151     * @return void
152     */
153    public function emptyCart()
154    {
155        $this->items = [];
156        $this->save();
157    }
158
159    /**
160     * Add an item to the cart.
161     *
162     * @param string $item ID of item to remove
163     *
164     * @return array       Associative array with two keys: success (bool) and
165     * notAdded (array of IDs that were unable to be added to the cart)
166     */
167    public function addItem($item)
168    {
169        return $this->addItems([$item]);
170    }
171
172    /**
173     * Add an array of items to the cart.
174     *
175     * @param array $items IDs of items to add
176     *
177     * @return array       Associative array with two keys: success (bool) and
178     * notAdded (array of IDs that were unable to be added to the cart)
179     */
180    public function addItems($items)
181    {
182        $items = array_merge($this->items, $items);
183
184        $total = count($items);
185        $this->items = array_slice(array_unique($items), 0, $this->maxSize);
186        $this->save();
187        if ($total > $this->maxSize) {
188            $notAdded = $total - $this->maxSize;
189            return ['success' => false, 'notAdded' => $notAdded];
190        }
191        return ['success' => true];
192    }
193
194    /**
195     * Remove an item from the cart.
196     *
197     * @param array $items An array of item IDS
198     *
199     * @return void
200     */
201    public function removeItems($items)
202    {
203        $results = [];
204        foreach ($this->items as $id) {
205            if (!in_array($id, $items)) {
206                $results[] = $id;
207            }
208        }
209        $this->items = $results;
210        $this->save();
211    }
212
213    /**
214     * Get cart size.
215     *
216     * @return int The maximum cart size
217     */
218    public function getMaxSize()
219    {
220        return $this->maxSize;
221    }
222
223    /**
224     * Check whether the cart is full.
225     *
226     * @return bool      true if full, false otherwise
227     */
228    public function isFull()
229    {
230        return count($this->items) >= $this->maxSize;
231    }
232
233    /**
234     * Check whether the cart is empty.
235     *
236     * @return bool      true if empty, false otherwise
237     */
238    public function isEmpty()
239    {
240        return empty($this->items);
241    }
242
243    /**
244     * Check whether cart is enabled.
245     *
246     * @return bool
247     */
248    public function isActive()
249    {
250        return $this->active;
251    }
252
253    /**
254     * Process parameters and return the cart content.
255     *
256     * @return bool
257     */
258    public function isActiveInSearch()
259    {
260        return $this->active && $this->showTogglesInSearch;
261    }
262
263    /**
264     * Initialize the cart model.
265     *
266     * @param array $cookies Current cookie values
267     *
268     * @return void
269     */
270    protected function init($cookies)
271    {
272        $items = null;
273        if (isset($cookies[self::CART_COOKIE])) {
274            $cookie = $cookies[self::CART_COOKIE];
275            $items = explode(self::CART_COOKIE_DELIM, $cookie);
276
277            if (!isset($cookies[self::CART_COOKIE_SOURCES])) {
278                // Backward compatibility with VuFind 1.x -- if no source cookie, all
279                // items come from the default source:
280                for ($i = 0; $i < count($items); $i++) {
281                    $items[$i] = DEFAULT_SEARCH_BACKEND . '|' . $items[$i];
282                }
283            } else {
284                // Default case for VuFind 2.x carts -- decompress source data:
285                $sources = explode(
286                    self::CART_COOKIE_DELIM,
287                    $cookies[self::CART_COOKIE_SOURCES]
288                );
289                for ($i = 0; $i < count($items); $i++) {
290                    $sourceIndex = ord(substr($items[$i], 0, 1)) - 65;
291                    $items[$i]
292                        = $sources[$sourceIndex] . '|' . substr($items[$i], 1);
293                }
294            }
295        }
296        $this->items = $items ? $items : [];
297    }
298
299    /**
300     * Save the state of the cart. This implementation uses cookie
301     * so the cart contents can be manipulated on the client side as well.
302     *
303     * @return void
304     */
305    protected function save()
306    {
307        $sources = [];
308        $ids = [];
309
310        foreach ($this->items as $item) {
311            // Break apart the source and the ID:
312            [$source, $id] = explode('|', $item, 2);
313
314            // Add the source to the source array if it is not already there:
315            $sourceIndex = array_search($source, $sources);
316            if ($sourceIndex === false) {
317                $sourceIndex = count($sources);
318                $sources[$sourceIndex] = $source;
319            }
320
321            // Encode the source into the ID as a single character:
322            $ids[] = chr(65 + $sourceIndex) . $id;
323        }
324
325        // Save the cookies:
326        $cookie = implode(self::CART_COOKIE_DELIM, $ids);
327        $this->cookieManager->set(self::CART_COOKIE, $cookie, 0, false);
328        $srcCookie = implode(self::CART_COOKIE_DELIM, $sources);
329        $this->cookieManager->set(self::CART_COOKIE_SOURCES, $srcCookie, 0, false);
330    }
331
332    /**
333     * Get cookie domain context (null if unset).
334     *
335     * @return string
336     */
337    public function getCookieDomain()
338    {
339        return $this->cookieManager->getDomain();
340    }
341
342    /**
343     * Get cookie path ('/' if unset).
344     *
345     * @return string
346     */
347    public function getCookiePath()
348    {
349        return $this->cookieManager->getPath();
350    }
351
352    /**
353     * Get cookie SameSite attribute.
354     *
355     * @return string
356     */
357    public function getCookieSameSite()
358    {
359        return $this->cookieManager->getSameSite();
360    }
361
362    /**
363     * Process parameters and return the cart content.
364     *
365     * @return array $records The cart content
366     */
367    public function getRecordDetails()
368    {
369        return $this->recordLoader->loadBatch($this->items);
370    }
371}