Passed
Push — master ( 35fb8a...2dccb2 )
by Pieter van der
05:32 queued 14s
created

Tiqr_UserStorage_Pdo::_setStringValue()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 7.3471

Importance

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