Tiqr_UserStorage_Pdo::setBlocked()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 1
c 2
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 2
nc 1
nop 2
crap 2
1
<?php
2
/**
3
 * This file is part of the tiqr project.
4
 * 
5
 * The tiqr project aims to provide an open implementation for 
6
 * authentication using mobile devices. It was initiated by 
7
 * SURFnet and developed by Egeniq.
8
 *
9
 * More information: http://www.tiqr.org
10
 *
11
 * @author Patrick Honing <[email protected]>
12
 * 
13
 * @package tiqr
14
 *
15
 * @license New BSD License - See LICENSE file for details.
16
 *
17
 * @copyright (C) 2010-2012 SURFnet BV
18
 * 
19
For MySQL:
20
21
CREATE TABLE IF NOT EXISTS user (
22
    id integer NOT NULL PRIMARY KEY AUTO_INCREMENT,
23
    userid varchar(30) NOT NULL UNIQUE,
24
    displayname varchar(30) NOT NULL,
25
    secret varchar(128),        // Optional column, see Tiqr_UserSecretStorage_Pdo
26
    loginattempts integer,      // number of failed login attempts counting towards a permanent block
27
    tmpblocktimestamp BIGINT,   // 8-byte integer, holds unix timestamp of temporary block. 0=not temporary block
28
    tmpblockattempts integer,   // Number of failed login attempts counting towards a temporary block
29
    blocked tinyint(1),         // used as boolean: 0=not blocked. 1=blocked
30
    notificationtype varchar(10),
31
    notificationaddress varchar(256)
32
);
33
34
 *
35
 * In version 3.0 the format of the tmpblocktimestamp was changed from a datetime format to an integer.
36
 * Because it holds a unix timestamp a 64-bit (8-byte) integer. To upgrade the user table to the new format use:
37
38
ALTER TABLE user MODIFY tmpblocktimestamp BIGINT;
39
40
 */
41
42
use Psr\Log\LoggerInterface;
43
44
45
/**
46
 * This user storage implementation implements a user storage using PDO.
47
 * It is usable for any database with a PDO driver
48
 * 
49
 * @author Patrick Honing <[email protected]>
50
 *
51
 * @see Tiqr_UserStorage::getStorage()
52
 * @see Tiqr_UserStorage_Interface
53
 *
54
 * Supported options:
55
 * table    : The name of the user table in the database. Optional. Defaults to "tiqruser".
56
 * dsn      : The dsn, see the PDO interface documentation
57
 * username : The database username
58
 * password : The database password
59
 */
60
61
class Tiqr_UserStorage_Pdo extends Tiqr_UserStorage_Abstract
62
{
63
    protected $handle = null;
64
    protected $tablename;
65
66
    private $_allowedStringColumns = ['displayname', 'notificationtype', 'notificationaddress'];
67
    private $_allowedIntColumns = ['loginattempts', 'tmpblockattempts', 'blocked', 'tmpblocktimestamp'];
68
    
69
    /**
70
     * Create an instance
71
     * @param array $config
72
     * @param array $secretconfig
73
     * @throws Exception
74
     */
75 3
    public function __construct(array $config, LoggerInterface $logger)
76
    {
77 3
        parent::__construct($config, $logger);
78 3
        $this->tablename = $config['table'] ?? 'tiqruser';
79
        try {
80 3
            $this->handle = new PDO(
81 3
                $config['dsn'],
82 3
                $config['username'],
83 3
                $config['password'],
84 3
                array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
85 3
            );
86
        } catch (PDOException $e) {
87
            $this->logger->error('Unable to establish a PDO connection.', array('exception'=>$e));
88
            throw ReadWriteException::fromOriginalException($e);
89
        }
90
    }
91
92
    /**
93
     * @param string $columnName to query
94
     * @param string $userId os an existing user, throws when the user does not exist
95
     * @return string The string value of the column, returns string('') when the column is NULL
96
     * @throws RuntimeException | InvalidArgumentException
97
     * @throws ReadWriteException when there was a problem communicating with the backed
98
     */
99 2
    private function _getStringValue(string $columnName, string $userId): string
100
    {
101 2
        if ( !in_array($columnName, $this->_allowedStringColumns) ) {
102
            throw new InvalidArgumentException('Unsupported column name');
103
        }
104
105
        try {
106 2
            $sth = $this->handle->prepare('SELECT ' . $columnName . ' FROM ' . $this->tablename . ' WHERE userid = ?');
107 2
            $sth->execute(array($userId));
108 2
            $res=$sth->fetchColumn();
109 2
            if ($res === false) {
110
                // No result
111
                $this->logger->error(sprintf('No result getting "%s" for user "%s"', $columnName, $userId));
112
                throw new RuntimeException('User not found');
113
            }
114 2
            if ($res === NULL) {
115 2
                return '';  // Value unset
116
            }
117 2
            if (!is_string($res)) {
118
                $this->logger->error(sprintf('Expected string type while getting "%s" for user "%s"', $columnName, $userId));
119
                throw new RuntimeException('Unexpected return type');
120
            }
121 2
            return $res;
122
        }
123
        catch (Exception $e) {
124
            $this->logger->error('PDO error getting user', array('exception' => $e, 'userId' => $userId, 'columnName'=>$columnName));
125
            throw ReadWriteException::fromOriginalException($e);
126
        }
127
    }
128
129
    /**
130
     * @param string $columnName to query
131
     * @param string $userId of an existing user, throws when the user does not exist
132
     * @return int The int value of the column, returns int(0) when the column is NULL
133
     * @throws RuntimeException | InvalidArgumentException
134
     * @throws ReadWriteException when there was a problem communicating with the backend
135
     *
136
     */
137 2
    private function _getIntValue(string $columnName, string $userId): int
138
    {
139 2
        if ( !in_array($columnName, $this->_allowedIntColumns) ) {
140
            throw new InvalidArgumentException('Unsupported column name');
141
        }
142
143
        try {
144 2
            $sth = $this->handle->prepare('SELECT ' . $columnName . ' FROM ' . $this->tablename . ' WHERE userid = ?');
145 2
            $sth->execute(array($userId));
146 2
            $res=$sth->fetchColumn();
147 2
            if ($res === false) {
148
                // No result
149
                $this->logger->error(sprintf('No result getting "%s" for user "%s"', $columnName, $userId));
150
                throw new RuntimeException('User not found');
151
            }
152 2
            if ($res === NULL) {
153 2
                return 0;  // Value unset
154
            }
155
            // Return type for integers depends on the PDO driver, can be string
156 2
            if (!is_numeric($res)) {
157
                $this->logger->error(sprintf('Expected int type while getting "%s" for user "%s"', $columnName, $userId));
158
                throw new RuntimeException('Unexpected return type');
159
            }
160 2
            return (int)$res;
161
        }
162
        catch (Exception $e) {
163
            $this->logger->error('PDO error getting user', array('exception' => $e, 'userId' => $userId, 'columnName'=>$columnName));
164
            throw ReadWriteException::fromOriginalException($e);
165
        }
166
    }
167
168
    /**
169
     * @param string $columnName name of the column to set
170
     * @param string $userId of an existing user, throws when the user does not exist
171
     * @param string $value The value to set in $columnName
172
     * @throws RuntimeException | InvalidArgumentException
173
     * @throws ReadWriteException when there was a problem communicating with the backend
174
     */
175 2
    private function _setStringValue(string $columnName, string $userId, string $value): void
176
    {
177 2
        if ( !in_array($columnName, $this->_allowedStringColumns) ) {
178
            throw new InvalidArgumentException('Unsupported column name');
179
        }
180
        try {
181 2
            $sth = $this->handle->prepare('UPDATE ' . $this->tablename . ' SET ' . $columnName . ' = ? WHERE userid = ?');
182 2
            $sth->execute(array($value, $userId));
183 2
            if ($sth->rowCount() == 0) {
184
                // Required for mysql which only returns the number of rows that were actually updated
185
                if (!$this->userExists($userId)) {
186 2
                    throw new RuntimeException('User not found');
187
                }
188
            }
189
        }
190
        catch (Exception $e) {
191
            $this->logger->error('PDO error updating user', array('exception' => $e, 'userId' => $userId, 'columnName'=>$columnName));
192
            throw ReadWriteException::fromOriginalException($e);
193
        }
194
    }
195
196
    /**
197
     * @param string $columnName name of the column to set
198
     * @param string $userId of an existing user, throws when the user does not exist
199
     * @param int $value The value to set in $columnName
200
     * @throws RuntimeException | InvalidArgumentException
201
     * @throws ReadWriteException when there was a problem communicating with the backend
202
     */
203 2
    private function _setIntValue(string $columnName, string $userId, int $value): void
204
    {
205 2
        if ( !in_array($columnName, $this->_allowedIntColumns) ) {
206
            throw new InvalidArgumentException('Unsupported column name');
207
        }
208
        try {
209 2
            $sth = $this->handle->prepare('UPDATE ' . $this->tablename . ' SET ' . $columnName . ' = ? WHERE userid = ?');
210 2
            $sth->execute(array($value, $userId));
211 2
            if ($sth->rowCount() == 0) {
212
                // Required for mysql which only returns the number of rows that were actually updated
213
                if (!$this->userExists($userId)) {
214 2
                    throw new RuntimeException('User not found');
215
                }
216
            }
217
        }
218
        catch (Exception $e) {
219
            $this->logger->error('PDO error updating user', array('exception' => $e, 'userId' => $userId, 'columnName'=>$columnName));
220
            throw ReadWriteException::fromOriginalException($e);
221
        }
222
    }
223
224
    /**
225
     * @see Tiqr_UserStorage_Interface::createUser()
226
     */
227 2
    public function createUser(string $userId, string $displayName): void
228
    {
229 2
        if ($this->userExists($userId)) {
230 2
            throw new RuntimeException(sprintf('User "%s" already exists', $userId));
231
        }
232
        try {
233 2
            $sth = $this->handle->prepare("INSERT INTO ".$this->tablename." (displayname,userid) VALUES (?,?)");
234 2
            $sth->execute(array($displayName, $userId));
235
        }
236
        catch (Exception $e) {
237
            $this->logger->error(sprintf('Error creating user "%s"', $userId), array('exception'=>$e));
238
            throw new ReadWriteException('The user could not be saved in the user storage (PDO)');
239
        }
240
    }
241
242
    /**
243
     * @see Tiqr_UserStorage_Interface::userExists()
244
     */
245 2
    public function userExists(string $userId): bool
246
    {
247
        try {
248 2
            $sth = $this->handle->prepare("SELECT userid FROM ".$this->tablename." WHERE userid = ?");
249 2
            $sth->execute(array($userId));
250 2
            return (false !== $sth->fetchColumn());
251
        }
252
        catch (Exception $e) {
253
            $this->logger->error('PDO error checking user exists', array('exception'=>$e, 'userId'=>$userId));
254
            throw ReadWriteException::fromOriginalException($e);
255
        }
256
    }
257
258
    /**
259
     * @see Tiqr_UserStorage_Interface::getDisplayName()
260
     */
261 2
    public function getDisplayName(string $userId): string
262
    {
263 2
        return $this->_getStringValue('displayname', $userId);
264
    }
265
266
    /**
267
     * @see Tiqr_UserStorage_Interface::getNotificationType()
268
     */
269 2
    public function getNotificationType(string $userId): string
270
    {
271 2
        return $this->_getStringValue('notificationtype', $userId);
272
    }
273
274
    /**
275
     * @see Tiqr_UserStorage_Interface::getNotificationType()
276
     */
277 2
    public function setNotificationType(string $userId, string $type): void
278
    {
279 2
        $this->_setStringValue('notificationtype', $userId, $type);
280
    }
281
282
    /**
283
     * @see Tiqr_UserStorage_Interface::getNotificationAddress()
284
     */
285 2
    public function getNotificationAddress(string $userId): string
286
    {
287 2
        return $this->_getStringValue('notificationaddress', $userId);
288
    }
289
290
    /**
291
     * @see Tiqr_UserStorage_Interface::setNotificationAddress()
292
     */
293
    public function setNotificationAddress(string $userId, string $address): void
294
    {
295
        $this->_setStringValue('notificationaddress', $userId, $address);
296
    }
297
298
    /**
299
     * @see Tiqr_UserStorage_Interface::getLoginAttempts()
300
     */
301 2
    public function getLoginAttempts(string $userId): int
302
    {
303 2
        return $this->_getIntValue('loginattempts', $userId);
304
    }
305
306
    /**
307
     * @see Tiqr_UserStorage_Interface::setLoginAttempts()
308
     */
309 2
    public function setLoginAttempts(string $userId, int $amount): void
310
    {
311 2
        $this->_setIntValue('loginattempts', $userId, $amount);
312
    }
313
314
    /**
315
     * @see Tiqr_UserStorage_Interface::isBlocked()
316
     */
317 2
    public function isBlocked(string $userId, int $tempBlockDuration = 0): bool
318
    {
319
        // Check for blocked
320 2
        if ($this->_getIntValue('blocked', $userId) != 0) {
321 2
            return true;   // Blocked
322
        }
323
324 2
        if (0 == $tempBlockDuration) {
325 2
            return false;   // No check for temporary block
326
        }
327
328
        // Check for temporary block
329 2
        $timestamp = $this->getTemporaryBlockTimestamp($userId);
330
        // if no temporary block timestamp is set or if the temporary block is expired, return false
331 2
        if ( 0 == $timestamp || ($timestamp + $tempBlockDuration * 60) < time()) {
332 2
            return false;
333
        }
334 2
        return true;
335
    }
336
337
    /**
338
     * @see Tiqr_UserStorage_Interface::setBlocked()
339
     */
340 2
    public function setBlocked(string $userId, bool $blocked): void
341
    {
342 2
        $this->_setIntValue('blocked', $userId, ($blocked) ? 1 : 0);
343
    }
344
345
    /**
346
     * @see Tiqr_UserStorage_Interface::setTemporaryBlockAttempts()
347
     */
348 2
    public function setTemporaryBlockAttempts(string $userId, int $amount): void
349
    {
350 2
        $this->_setIntValue('tmpblockattempts', $userId, $amount);
351
    }
352
353
    /**
354
     * @see Tiqr_UserStorage_Interface::getTemporaryBlockAttempts()
355
     */
356 2
    public function getTemporaryBlockAttempts(string $userId): int {
357 2
        return $this->_getIntValue('tmpblockattempts', $userId);
358
    }
359
360
    /**
361
     * @see Tiqr_UserStorage_Interface::setTemporaryBlockTimestamp()
362
     */
363 2
    public function setTemporaryBlockTimestamp(string $userId, int $timestamp): void
364
    {
365 2
        $this->_setIntValue('tmpblocktimestamp', $userId, $timestamp);
366
    }
367
368
    /**
369
     * @see Tiqr_UserStorage_Interface::getTemporaryBlockTimestamp()
370
     */
371 2
    public function getTemporaryBlockTimestamp(string $userId): int
372
    {
373 2
        return $this->_getIntValue('tmpblocktimestamp', $userId);
374
    }
375
376
    /**
377
     * @see Tiqr_HealthCheck_Interface::healthCheck()
378
     */
379 3
    public function healthCheck(string &$statusMessage = ''): bool
380
    {
381
        // Check whether the table exists by reading a random row
382
        try {
383 3
            $sth = $this->handle->prepare('SELECT displayname, notificationtype, notificationaddress, loginattempts, tmpblockattempts, blocked, tmpblocktimestamp FROM '.$this->tablename.' LIMIT 1');
384 2
            $sth->execute();
385
        }
386 1
        catch (Exception $e) {
387 1
            $statusMessage = "Error reading from UserStorage_PDO: ". $e->getMessage();
388 1
            return false;
389
        }
390
391 2
        return true;
392
    }
393
}
394