SQLTicketStore   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 35
eloc 142
dl 0
loc 344
rs 9.6
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A cleanKVStore() 0 7 1
A get() 0 30 5
A set() 0 28 4
A delete() 0 16 2
A scopeTicketId() 0 3 1
A __construct() 0 22 2
A insertOrUpdateFallback() 0 34 5
A insertOrUpdate() 0 27 4
A getTicket() 0 5 1
A addTicket() 0 5 1
A getTableVersion() 0 7 2
A setTableVersion() 0 11 1
A deleteTicket() 0 5 1
A initKVTable() 0 15 2
A initTableVersionTable() 0 14 3
1
<?php
2
3
/*
4
 *    simpleSAMLphp-casserver is a CAS 1.0 and 2.0 compliant CAS server in the form of a simpleSAMLphp module
5
 *
6
 *    Copyright (C) 2013  Bjorn R. Jensen
7
 *
8
 *    This library is free software; you can redistribute it and/or
9
 *    modify it under the terms of the GNU Lesser General Public
10
 *    License as published by the Free Software Foundation; either
11
 *    version 2.1 of the License, or (at your option) any later version.
12
 *
13
 *    This library is distributed in the hope that it will be useful,
14
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16
 *    Lesser General Public License for more details.
17
 *
18
 *    You should have received a copy of the GNU Lesser General Public
19
 *    License along with this library; if not, write to the Free Software
20
 *    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
21
 *
22
 */
23
24
declare(strict_types=1);
25
26
namespace SimpleSAML\Module\casserver\Cas\Ticket;
27
28
use Exception;
29
use PDO;
30
use PDOException;
31
use PDOStatement;
32
use SimpleSAML\Assert\Assert;
33
use SimpleSAML\Configuration;
34
use SimpleSAML\Logger;
35
36
class SQLTicketStore extends TicketStore
37
{
38
    /** @var \PDO $pdo */
39
    public PDO $pdo;
40
41
    /** @var string $driver */
42
    public string $driver = 'pdo';
43
44
    /** @var string $prefix */
45
    public string $prefix;
46
47
    /** @var array $tableVersions */
48
    private array $tableVersions = [];
49
50
51
    /**
52
     * @param \SimpleSAML\Configuration $config
53
     */
54
    public function __construct(Configuration $config)
55
    {
56
        parent::__construct($config);
57
58
        $storeConfig = $config->getConfigItem('ticketstore');
59
        $dsn = $storeConfig->getString('dsn');
60
        $username = $storeConfig->getString('username');
61
        $password = $storeConfig->getString('password');
62
        $options = $storeConfig->getOptionalArray('options', []);
63
        $this->prefix = $storeConfig->getOptionalString('prefix', '');
64
65
        $this->pdo = new PDO($dsn, $username, $password, $options);
66
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
67
68
        $this->driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
69
70
        if ($this->driver === 'mysql') {
71
            $this->pdo->exec('SET time_zone = "+00:00"');
72
        }
73
74
        $this->initTableVersionTable();
75
        $this->initKVTable();
76
    }
77
78
79
    /**
80
     * @param string $ticketId
81
     * @return array|null
82
     */
83
    public function getTicket(string $ticketId): ?array
84
    {
85
        $scopedTicketId = $this->scopeTicketId($ticketId);
86
87
        return $this->get($scopedTicketId);
88
    }
89
90
91
    /**
92
     * @param array $ticket
93
     */
94
    public function addTicket(array $ticket): void
95
    {
96
        $scopedTicketId = $this->scopeTicketId($ticket['id']);
97
98
        $this->set($scopedTicketId, $ticket, $ticket['validBefore']);
99
    }
100
101
102
    /**
103
     * @param string $ticketId
104
     */
105
    public function deleteTicket(string $ticketId): void
106
    {
107
        $scopedTicketId = $this->scopeTicketId($ticketId);
108
109
        $this->delete($scopedTicketId);
110
    }
111
112
113
    /**
114
     * @param string $ticketId
115
     * @return string
116
     */
117
    private function scopeTicketId(string $ticketId): string
118
    {
119
        return $this->prefix . '.' . $ticketId;
120
    }
121
122
123
    /**
124
     */
125
    private function initTableVersionTable(): void
126
    {
127
        $this->tableVersions = [];
128
129
        try {
130
            $fetchTableVersion = $this->pdo->query('SELECT _name, _version FROM ' . $this->prefix . '_tableVersion');
131
        } catch (PDOException $e) {
132
            $this->pdo->exec('CREATE TABLE ' . $this->prefix .
133
                '_tableVersion (_name VARCHAR(30) NOT NULL UNIQUE, _version INTEGER NOT NULL)');
134
            return;
135
        }
136
137
        while (($row = $fetchTableVersion->fetch(PDO::FETCH_ASSOC)) !== false) {
138
            $this->tableVersions[$row['_name']] = intval($row['_version']);
139
        }
140
    }
141
142
143
    /**
144
     */
145
    private function initKVTable(): void
146
    {
147
        if ($this->getTableVersion('kvstore') === 1) {
148
            /* Table initialized. */
149
            return;
150
        }
151
152
        $query = 'CREATE TABLE ' . $this->prefix .
153
            '_kvstore (_key VARCHAR(50) NOT NULL, _value TEXT NOT NULL, _expire TIMESTAMP, PRIMARY KEY (_key))';
154
        $this->pdo->exec($query);
155
156
        $query = 'CREATE INDEX ' . $this->prefix . '_kvstore_expire ON ' . $this->prefix . '_kvstore (_expire)';
157
        $this->pdo->exec($query);
158
159
        $this->setTableVersion('kvstore', 1);
160
    }
161
162
163
    /**
164
     * @param string $name
165
     * @return int
166
     */
167
    private function getTableVersion(string $name): int
168
    {
169
        if (!isset($this->tableVersions[$name])) {
170
            return 0;
171
        }
172
173
        return $this->tableVersions[$name];
174
    }
175
176
177
    /**
178
     * @param string $name
179
     * @param int $version
180
     */
181
    private function setTableVersion(string $name, int $version): void
182
    {
183
        $this->insertOrUpdate(
184
            $this->prefix . '_tableVersion',
185
            ['_name'],
186
            [
187
                '_name' => $name,
188
                '_version' => $version,
189
            ],
190
        );
191
        $this->tableVersions[$name] = $version;
192
    }
193
194
195
    /**
196
     * @param string $table
197
     * @param array $keys
198
     * @param array $data
199
     */
200
    private function insertOrUpdate(string $table, array $keys, array $data): void
201
    {
202
        $colNames = '(' . implode(', ', array_keys($data)) . ')';
203
        $values = 'VALUES(:' . implode(', :', array_keys($data)) . ')';
204
205
        switch ($this->driver) {
206
            case 'mysql':
207
                $query = 'REPLACE INTO ' . $table . ' ' . $colNames . ' ' . $values;
208
                $query = $this->pdo->prepare($query);
209
                $query->execute($data);
210
                return;
211
            case 'sqlite':
212
                $query = 'INSERT OR REPLACE INTO ' . $table . ' ' . $colNames . ' ' . $values;
213
                $query = $this->pdo->prepare($query);
214
                $query->execute($data);
215
                return;
216
            default:
217
                /* Default implementation. Try INSERT, and UPDATE if that fails. */
218
219
                $insertQuery = 'INSERT INTO ' . $table . ' ' . $colNames . ' ' . $values;
220
                $insertQuery = $this->pdo->prepare($insertQuery);
221
222
                if ($insertQuery === false) {
223
                    throw new Exception("Error preparing statement.");
224
                }
225
                $this->insertOrUpdateFallback($table, $keys, $data, $insertQuery);
226
                return;
227
        }
228
    }
229
230
231
    /**
232
     * @param string $table
233
     * @param array $keys
234
     * @param array $data
235
     * @param \PDOStatement $insertQuery
236
     */
237
    private function insertOrUpdateFallback(string $table, array $keys, array $data, PDOStatement $insertQuery): void
238
    {
239
        try {
240
            $insertQuery->execute($data);
241
            return;
242
        } catch (PDOException $e) {
243
            $ecode = strval($e->getCode());
244
            switch ($ecode) {
245
                case '23505': /* PostgreSQL */
246
                    break;
247
                default:
248
                    Logger::error('casserver: Error while saving data: ' . $e->getMessage());
249
                    throw $e;
250
            }
251
        }
252
253
        $updateCols = [];
254
        $condCols = [];
255
256
        foreach ($data as $col => $value) {
257
            $tmp = $col . ' = :' . $col;
258
259
            if (in_array($col, $keys, true)) {
260
                $condCols[] = $tmp;
261
            } else {
262
                $updateCols[] = $tmp;
263
            }
264
        }
265
266
        $updateCols = implode(',', $updateCols);
267
        $condCols = implode(' AND ', $condCols);
268
        $updateQuery = 'UPDATE ' . $table . ' SET ' . $updateCols . ' WHERE ' . $condCols;
269
        $updateQuery = $this->pdo->prepare($updateQuery);
270
        $updateQuery->execute($data);
271
    }
272
273
274
    /**
275
     */
276
    private function cleanKVStore(): void
277
    {
278
        $query = 'DELETE FROM ' . $this->prefix . '_kvstore WHERE _expire < :now';
279
        $params = ['now' => gmdate('Y-m-d H:i:s')];
280
281
        $query = $this->pdo->prepare($query);
282
        $query->execute($params);
283
    }
284
285
286
    /**
287
     * @param string $key
288
     * @return array|null
289
     */
290
    private function get(string $key): ?array
291
    {
292
        if (strlen($key) > 50) {
293
            $key = sha1($key);
294
        }
295
296
        $query = 'SELECT _value FROM ' . $this->prefix .
297
            '_kvstore WHERE _key = :key AND (_expire IS NULL OR _expire > :now)';
298
        $params = ['key' => $key, 'now' => gmdate('Y-m-d H:i:s')];
299
300
        $query = $this->pdo->prepare($query);
301
        $query->execute($params);
302
303
        $row = $query->fetch(PDO::FETCH_ASSOC);
304
        if ($row === false) {
305
            return null;
306
        }
307
308
        $value = $row['_value'];
309
        if (is_resource($value)) {
310
            $value = stream_get_contents($value);
311
        }
312
        $value = urldecode($value);
313
        $value = unserialize($value);
314
315
        if ($value === false) {
316
            return null;
317
        }
318
319
        return $value;
320
    }
321
322
323
    /**
324
     * @param   string    $key
325
     * @param   array     $value
326
     * @param   int|null  $expire
327
     *
328
     * @throws Exception
329
     */
330
    private function set(string $key, array $value, int $expire = null): void
331
    {
332
        Assert::string($key);
333
        Assert::nullOrInteger($expire);
334
        Assert::greaterThan($expire, 2592000);
335
336
        if (rand(0, 1000) < 10) {
337
            $this->cleanKVStore();
338
        }
339
340
        if (strlen($key) > 50) {
341
            $key = sha1($key);
342
        }
343
344
        if ($expire !== null) {
345
            $expire = gmdate('Y-m-d H:i:s', $expire);
346
        }
347
348
        $value = serialize($value);
349
        $value = rawurlencode($value);
350
351
        $data = [
352
            '_key' => $key,
353
            '_value' => $value,
354
            '_expire' => $expire,
355
        ];
356
357
        $this->insertOrUpdate($this->prefix . '_kvstore', ['_key'], $data);
358
    }
359
360
361
    /**
362
     * @param string $key
363
     */
364
    private function delete(string $key): void
365
    {
366
        Assert::string($key);
367
368
        if (strlen($key) > 50) {
369
            $key = sha1($key);
370
        }
371
372
        $data = [
373
            '_key' => $key,
374
375
        ];
376
377
        $query = 'DELETE FROM ' . $this->prefix . '_kvstore WHERE _key=:_key';
378
        $query = $this->pdo->prepare($query);
379
        $query->execute($data);
380
    }
381
}
382