Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
LoginToken
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 7
182
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 matchToken
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 deleteBySeries
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 deleteByUserId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getByUserId
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
6
 getBySeries
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 expirationCallback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * Table Definition for login_token
5 *
6 * PHP version 8
7 *
8 * Copyright (C) The National Library of Finland 2023-2024.
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  Db_Table
25 * @author   Jaro Ravila <jaro.ravila@helsinki.fi>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org Main Page
28 */
29
30namespace VuFind\Db\Table;
31
32use Laminas\Db\Adapter\Adapter;
33use Laminas\Db\ResultSet\ResultSetInterface;
34use Laminas\Db\Sql\Expression;
35use VuFind\Db\Row\LoginToken as LoginTokenRow;
36use VuFind\Db\Row\RowGateway;
37use VuFind\Exception\LoginToken as LoginTokenException;
38
39/**
40 * Table Definition for login_token
41 *
42 * @category VuFind
43 * @package  Db_Table
44 * @author   Jaro Ravila <jaro.ravila@helsinki.fi>
45 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
46 * @link     https://vufind.org Main Site
47 */
48class LoginToken extends Gateway
49{
50    use ExpirationTrait;
51
52    /**
53     * Constructor
54     *
55     * @param Adapter       $adapter Database adapter
56     * @param PluginManager $tm      Table manager
57     * @param array         $cfg     Laminas configuration
58     * @param RowGateway    $rowObj  Row prototype object (null for default)
59     * @param string        $table   Name of database table to interface with
60     */
61    public function __construct(
62        Adapter $adapter,
63        PluginManager $tm,
64        $cfg,
65        ?RowGateway $rowObj = null,
66        $table = 'login_token'
67    ) {
68        parent::__construct($adapter, $tm, $cfg, $rowObj, $table);
69    }
70
71    /**
72     * Check if a login token matches one in database.
73     *
74     * @param array $token array containing user id, token and series
75     *
76     * @return ?LoginTokenRow
77     * @throws LoginTokenException
78     */
79    public function matchToken(array $token): ?LoginTokenRow
80    {
81        $userId = null;
82        foreach ($this->getBySeries($token['series']) as $row) {
83            $userId = $row->user_id;
84            if (hash_equals($row['token'], hash('sha256', $token['token']))) {
85                if (time() > $row['expires']) {
86                    $row->delete();
87                    return null;
88                }
89                return $row;
90            }
91        }
92        if ($userId) {
93            throw new LoginTokenException('Tokens do not match', $userId);
94        }
95        return null;
96    }
97
98    /**
99     * Delete all tokens in a given series
100     *
101     * @param string $series         series
102     * @param ?int   $currentTokenId Current token ID to keep
103     *
104     * @return void
105     */
106    public function deleteBySeries(string $series, ?int $currentTokenId = null): void
107    {
108        $callback = function ($select) use ($series, $currentTokenId) {
109            $select->where->equalTo('series', $series);
110            if ($currentTokenId) {
111                $select->where->notEqualTo('id', $currentTokenId);
112            }
113        };
114        $this->delete($callback);
115    }
116
117    /**
118     * Delete all tokens for a user
119     *
120     * @param int $userId user identifier
121     *
122     * @return void
123     */
124    public function deleteByUserId(int $userId): void
125    {
126        $this->delete(['user_id' => $userId]);
127    }
128
129    /**
130     * Get tokens for a given user
131     *
132     * @param int  $userId  User identifier
133     * @param bool $grouped Whether to return results grouped by series
134     *
135     * @return array
136     */
137    public function getByUserId(int $userId, bool $grouped = true): array
138    {
139        $callback = function ($select) use ($userId, $grouped) {
140            $select->where->equalTo('user_id', $userId);
141            $select->order('last_login DESC');
142            if ($grouped) {
143                $select->columns(
144                    [
145                        // RowGateway requires an id field:
146                        'id' => new Expression(
147                            '1',
148                            [],
149                            [Expression::TYPE_IDENTIFIER]
150                        ),
151                        'series',
152                        'user_id',
153                        'last_login' => new Expression(
154                            'MAX(?)',
155                            ['last_login'],
156                            [Expression::TYPE_IDENTIFIER]
157                        ),
158                        'browser',
159                        'platform',
160                        'expires',
161                    ]
162                );
163                $select->group(['series', 'user_id', 'browser', 'platform', 'expires']);
164            }
165        };
166        return iterator_to_array($this->select($callback));
167    }
168
169    /**
170     * Get token by series
171     *
172     * @param string $series Series identifier
173     *
174     * @return ResultSetInterface
175     */
176    public function getBySeries(string $series): ResultSetInterface
177    {
178        return $this->select(compact('series'));
179    }
180
181    /**
182     * Update the select statement to find records to delete.
183     *
184     * @param Select $select    Select clause
185     * @param string $dateLimit Date threshold of an "expired" record in format
186     * 'Y-m-d H:i:s'.
187     *
188     * @return void
189     *
190     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
191     */
192    protected function expirationCallback($select, $dateLimit)
193    {
194        // Date limit ignored since login token already contains an expiration time.
195        $select->where->lessThan('expires', time());
196    }
197}