Passed
Push — master ( 63c666...b91fac )
by Pieter van der
03:51
created

Tiqr_StateStorage_Pdo::healthCheck()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

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