Completed
Push — master ( 17843f...6b9e14 )
by Tim
03:37 queued 01:40
created

lib/Cas/Ticket/SQLTicketStore.php (3 issues)

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
namespace SimpleSAML\Module\casserver\Cas\Ticket;
25
26
use Exception;
27
use PDO;
28
use PDOException;
29
use PDOStatement;
30
use SimpleSAML\Configuration;
31
use SimpleSAML\Logger;
32
use Webmozart\Assert\Assert;
33
34
class SQLTicketStore extends TicketStore
35
{
36
    /** @var \PDO $pdo */
37
    public $pdo;
38
39
    /** @var string $driver */
40
    public $driver = 'pdo';
41
42
    /** @var string $prefix */
43
    public $prefix;
44
45
    /** @var array $tableVersions */
46
    private $tableVersions = [];
47
48
49
    /**
50
     * @param \SimpleSAML\Configuration $config
51
     */
52
    public function __construct(Configuration $config)
53
    {
54
        parent::__construct($config);
55
56
        /** @var \SimpleSAML\Configuration $storeConfig */
57
        $storeConfig = $config->getConfigItem('ticketstore');
58
        $dsn = $storeConfig->getString('dsn');
59
        $username = $storeConfig->getString('username');
60
        $password = $storeConfig->getString('password');
61
        $options = $storeConfig->getArray('options', []);
62
        $this->prefix = $storeConfig->getString('prefix', '');
63
64
        $this->pdo = new PDO($dsn, $username, $password, $options);
65
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
66
67
        $this->driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
68
69
        if ($this->driver === 'mysql') {
70
            $this->pdo->exec('SET time_zone = "+00:00"');
71
        }
72
73
        $this->initTableVersionTable();
74
        $this->initKVTable();
75
    }
76
77
78
    /**
79
     * @param string $ticketId
80
     * @return array|null
81
     */
82
    public function getTicket(string $ticketId): ?array
83
    {
84
        $scopedTicketId = $this->scopeTicketId($ticketId);
85
86
        return $this->get($scopedTicketId);
87
    }
88
89
90
    /**
91
     * @param array $ticket
92
     * @return void
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
     * @return void
105
     */
106
    public function deleteTicket(string $ticketId): void
107
    {
108
        $scopedTicketId = $this->scopeTicketId($ticketId);
109
110
        $this->delete($scopedTicketId);
111
    }
112
113
114
    /**
115
     * @param string $ticketId
116
     * @return string
117
     */
118
    private function scopeTicketId(string $ticketId): string
119
    {
120
        return $this->prefix . '.' . $ticketId;
121
    }
122
123
124
    /**
125
     * @return void
126
     */
127
    private function initTableVersionTable(): void
128
    {
129
        $this->tableVersions = [];
130
131
        try {
132
            $fetchTableVersion = $this->pdo->query('SELECT _name, _version FROM ' . $this->prefix . '_tableVersion');
133
        } catch (PDOException $e) {
134
            $this->pdo->exec('CREATE TABLE ' . $this->prefix .
135
                '_tableVersion (_name VARCHAR(30) NOT NULL UNIQUE, _version INTEGER NOT NULL)');
136
            return;
137
        }
138
139
        while (($row = $fetchTableVersion->fetch(PDO::FETCH_ASSOC)) !== false) {
140
            $this->tableVersions[$row['_name']] = intval($row['_version']);
141
        }
142
    }
143
144
145
    /**
146
     * @return void
147
     */
148
    private function initKVTable(): void
149
    {
150
        if ($this->getTableVersion('kvstore') === 1) {
0 ignored issues
show
The condition $this->getTableVersion('kvstore') === 1 is always false.
Loading history...
Are you sure the usage of $this->getTableVersion('kvstore') targeting SimpleSAML\Module\casser...tore::getTableVersion() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
151
            /* Table initialized. */
152
            return;
153
        }
154
155
        $query = 'CREATE TABLE ' . $this->prefix .
156
            '_kvstore (_key VARCHAR(50) NOT NULL, _value TEXT NOT NULL, _expire TIMESTAMP, PRIMARY KEY (_key))';
157
        $this->pdo->exec($query);
158
159
        $query = 'CREATE INDEX ' . $this->prefix . '_kvstore_expire ON ' . $this->prefix . '_kvstore (_expire)';
160
        $this->pdo->exec($query);
161
162
        $this->setTableVersion('kvstore', 1);
163
    }
164
165
166
    /**
167
     * @param string $name
168
     * @return int
169
     */
170
    private function getTableVersion(string $name): void
171
    {
172
        if (!isset($this->tableVersions[$name])) {
173
            return 0;
0 ignored issues
show
Bug Best Practice introduced by
The expression return 0 returns the type integer which is incompatible with the type-hinted return void.
Loading history...
174
        }
175
176
        return $this->tableVersions[$name];
177
    }
178
179
180
    /**
181
     * @param string $name
182
     * @param int $version
183
     * @return void
184
     */
185
    private function setTableVersion(string $name, int $version): void
186
    {
187
        $this->insertOrUpdate(
188
            $this->prefix . '_tableVersion',
189
            ['_name'],
190
            [
191
                '_name' => $name,
192
                '_version' => $version
193
            ]
194
        );
195
        $this->tableVersions[$name] = $version;
196
    }
197
198
199
    /**
200
     * @param string $table
201
     * @param array $keys
202
     * @param array $data
203
     * @return void
204
     */
205
    private function insertOrUpdate(string $table, array $keys, array $data): void
206
    {
207
        $colNames = '(' . implode(', ', array_keys($data)) . ')';
208
        $values = 'VALUES(:' . implode(', :', array_keys($data)) . ')';
209
210
        switch ($this->driver) {
211
            case 'mysql':
212
                $query = 'REPLACE INTO ' . $table . ' ' . $colNames . ' ' . $values;
213
                $query = $this->pdo->prepare($query);
214
                $query->execute($data);
215
                return;
216
            case 'sqlite':
217
                $query = 'INSERT OR REPLACE INTO ' . $table . ' ' . $colNames . ' ' . $values;
218
                $query = $this->pdo->prepare($query);
219
                $query->execute($data);
220
                return;
221
            default:
222
                /* Default implementation. Try INSERT, and UPDATE if that fails. */
223
224
                $insertQuery = 'INSERT INTO ' . $table . ' ' . $colNames . ' ' . $values;
225
                /** @var \PDOStatement|false $insertQuery */
226
                $insertQuery = $this->pdo->prepare($insertQuery);
227
228
                if ($insertQuery === false) {
229
                    throw new Exception("Error preparing statement.");
230
                }
231
                $this->insertOrUpdateFallback($table, $keys, $data, $insertQuery);
232
                return;
233
        }
234
    }
235
236
237
    /**
238
     * @param string $table
239
     * @param array $keys
240
     * @param array $data
241
     * @param \PDOStatement $insertQuery
242
     * @return void
243
     */
244
    private function insertOrUpdateFallback(string $table, array $keys, array $data, PDOStatement $insertQuery): void
245
    {
246
        try {
247
            $insertQuery->execute($data);
248
            return;
249
        } catch (PDOException $e) {
250
            $ecode = strval($e->getCode());
251
            switch ($ecode) {
252
                case '23505': /* PostgreSQL */
253
                    break;
254
                default:
255
                    Logger::error('casserver: Error while saving data: ' . $e->getMessage());
256
                    throw $e;
257
            }
258
        }
259
260
        $updateCols = [];
261
        $condCols = [];
262
263
        foreach ($data as $col => $value) {
264
            $tmp = $col . ' = :' . $col;
265
266
            if (in_array($col, $keys, true)) {
267
                $condCols[] = $tmp;
268
            } else {
269
                $updateCols[] = $tmp;
270
            }
271
        }
272
273
        $updateQuery = 'UPDATE ' . $table . ' SET ' . implode(',', $updateCols) . ' WHERE ' . implode(' AND ', $condCols);
274
        $updateQuery = $this->pdo->prepare($updateQuery);
275
        $updateQuery->execute($data);
276
    }
277
278
279
    /**
280
     * @return void
281
     */
282
    private function cleanKVStore(): void
283
    {
284
        $query = 'DELETE FROM ' . $this->prefix . '_kvstore WHERE _expire < :now';
285
        $params = ['now' => gmdate('Y-m-d H:i:s')];
286
287
        $query = $this->pdo->prepare($query);
288
        $query->execute($params);
289
    }
290
291
292
    /**
293
     * @param string $key
294
     * @return array|null
295
     */
296
    private function get(string $key): ?array
297
    {
298
        if (strlen($key) > 50) {
299
            $key = sha1($key);
300
        }
301
302
        $query = 'SELECT _value FROM ' . $this->prefix .
303
            '_kvstore WHERE _key = :key AND (_expire IS NULL OR _expire > :now)';
304
        $params = ['key' => $key, 'now' => gmdate('Y-m-d H:i:s')];
305
306
        $query = $this->pdo->prepare($query);
307
        $query->execute($params);
308
309
        $row = $query->fetch(PDO::FETCH_ASSOC);
310
        if ($row === false) {
311
            return null;
312
        }
313
314
        $value = $row['_value'];
315
        if (is_resource($value)) {
316
            $value = stream_get_contents($value);
317
        }
318
        $value = urldecode($value);
319
        $value = unserialize($value);
320
321
        if ($value === false) {
322
            return null;
323
        }
324
325
        return $value;
326
    }
327
328
329
    /**
330
     * @param string $key
331
     * @param array $value
332
     * @param int|null $expire
333
     * @return void
334
     */
335
    private function set(string $key, array $value, int $expire = null): void
336
    {
337
        Assert::string($key);
338
        Assert::nullOrInteger($expire);
339
        Assert::greaterThan($expire, 2592000);
340
341
        if (rand(0, 1000) < 10) {
342
            $this->cleanKVStore();
343
        }
344
345
        if (strlen($key) > 50) {
346
            $key = sha1($key);
347
        }
348
349
        if ($expire !== null) {
350
            $expire = gmdate('Y-m-d H:i:s', $expire);
351
        }
352
353
        $value = serialize($value);
354
        $value = rawurlencode($value);
355
356
        $data = [
357
            '_key' => $key,
358
            '_value' => $value,
359
            '_expire' => $expire,
360
        ];
361
362
        $this->insertOrUpdate($this->prefix . '_kvstore', ['_key'], $data);
363
    }
364
365
366
    /**
367
     * @param string $key
368
     * @return void
369
     */
370
    private function delete(string $key): void
371
    {
372
        Assert::string($key);
373
374
        if (strlen($key) > 50) {
375
            $key = sha1($key);
376
        }
377
378
        $data = [
379
            '_key' => $key,
380
381
        ];
382
383
        $query = 'DELETE FROM ' . $this->prefix . '_kvstore WHERE _key=:_key';
384
        $query = $this->pdo->prepare($query);
385
        $query->execute($data);
386
    }
387
}
388