Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
2.83% covered (danger)
2.83%
3 / 106
10.00% covered (danger)
10.00%
1 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
UserCardService
2.83% covered (danger)
2.83%
3 / 106
10.00% covered (danger)
10.00%
1 / 10
1739.41
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInsecureRows
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllRowsWithUsernames
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getLibraryCards
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 getOrCreateLibraryCard
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 deleteLibraryCard
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
 persistLibraryCardData
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
210
 synchronizeUserLibraryCardData
11.11% covered (danger)
11.11%
2 / 18
0.00% covered (danger)
0.00%
0 / 1
22.56
 activateLibraryCard
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 createEntity
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * Database service for UserCard.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2023.
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  Database
25 * @author   Sudharma Kellampalli <skellamp@villanova.edu>
26 * @author   Demian Katz <demian.katz@villanova.edu>
27 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
28 * @link     https://vufind.org/wiki/development:plugins:database_gateways Wiki
29 */
30
31namespace VuFind\Db\Service;
32
33use DateTime;
34use VuFind\Auth\ILSAuthenticator;
35use VuFind\Config\AccountCapabilities;
36use VuFind\Db\Entity\UserCardEntityInterface;
37use VuFind\Db\Entity\UserEntityInterface;
38use VuFind\Db\Table\DbTableAwareInterface;
39use VuFind\Db\Table\DbTableAwareTrait;
40
41use function count;
42use function is_int;
43
44/**
45 * Database service for UserCard.
46 *
47 * @category VuFind
48 * @package  Database
49 * @author   Demian Katz <demian.katz@villanova.edu>
50 * @author   Demian Katz <demian.katz@villanova.edu>
51 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
52 * @link     https://vufind.org/wiki/development:plugins:database_gateways Wiki
53 */
54class UserCardService extends AbstractDbService implements
55    DbServiceAwareInterface,
56    DbTableAwareInterface,
57    UserCardServiceInterface
58{
59    use DbServiceAwareTrait;
60    use DbTableAwareTrait;
61
62    /**
63     * Constructor
64     *
65     * @param ILSAuthenticator    $ilsAuthenticator ILS authenticator
66     * @param AccountCapabilities $capabilities     Account capabilities configuration
67     */
68    public function __construct(
69        protected ILSAuthenticator $ilsAuthenticator,
70        protected AccountCapabilities $capabilities
71    ) {
72    }
73
74    /**
75     * Get user_card rows with insecure catalog passwords.
76     *
77     * @return UserCardEntityInterface[]
78     */
79    public function getInsecureRows(): array
80    {
81        return iterator_to_array($this->getDbTable('UserCard')->getInsecureRows());
82    }
83
84    /**
85     * Get user_card rows with catalog usernames set.
86     *
87     * @return UserCardEntityInterface[]
88     */
89    public function getAllRowsWithUsernames(): array
90    {
91        $callback = function ($select) {
92            $select->where->isNotNull('cat_username');
93        };
94        return iterator_to_array($this->getDbTable('UserCard')->select($callback));
95    }
96
97    /**
98     * Get all library cards associated with the user.
99     *
100     * @param UserEntityInterface|int $userOrId    User object or identifier
101     * @param ?int                    $id          Optional card ID filter
102     * @param ?string                 $catUsername Optional catalog username filter
103     *
104     * @return UserCardEntityInterface[]
105     */
106    public function getLibraryCards(
107        UserEntityInterface|int $userOrId,
108        ?int $id = null,
109        ?string $catUsername = null
110    ): array {
111        if (!$this->capabilities->libraryCardsEnabled()) {
112            return [];
113        }
114        $userCard = $this->getDbTable('UserCard');
115        $criteria = [
116            'user_id' => is_int($userOrId) ? $userOrId : $userOrId->getId(),
117        ];
118        if ($id) {
119            $criteria['id'] = $id;
120        }
121        if ($catUsername) {
122            $criteria['cat_username'] = $catUsername;
123        }
124        return iterator_to_array($userCard->select($criteria));
125    }
126
127    /**
128     * Get or create library card data.
129     *
130     * @param UserEntityInterface|int $userOrId User object or identifier
131     * @param ?int                    $id       Card ID to fetch (or null to create a new card)
132     *
133     * @return UserCardEntityInterface Card data if found; throws exception otherwise
134     * @throws \VuFind\Exception\LibraryCard
135     */
136    public function getOrCreateLibraryCard(UserEntityInterface|int $userOrId, ?int $id = null): UserCardEntityInterface
137    {
138        if (!$this->capabilities->libraryCardsEnabled()) {
139            throw new \VuFind\Exception\LibraryCard('Library Cards Disabled');
140        }
141
142        if ($id === null) {
143            $user = is_int($userOrId)
144                ? $this->getDbService(UserServiceInterface::class)->getUserById($userOrId) : $userOrId;
145            $row = $this->createEntity()
146                ->setCardName('')
147                ->setUser($user)
148                ->setCatUsername('')
149                ->setRawCatPassword('');
150        } else {
151            $row = current($this->getLibraryCards($userOrId, $id));
152            if ($row === false) {
153                throw new \VuFind\Exception\LibraryCard('Library Card Not Found');
154            }
155        }
156        return $row;
157    }
158
159    /**
160     * Delete library card.
161     *
162     * @param UserEntityInterface         $user     User owning card to delete
163     * @param UserCardEntityInterface|int $userCard UserCard id or object to be deleted
164     *
165     * @return bool
166     * @throws \Exception
167     */
168    public function deleteLibraryCard(UserEntityInterface $user, UserCardEntityInterface|int $userCard): bool
169    {
170        if (!$this->capabilities->libraryCardsEnabled()) {
171            throw new \VuFind\Exception\LibraryCard('Library Cards Disabled');
172        }
173        $cardId = is_int($userCard) ? $userCard : $userCard->getId();
174        $row = current($this->getLibraryCards($user, $cardId));
175        if (!$row) {
176            throw new \Exception('Library card not found');
177        }
178        if (!$row instanceof \VuFind\Db\Row\UserCard) {
179            $row = $this->getDbTable('UserCard')->select(['id' => $cardId])->current();
180        }
181        $row->delete();
182
183        if ($row->getCatUsername() == $user->getCatUsername()) {
184            // Activate another card (if any) or remove cat_username and cat_password
185            $cards = $this->getLibraryCards($user);
186            if (count($cards) > 0) {
187                $this->activateLibraryCard($user, current($cards)->getId());
188            } else {
189                $user->setCatUsername(null);
190                $user->setRawCatPassword(null);
191                $user->setCatPassEnc(null);
192                $this->persistEntity($user);
193            }
194        }
195
196        return true;
197    }
198
199    /**
200     * Persist the provided library card data, either by updating a specified card
201     * or by creating a new one (when $card is null). Also updates the primary user
202     * row when appropriate. Will throw an exception if a duplicate $username value
203     * is provided; there should only be one card row per username.
204     *
205     * Returns the row that was added or updated.
206     *
207     * @param UserEntityInterface|int          $userOrId User object or identifier
208     * @param UserCardEntityInterface|int|null $cardOrId Card entity or ID (null = create new)
209     * @param string                           $cardName Card name
210     * @param string                           $username Username
211     * @param string                           $password Password
212     * @param string                           $homeLib  Home Library
213     *
214     * @return UserCardEntityInterface
215     * @throws \VuFind\Exception\LibraryCard
216     */
217    public function persistLibraryCardData(
218        UserEntityInterface|int $userOrId,
219        UserCardEntityInterface|int|null $cardOrId,
220        string $cardName,
221        string $username,
222        string $password,
223        string $homeLib = ''
224    ): UserCardEntityInterface {
225        if (!$this->capabilities->libraryCardsEnabled()) {
226            throw new \VuFind\Exception\LibraryCard('Library Cards Disabled');
227        }
228        // Extract a card ID, if available:
229        $id = $cardOrId instanceof UserCardEntityInterface ? $cardOrId->getId() : $cardOrId;
230        // Check that the username is not already in use in another card
231        $usernameCheck = current($this->getLibraryCards($userOrId, catUsername: $username));
232        if (!empty($usernameCheck) && ($id === null || $usernameCheck->getId() != $id)) {
233            throw new \VuFind\Exception\LibraryCard(
234                'Username is already in use in another library card'
235            );
236        }
237
238        $user = is_int($userOrId)
239            ? $this->getDbService(UserServiceInterface::class)->getUserById($userOrId) : $userOrId;
240
241        $row = ($id !== null) ? current($this->getLibraryCards($user, $id)) : null;
242        if (empty($row)) {
243            $row = $this->createEntity()
244                ->setUser($user)
245                ->setCreated(new DateTime());
246        }
247        $row->setCardName($cardName);
248        $row->setCatUsername($username);
249        if (!empty($homeLib)) {
250            $row->setHomeLibrary($homeLib);
251        }
252        if ($this->ilsAuthenticator->passwordEncryptionEnabled()) {
253            $row->setRawCatPassword(null);
254            $row->setCatPassEnc($this->ilsAuthenticator->encrypt($password));
255        } else {
256            $row->setRawCatPassword($password);
257            $row->setCatPassEnc(null);
258        }
259
260        $this->persistEntity($row);
261
262        // If this is the first or active library card, or no credentials are
263        // currently set, activate the card now
264        if (
265            count($this->getLibraryCards($user)) == 1 || !$user->getCatUsername()
266            || $user->getCatUsername() === $row->getCatUsername()
267        ) {
268            $this->activateLibraryCard($user, $row->getId());
269        }
270
271        return $row;
272    }
273
274    /**
275     * Verify that the user's current ILS settings exist in their library card data
276     * (if enabled) and are up to date. Designed to be called after updating the
277     * user row; will create or modify library card rows as needed.
278     *
279     * @param UserEntityInterface|int $userOrId User object or identifier
280     *
281     * @return bool
282     * @throws \VuFind\Exception\PasswordSecurity
283     */
284    public function synchronizeUserLibraryCardData(UserEntityInterface|int $userOrId): bool
285    {
286        if (!$this->capabilities->libraryCardsEnabled()) {
287            return true; // success, because there's nothing to do
288        }
289        $user = is_int($userOrId)
290            ? $this->getDbService(UserServiceInterface::class)->getUserById($userOrId) : $userOrId;
291        if (!$user->getCatUsername()) {
292            return true; // success, because there's nothing to do
293        }
294        $row = current($this->getLibraryCards($user, catUsername: $user->getCatUsername()));
295        if (empty($row)) {
296            $row = $this->createEntity()
297                ->setUser($user)
298                ->setCatUsername($user->getCatUsername())
299                ->setCardName($user->getCatUsername())
300                ->setCreated(new DateTime());
301        }
302        // Always update home library and password
303        $row->setHomeLibrary($user->getHomeLibrary());
304        $row->setRawCatPassword($user->getRawCatPassword());
305        $row->setCatPassEnc($user->getCatPassEnc());
306
307        $this->persistEntity($row);
308
309        return true;
310    }
311
312    /**
313     * Activate a library card for the given username.
314     *
315     * @param UserEntityInterface|int $userOrId User owning card
316     * @param int                     $id       Library card ID to activate
317     *
318     * @return void
319     * @throws \VuFind\Exception\LibraryCard
320     */
321    public function activateLibraryCard(UserEntityInterface|int $userOrId, int $id): void
322    {
323        if (!$this->capabilities->libraryCardsEnabled()) {
324            throw new \VuFind\Exception\LibraryCard('Library Cards Disabled');
325        }
326        $row = $this->getOrCreateLibraryCard($userOrId, $id);
327        $user = is_int($userOrId)
328            ? $this->getDbService(UserServiceInterface::class)->getUserById($userOrId) : $userOrId;
329        $user->setCatUsername($row->getCatUsername());
330        $user->setRawCatPassword($row->getRawCatPassword());
331        $user->setCatPassEnc($row->getCatPassEnc());
332        $user->setHomeLibrary($row->getHomeLibrary());
333        $this->persistEntity($user);
334    }
335
336    /**
337     * Create a UserCard entity object.
338     *
339     * @return UserCardEntityInterface
340     */
341    public function createEntity(): UserCardEntityInterface
342    {
343        return $this->getDbTable('UserCard')->createRow();
344    }
345}