Passed
Push — develop ( 339f21...34e8b6 )
by Pieter van der
14:45
created

Tiqr_UserStorage_Pdo::setNotificationAddress()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 2
crap 1
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(15),
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 sql/user.sql for the table definition
52
 *
53
 * You can create separate tables for Tiqr_UserSecretStorage_Pdo and Tiqr_UserStorage_Pdo. In that
54
 * case use:
55
 * - sql/usersecret.sql for Tiqr_UserSecretStorage_Pdo
56
 * - sql/user.sql for Tiqr_UserStorage_Pdo
57
 * You can also combine the two tables by adding a "secret" column to the user storage table.
58
 * In that case use sql/user_combined.sql for both Tiqr_UserSecretStorage_Pdo and Tiqr_UserStorage_Pdo.
59
 *
60
 * @see Tiqr_UserStorage::getStorage()
61
 * @see Tiqr_UserStorage_Interface
62
 *
63
 * Supported options:
64
 * table    : The name of the user table in the database. Optional. Defaults to "tiqruser".
65
 * dsn      : The dsn, see the PDO interface documentation
66
 * username : The database username
67
 * password : The database password
68
 */
69
70
class Tiqr_UserStorage_Pdo extends Tiqr_UserStorage_Abstract
71
{
72
    protected $handle = null;
73
    protected $tablename;
74
75
    private $_allowedStringColumns = ['displayname', 'notificationtype', 'notificationaddress'];
76
    private $_allowedIntColumns = ['loginattempts', 'tmpblockattempts', 'blocked', 'tmpblocktimestamp'];
77
    
78
    /**
79
     * Create an instance
80
     * @param array $config
81
     * @param array $secretconfig
82
     * @throws Exception
83
     */
84 3
    public function __construct(array $config, LoggerInterface $logger)
85
    {
86 3
        parent::__construct($config, $logger);
87 3
        $this->tablename = $config['table'] ?? 'tiqruser';
88
        try {
89 3
            $this->handle = new PDO(
90 3
                $config['dsn'],
91 3
                $config['username'],
92 3
                $config['password'],
93 3
                array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
94 3
            );
95
        } catch (PDOException $e) {
96
            $this->logger->error('Unable to establish a PDO connection.', array('exception'=>$e));
97
            throw ReadWriteException::fromOriginalException($e);
98
        }
99
    }
100
101
    /**
102
     * @param string $columnName to query
103
     * @param string $userId os an existing user, throws when the user does not exist
104
     * @return string The string value of the column, returns string('') when the column is NULL
105
     * @throws RuntimeException | InvalidArgumentException
106
     * @throws ReadWriteException when there was a problem communicating with the backed
107
     */
108 2
    private function _getStringValue(string $columnName, string $userId): string
109
    {
110 2
        if ( !in_array($columnName, $this->_allowedStringColumns) ) {
111
            throw new InvalidArgumentException('Unsupported column name');
112
        }
113
114
        try {
115 2
            $sth = $this->handle->prepare('SELECT ' . $columnName . ' FROM ' . $this->tablename . ' WHERE userid = ?');
116 2
            $sth->execute(array($userId));
117 2
            $res=$sth->fetchColumn();
118 2
            if ($res === false) {
119
                // No result
120
                $this->logger->error(sprintf('No result getting "%s" for user "%s"', $columnName, $userId));
121
                throw new RuntimeException('User not found');
122
            }
123 2
            if ($res === NULL) {
124 2
                return '';  // Value unset
125
            }
126 2
            if (!is_string($res)) {
127
                $this->logger->error(sprintf('Expected string type while getting "%s" for user "%s"', $columnName, $userId));
128
                throw new RuntimeException('Unexpected return type');
129
            }
130 2
            return $res;
131
        }
132
        catch (Exception $e) {
133
            $this->logger->error('PDO error getting user', array('exception' => $e, 'userId' => $userId, 'columnName'=>$columnName));
134
            throw ReadWriteException::fromOriginalException($e);
135
        }
136
    }
137
138
    /**
139
     * @param string $columnName to query
140
     * @param string $userId of an existing user, throws when the user does not exist
141
     * @return int The int value of the column, returns int(0) when the column is NULL
142
     * @throws RuntimeException | InvalidArgumentException
143
     * @throws ReadWriteException when there was a problem communicating with the backend
144
     *
145
     */
146 2
    private function _getIntValue(string $columnName, string $userId): int
147
    {
148 2
        if ( !in_array($columnName, $this->_allowedIntColumns) ) {
149
            throw new InvalidArgumentException('Unsupported column name');
150
        }
151
152
        try {
153 2
            $sth = $this->handle->prepare('SELECT ' . $columnName . ' FROM ' . $this->tablename . ' WHERE userid = ?');
154 2
            $sth->execute(array($userId));
155 2
            $res=$sth->fetchColumn();
156 2
            if ($res === false) {
157
                // No result
158
                $this->logger->error(sprintf('No result getting "%s" for user "%s"', $columnName, $userId));
159
                throw new RuntimeException('User not found');
160
            }
161 2
            if ($res === NULL) {
162 2
                return 0;  // Value unset
163
            }
164
            // Return type for integers depends on the PDO driver, can be string
165 2
            if (!is_numeric($res)) {
166
                $this->logger->error(sprintf('Expected int type while getting "%s" for user "%s"', $columnName, $userId));
167
                throw new RuntimeException('Unexpected return type');
168
            }
169 2
            return (int)$res;
170
        }
171
        catch (Exception $e) {
172
            $this->logger->error('PDO error getting user', array('exception' => $e, 'userId' => $userId, 'columnName'=>$columnName));
173
            throw ReadWriteException::fromOriginalException($e);
174
        }
175
    }
176
177
    /**
178
     * @param string $columnName name of the column to set
179
     * @param string $userId of an existing user, throws when the user does not exist
180
     * @param string $value The value to set in $columnName
181
     * @throws RuntimeException | InvalidArgumentException
182
     * @throws ReadWriteException when there was a problem communicating with the backend
183
     */
184 2
    private function _setStringValue(string $columnName, string $userId, string $value): void
185
    {
186 2
        if ( !in_array($columnName, $this->_allowedStringColumns) ) {
187
            throw new InvalidArgumentException('Unsupported column name');
188
        }
189
        try {
190 2
            $sth = $this->handle->prepare('UPDATE ' . $this->tablename . ' SET ' . $columnName . ' = ? WHERE userid = ?');
191 2
            $sth->execute(array($value, $userId));
192 2
            if ($sth->rowCount() == 0) {
193
                // Required for mysql which only returns the number of rows that were actually updated
194
                if (!$this->userExists($userId)) {
195 2
                    throw new RuntimeException('User not found');
196
                }
197
            }
198
        }
199
        catch (Exception $e) {
200
            $this->logger->error('PDO error updating user', array('exception' => $e, 'userId' => $userId, 'columnName'=>$columnName));
201
            throw ReadWriteException::fromOriginalException($e);
202
        }
203
    }
204
205
    /**
206
     * @param string $columnName name of the column to set
207
     * @param string $userId of an existing user, throws when the user does not exist
208
     * @param int $value The value to set in $columnName
209
     * @throws RuntimeException | InvalidArgumentException
210
     * @throws ReadWriteException when there was a problem communicating with the backend
211
     */
212 2
    private function _setIntValue(string $columnName, string $userId, int $value): void
213
    {
214 2
        if ( !in_array($columnName, $this->_allowedIntColumns) ) {
215
            throw new InvalidArgumentException('Unsupported column name');
216
        }
217
        try {
218 2
            $sth = $this->handle->prepare('UPDATE ' . $this->tablename . ' SET ' . $columnName . ' = ? WHERE userid = ?');
219 2
            $sth->execute(array($value, $userId));
220 2
            if ($sth->rowCount() == 0) {
221
                // Required for mysql which only returns the number of rows that were actually updated
222
                if (!$this->userExists($userId)) {
223 2
                    throw new RuntimeException('User not found');
224
                }
225
            }
226
        }
227
        catch (Exception $e) {
228
            $this->logger->error('PDO error updating user', array('exception' => $e, 'userId' => $userId, 'columnName'=>$columnName));
229
            throw ReadWriteException::fromOriginalException($e);
230
        }
231
    }
232
233
    /**
234
     * @see Tiqr_UserStorage_Interface::createUser()
235
     */
236 2
    public function createUser(string $userId, string $displayName): void
237
    {
238 2
        if ($this->userExists($userId)) {
239 2
            throw new RuntimeException(sprintf('User "%s" already exists', $userId));
240
        }
241
        try {
242 2
            $sth = $this->handle->prepare("INSERT INTO ".$this->tablename." (displayname,userid) VALUES (?,?)");
243 2
            $sth->execute(array($displayName, $userId));
244
        }
245
        catch (Exception $e) {
246
            $this->logger->error(sprintf('Error creating user "%s"', $userId), array('exception'=>$e));
247
            throw new ReadWriteException('The user could not be saved in the user storage (PDO)');
248
        }
249
    }
250
251
    /**
252
     * @see Tiqr_UserStorage_Interface::userExists()
253
     */
254 2
    public function userExists(string $userId): bool
255
    {
256
        try {
257 2
            $sth = $this->handle->prepare("SELECT userid FROM ".$this->tablename." WHERE userid = ?");
258 2
            $sth->execute(array($userId));
259 2
            return (false !== $sth->fetchColumn());
260
        }
261
        catch (Exception $e) {
262
            $this->logger->error('PDO error checking user exists', array('exception'=>$e, 'userId'=>$userId));
263
            throw ReadWriteException::fromOriginalException($e);
264
        }
265
    }
266
267
    /**
268
     * @see Tiqr_UserStorage_Interface::getDisplayName()
269
     */
270 2
    public function getDisplayName(string $userId): string
271
    {
272 2
        return $this->_getStringValue('displayname', $userId);
273
    }
274
275
    /**
276
     * @see Tiqr_UserStorage_Interface::getNotificationType()
277
     */
278 2
    public function getNotificationType(string $userId): string
279
    {
280 2
        return $this->_getStringValue('notificationtype', $userId);
281
    }
282
283
    /**
284
     * @see Tiqr_UserStorage_Interface::getNotificationType()
285
     */
286 2
    public function setNotificationType(string $userId, string $type): void
287
    {
288 2
        $this->_setStringValue('notificationtype', $userId, $type);
289
    }
290
291
    /**
292
     * @see Tiqr_UserStorage_Interface::getNotificationAddress()
293
     */
294 2
    public function getNotificationAddress(string $userId): string
295
    {
296 2
        return $this->_getStringValue('notificationaddress', $userId);
297
    }
298
299
    /**
300
     * @see Tiqr_UserStorage_Interface::setNotificationAddress()
301
     */
302 2
    public function setNotificationAddress(string $userId, string $address): void
303
    {
304 2
        $this->_setStringValue('notificationaddress', $userId, $address);
305
    }
306
307
    /**
308
     * @see Tiqr_UserStorage_Interface::getLoginAttempts()
309
     */
310 2
    public function getLoginAttempts(string $userId): int
311
    {
312 2
        return $this->_getIntValue('loginattempts', $userId);
313
    }
314
315
    /**
316
     * @see Tiqr_UserStorage_Interface::setLoginAttempts()
317
     */
318 2
    public function setLoginAttempts(string $userId, int $amount): void
319
    {
320 2
        $this->_setIntValue('loginattempts', $userId, $amount);
321
    }
322
323
    /**
324
     * @see Tiqr_UserStorage_Interface::isBlocked()
325
     */
326 2
    public function isBlocked(string $userId, int $tempBlockDuration = 0): bool
327
    {
328
        // Check for blocked
329 2
        if ($this->_getIntValue('blocked', $userId) != 0) {
330 2
            return true;   // Blocked
331
        }
332
333 2
        if (0 == $tempBlockDuration) {
334 2
            return false;   // No check for temporary block
335
        }
336
337
        // Check for temporary block
338 2
        $timestamp = $this->getTemporaryBlockTimestamp($userId);
339
        // if no temporary block timestamp is set or if the temporary block is expired, return false
340 2
        if ( 0 == $timestamp || ($timestamp + $tempBlockDuration * 60) < time()) {
341 2
            return false;
342
        }
343 2
        return true;
344
    }
345
346
    /**
347
     * @see Tiqr_UserStorage_Interface::setBlocked()
348
     */
349 2
    public function setBlocked(string $userId, bool $blocked): void
350
    {
351 2
        $this->_setIntValue('blocked', $userId, ($blocked) ? 1 : 0);
352
    }
353
354
    /**
355
     * @see Tiqr_UserStorage_Interface::setTemporaryBlockAttempts()
356
     */
357 2
    public function setTemporaryBlockAttempts(string $userId, int $amount): void
358
    {
359 2
        $this->_setIntValue('tmpblockattempts', $userId, $amount);
360
    }
361
362
    /**
363
     * @see Tiqr_UserStorage_Interface::getTemporaryBlockAttempts()
364
     */
365 2
    public function getTemporaryBlockAttempts(string $userId): int {
366 2
        return $this->_getIntValue('tmpblockattempts', $userId);
367
    }
368
369
    /**
370
     * @see Tiqr_UserStorage_Interface::setTemporaryBlockTimestamp()
371
     */
372 2
    public function setTemporaryBlockTimestamp(string $userId, int $timestamp): void
373
    {
374 2
        $this->_setIntValue('tmpblocktimestamp', $userId, $timestamp);
375
    }
376
377
    /**
378
     * @see Tiqr_UserStorage_Interface::getTemporaryBlockTimestamp()
379
     */
380 2
    public function getTemporaryBlockTimestamp(string $userId): int
381
    {
382 2
        return $this->_getIntValue('tmpblocktimestamp', $userId);
383
    }
384
385
    /**
386
     * @see Tiqr_HealthCheck_Interface::healthCheck()
387
     */
388 3
    public function healthCheck(string &$statusMessage = ''): bool
389
    {
390
        // Check whether the table exists by reading a random row
391
        try {
392 3
            $sth = $this->handle->prepare('SELECT displayname, notificationtype, notificationaddress, loginattempts, tmpblockattempts, blocked, tmpblocktimestamp FROM '.$this->tablename.' LIMIT 1');
393 2
            $sth->execute();
394
        }
395 1
        catch (Exception $e) {
396 1
            $statusMessage = "Error reading from UserStorage_PDO: ". $e->getMessage();
397 1
            return false;
398
        }
399
400 2
        return true;
401
    }
402
}
403