Passed
Pull Request — develop (#48)
by Pieter van der
04:16
created

Tiqr_StateStorage_Pdo   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 178
Duplicated Lines 0 %

Test Coverage

Coverage 53.25%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 22
eloc 76
c 5
b 0
f 0
dl 0
loc 178
ccs 41
cts 77
cp 0.5325
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 3
A cleanExpired() 0 13 2
A getValue() 0 29 5
A unsetValue() 0 21 4
A setValue() 0 29 5
A keyExists() 0 16 3
1
<?php
2
3
use Psr\Log\LoggerInterface;
4
5
/**
6
 * This file is part of the tiqr project.
7
 * 
8
 * The tiqr project aims to provide an open implementation for 
9
 * authentication using mobile devices. It was initiated by 
10
 * SURFnet and developed by Egeniq.
11
 *
12
 * More information: http://www.tiqr.org
13
 *
14
 * @author Patrick Honing <[email protected]>
15
 * 
16
 * @package tiqr
17
 *
18
 * @license New BSD License - See LICENSE file for details.
19
 *
20
 * @copyright (C) 2010-2012 SURFnet BV
21
 * 
22
 * 
23
 *
24
 *
25
 * Use a database table for storing the tiqr state (session) information
26
 * There is a cleanup query that runs with a default probability of 10% each time
27
 * Tiqr_StateStorage_StateStorageInterface::setValue() is used to cleanup expired keys and prevent the table
28
 * from growing indefinitely.
29
 * The default 10% probability is good for test setups, but is unnecessary high for production use. For production use,
30
 * with high number of authentications and registrations, a much lower value should be used to prevent unnecessary
31
 * loading the database with DELETE queries. Note that the cleanup is only there to prevent indefinite growth of the
32
 * table, expired keys are never returned, whether they exist in the database or not.
33
 *
34
 * The goal is to prevent on the one hand running the cleanup to often with the chance of running multiple queries at
35
 * the same time, while on the other hand hardly ever running the cleanup leading to an unnecessary large table and query
36
 * execution time when it does run.
37
 *
38
 * The following information can be used for choosing a suitable cleanup_probability:
39
 * - A successful authentication creates two keys, i.e. two calls to setValue()
40
 * - A successful enrollment creates three keys, i.e. three calls to setValue()
41
 * Add to that the keys created by the application that uses the library (e.g. Stepup-Tiqr creates one key).
42
 *
43
 * A good starting point is using the typical peak number of authentications per hour and setting the cleanup_probability
44
 * so that it would on average run once during such an hour. Since the number of authentications >> number of enrollments
45
 * A good starting value for cleanup_probability would be:
46
 *
47
 *  1 / ( [number of authentications per hour] * ( 2 + [ keys used by application] ) )
48
 *
49
 * E.g. for 10000 authentications per hour, with one key created by the application:
50
 *      cleanup_probability = 1 / (10000 (2 + 1)) = 0.00003
51
 *
52
 *
53
 * Create SQL table (MySQL):
54
55
 CREATE TABLE IF NOT EXISTS tiqrstate (
56
    `key` varchar(255) PRIMARY KEY,
57
    expire BIGINT,
58
    `value` text
59
);
60
61
CREATE INDEX IF NOT EXISTS index_tiqrstate_expire ON tiqrstate (expire);
62
63
 * @see Tiqr_StateStorage::getStorage()
64
 * @see Tiqr_StateStorage_StateStorageInterface
65
 *
66
 * Supported options:
67
 * table               : The name of the table in the database
68
 * dsn                 : The dsn, see the PDO interface documentation
69
 * username            : The database username
70
 * password            : The database password
71
 * cleanup_probability : The probability that the cleanup of expired keys is executed. Optional, defaults to 0.1
72
 *                       Specify the desired probability as a float. E.g. 0.01 = 1%; 0.001 = 0.1%
73
 *
74
 */
75
76
77
class Tiqr_StateStorage_Pdo extends Tiqr_StateStorage_Abstract
78
{
79
    /**
80
     * @var PDO
81
     */
82
    protected $handle;
83
84
    /**
85
     * @var string
86
     */
87
    private $tablename;
88
89
    /**
90
     * @var int
91
     */
92
    private $cleanupProbability;
93
94
    /**
95
     * @param PDO $pdoInstance The PDO instance where all state storage operations are performed on
96
     * @param LoggerInterface
97
     * @param string $tablename The tablename that is used for storing and retrieving the state storage
98
     * @param float $cleanupProbability The probability the expired state storage items are removed on a 'setValue' call. Example usage: 0 = never, 0.5 = 50% chance, 1 = always
99
     *
100
     * @throws RuntimeException when an invalid cleanupProbability is configured
101
     */
102 5
    public function __construct(PDO $pdoInstance, LoggerInterface $logger, string $tablename, float $cleanupProbability)
103
    {
104 5
        if ($cleanupProbability < 0 || $cleanupProbability > 1) {
105 2
            throw new RuntimeException('The probability for removing the expired state should be expressed in a floating point value between 0 and 1.');
106
        }
107 5
        $this->cleanupProbability = $cleanupProbability;
108 5
        $this->tablename = $tablename;
109 5
        $this->handle = $pdoInstance;
110 5
        $this->logger = $logger;
111 5
    }
112
113
    /**
114
     * @param string $key to lookup
115
     * @return bool true when $key is found, false when the key does not exist
116
     * @throws ReadWriteException
117
     */
118
    private function keyExists(string $key): bool
0 ignored issues
show
Unused Code introduced by
The method keyExists() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
119
    {
120
        if (empty($key)) {
121
            throw new InvalidArgumentException('Empty key not allowed');
122
        }
123
        try {
124
            $sth = $this->handle->prepare('SELECT `key` FROM ' . $this->tablename . ' WHERE `key` = ?');
125
            $sth->execute(array($key));
126
            return $sth->fetchColumn() !== false;
127
        }
128
        catch (Exception $e) {
129
            $this->logger->error(
130
                sprintf('Error checking for key "%s" in PDO StateStorage', $key),
131
                array('exception' => $e)
132
            );
133
            throw ReadWriteException::fromOriginalException($e);
134
        }
135
    }
136
137
    /**
138
     * Remove expired keys
139
     * This is a maintenance task that should be periodically run
140
     * Does not throw
141
     */
142 1
    private function cleanExpired(): void {
143
        try {
144 1
            $sth = $this->handle->prepare("DELETE FROM " . $this->tablename . " WHERE `expire` < ? AND NOT `expire` = 0");
145 1
            $sth->execute(array(time()));
146 1
            $deletedRows=$sth->rowCount();
147 1
            $this->logger->notice(
148 1
                sprintf("Deleted %d expired keys", $deletedRows)
149
            );
150
        }
151
        catch (Exception $e) {
152
            $this->logger->error(
153
                sprintf("Deleting expired keys failed: %s", $e->getMessage()),
154
                array('exception', $e)
155
            );
156
        }
157 1
    }
158
    
159
    /**
160
     * @see Tiqr_StateStorage_StateStorageInterface::setValue()
161
     */
162 2
    public function setValue(string $key, $value, int $expire=0): void
163
    {
164 2
        if (empty($key)) {
165 1
            throw new InvalidArgumentException('Empty key not allowed');
166
        }
167 1
        if (((float) rand() /(float) getrandmax()) < $this->cleanupProbability) {
168 1
            $this->cleanExpired();
169
        }
170
        // REPLACE INTO is mysql dialect. Supported by sqlite as well.
171
        // This does:
172
        // INSERT INTO tablename (`value`,`expire`,`key`) VALUES (?,?,?)
173
        //   ON CONFLICT UPDATE tablename SET `value`=?, `expire`=? WHERE `key`=?
174
        // in pgsql "ON CONFLICT" is "ON DUPLICATE KEY"
175
176 1
        $sth = $this->handle->prepare("REPLACE INTO ".$this->tablename." (`value`,`expire`,`key`) VALUES (?,?,?)");
177
178
        // $expire == 0 means never expire
179 1
        if ($expire != 0) {
180 1
            $expire+=time();    // Store unix timestamp after which the key expires
181
        }
182
        try {
183 1
            $sth->execute(array(serialize($value), $expire, $key));
184
        }
185
        catch (Exception $e) {
186
            $this->logger->error(
187
                sprintf('Unable to store key "%s" in PDO StateStorage', $key),
188
                array('exception' => $e)
189
            );
190
            throw ReadWriteException::fromOriginalException($e);
191
        }
192 1
    }
193
        
194
    /**
195
     * @see Tiqr_StateStorage_StateStorageInterface::unsetValue()
196
     */
197 1
    public function unsetValue($key): void
198
    {
199 1
        if (empty($key)) {
200
            throw new InvalidArgumentException('Empty key not allowed');
201
        }
202
        try {
203 1
            $sth = $this->handle->prepare("DELETE FROM " . $this->tablename . " WHERE `key` = ?");
204 1
            $sth->execute(array($key));
205
        }
206
        catch (Exception $e) {
207
            $this->logger->error(
208
                sprintf('Error deleting key "%s" from PDO StateStorage', $key),
209
                array('exception' => $e)
210
            );
211
            throw ReadWriteException::fromOriginalException($e);
212
        }
213
214 1
        if ($sth->rowCount() === 0) {
215
            // Key did not exist, this is not an error
216 1
            $this->logger->info(
217 1
                sprintf('unsetValue: key "%s" not found in PDO StateStorage', $key
218
                )
219
            );
220
        }
221 1
    }
222
    
223
    /**
224
     * @see Tiqr_StateStorage_StateStorageInterface::getValue()
225
     */
226 1
    public function getValue(string $key)
227
    {
228 1
        if (empty($key)) {
229
            throw new InvalidArgumentException('Empty key not allowed');
230
        }
231
232
        try {
233 1
            $sth = $this->handle->prepare('SELECT `value` FROM ' . $this->tablename . ' WHERE `key` = ? AND (`expire` >= ? OR `expire` = 0)');
234 1
            $sth->execute(array($key, time()));
235
        }
236
        catch (Exception $e) {
237
            $this->logger->error(
238
                sprintf('Error getting value for key "%s" from PDO StateStorage', $key),
239
                array('exception' => $e)
240
            );
241
            throw ReadWriteException::fromOriginalException($e);
242
        }
243 1
        $result = $sth->fetchColumn();
244 1
        if (false === $result) {
245
            // Occurs normally
246 1
            $this->logger->info(sprintf('getValue: Key "%s" not found in PDO StateStorage', $key));
247 1
            return NULL;    // Key not found
248
        }
249
        $result=unserialize($result, array('allowed_classes' => false));
250
        if (false === $result) {
251
            throw new RuntimeException(sprintf('getValue: unserialize error for key "%s" in PDO StateStorage', $key));
252
        }
253
254
        return $result;
255
    }
256
257
}
258