Passed
Pull Request — master (#5)
by Tim
01:33
created

SQLTicketStore::delete()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 1
dl 0
loc 16
rs 10
c 0
b 0
f 0
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 SimpleSAML\Configuration;
27
use Webmozart\Assert\Assert;
28
29
class SQLTicketStore extends TicketStore
30
{
31
    /** @var \PDO $pdo */
32
    public $pdo;
33
34
    /** @var string $driver */
35
    public $driver = 'pdo';
36
37
    /** @var string $prefix */
38
    public $prefix;
39
40
    /** @var array $tableVersions */
41
    private $tableVersions = [];
42
43
44
    /**
45
     * @param \SimpleSAML\Configuration $config
46
     */
47
    public function __construct(Configuration $config)
48
    {
49
        parent::__construct($config);
50
51
        /** @var \SimpleSAML\Configuration $storeConfig */
52
        $storeConfig = $config->getConfigItem('ticketstore');
53
        $dsn = $storeConfig->getString('dsn');
54
        $username = $storeConfig->getString('username');
55
        $password = $storeConfig->getString('password');
56
        $options = $storeConfig->getArray('options', []);
57
        $this->prefix = $storeConfig->getString('prefix', '');
58
59
        $this->pdo = new \PDO($dsn, $username, $password, $options);
60
        $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
61
62
        $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
63
64
        if ($this->driver === 'mysql') {
65
            $this->pdo->exec('SET time_zone = "+00:00"');
66
        }
67
68
        $this->initTableVersionTable();
69
        $this->initKVTable();
70
    }
71
72
73
    /**
74
     * @param string $ticketId
75
     * @return array|null
76
     */
77
    public function getTicket($ticketId)
78
    {
79
        $scopedTicketId = $this->scopeTicketId($ticketId);
80
81
        return $this->get($scopedTicketId);
82
    }
83
84
85
    /**
86
     * @param array $ticket
87
     * @return void
88
     */
89
    public function addTicket(array $ticket)
90
    {
91
        $scopedTicketId = $this->scopeTicketId($ticket['id']);
92
93
        $this->set($scopedTicketId, $ticket, $ticket['validBefore']);
94
    }
95
96
97
    /**
98
     * @param string $ticketId
99
     * @return void
100
     */
101
    public function deleteTicket($ticketId)
102
    {
103
        $scopedTicketId = $this->scopeTicketId($ticketId);
104
105
        $this->delete($scopedTicketId);
106
    }
107
108
109
    /**
110
     * @param string $ticketId
111
     * @return string
112
     */
113
    private function scopeTicketId($ticketId)
114
    {
115
        return $this->prefix.'.'.$ticketId;
116
    }
117
118
119
    /**
120
     * @return void
121
     */
122
    private function initTableVersionTable()
123
    {
124
125
        $this->tableVersions = [];
126
127
        try {
128
            $fetchTableVersion = $this->pdo->query('SELECT _name, _version FROM '.$this->prefix.'_tableVersion');
129
        } catch (\PDOException $e) {
130
            $this->pdo->exec('CREATE TABLE '.$this->prefix.
131
                '_tableVersion (_name VARCHAR(30) NOT NULL UNIQUE, _version INTEGER NOT NULL)');
132
            return;
133
        }
134
135
        while (($row = $fetchTableVersion->fetch(\PDO::FETCH_ASSOC)) !== false) {
136
            $this->tableVersions[$row['_name']] = intval($row['_version']);
137
        }
138
    }
139
140
141
    /**
142
     * @return void
143
     */
144
    private function initKVTable()
145
    {
146
        if ($this->getTableVersion('kvstore') === 1) {
147
            /* Table initialized. */
148
            return;
149
        }
150
151
        $query = 'CREATE TABLE '.$this->prefix.
152
            '_kvstore (_key VARCHAR(50) NOT NULL, _value TEXT NOT NULL, _expire TIMESTAMP, PRIMARY KEY (_key))';
153
        $this->pdo->exec($query);
154
155
        $query = 'CREATE INDEX '.$this->prefix.'_kvstore_expire ON '.$this->prefix.'_kvstore (_expire)';
156
        $this->pdo->exec($query);
157
158
        $this->setTableVersion('kvstore', 1);
159
    }
160
161
162
    /**
163
     * @param string $name
164
     * @return int
165
     */
166
    private function getTableVersion($name)
167
    {
168
        Assert::string($name);
169
170
        if (!isset($this->tableVersions[$name])) {
171
            return 0;
172
        }
173
174
        return $this->tableVersions[$name];
175
    }
176
177
178
    /**
179
     * @param string $name
180
     * @param int $version
181
     * @return void
182
     */
183
    private function setTableVersion($name, $version)
184
    {
185
        Assert::string($name);
186
        Assert::integer($version);
187
188
        $this->insertOrUpdate(
189
            $this->prefix.'_tableVersion',
190
            ['_name'],
191
            [
192
                '_name' => $name,
193
                '_version' => $version
194
            ]
195
        );
196
        $this->tableVersions[$name] = $version;
197
    }
198
199
200
    /**
201
     * @param string $table
202
     * @param array $keys
203
     * @param array $data
204
     * @return void
205
     */
206
    private function insertOrUpdate($table, array $keys, array $data)
207
    {
208
        Assert::string($table);
209
210
        $colNames = '('.implode(', ', array_keys($data)).')';
211
        $values = 'VALUES(:'.implode(', :', array_keys($data)).')';
212
213
        switch ($this->driver) {
214
            case 'mysql':
215
                $query = 'REPLACE INTO '.$table.' '.$colNames.' '.$values;
216
                $query = $this->pdo->prepare($query);
217
                $query->execute($data);
218
                return;
219
            case 'sqlite':
220
                $query = 'INSERT OR REPLACE INTO '.$table.' '.$colNames.' '.$values;
221
                $query = $this->pdo->prepare($query);
222
                $query->execute($data);
223
                return;
224
            default:
225
                /* Default implementation. Try INSERT, and UPDATE if that fails. */
226
227
                $insertQuery = 'INSERT INTO '.$table.' '.$colNames.' '.$values;
228
                $insertQuery = $this->pdo->prepare($insertQuery);
229
230
                $this->insertOrUpdateFallback($table, $keys, $data, $insertQuery);
231
                return;
232
        }
233
234
    }
235
236
237
    /**
238
     * @param string $table
239
     * @param array $keys
240
     * @param array $data
241
     * @param \PDOStatement|bool $insertQuery
242
     * @return void
243
     */
244
    private function insertOrUpdateFallback($table, array $keys, array $data, $insertQuery)
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
                    \SimpleSAML\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
265
            $tmp = $col.' = :'.$col;
266
267
            if (in_array($col, $keys, true)) {
268
                $condCols[] = $tmp;
269
            } else {
270
                $updateCols[] = $tmp;
271
            }
272
        }
273
274
        $updateQuery = 'UPDATE '.$table.' SET '.implode(',', $updateCols).' WHERE '.implode(' AND ', $condCols);
275
        $updateQuery = $this->pdo->prepare($updateQuery);
276
        $updateQuery->execute($data);
277
    }
278
279
280
    /**
281
     * @return void
282
     */
283
    private function cleanKVStore()
284
    {
285
        $query = 'DELETE FROM '.$this->prefix.'_kvstore WHERE _expire < :now';
286
        $params = ['now' => gmdate('Y-m-d H:i:s')];
287
288
        $query = $this->pdo->prepare($query);
289
        $query->execute($params);
290
    }
291
292
293
    /**
294
     * @param string $key
295
     * @return array|null
296
     */
297
    private function get($key)
298
    {
299
        Assert::string($key);
300
301
        if (strlen($key) > 50) {
302
            $key = sha1($key);
303
        }
304
305
        $query = 'SELECT _value FROM '.$this->prefix.
306
            '_kvstore WHERE _key = :key AND (_expire IS NULL OR _expire > :now)';
307
        $params = ['key' => $key, 'now' => gmdate('Y-m-d H:i:s')];
308
309
        $query = $this->pdo->prepare($query);
310
        $query->execute($params);
311
312
        $row = $query->fetch(\PDO::FETCH_ASSOC);
313
        if ($row === false) {
314
            return null;
315
        }
316
317
        $value = $row['_value'];
318
        if (is_resource($value)) {
319
            $value = stream_get_contents($value);
320
        }
321
        $value = urldecode($value);
322
        $value = unserialize($value);
323
324
        if ($value === false) {
325
            return null;
326
        }
327
328
        return $value;
329
    }
330
331
332
    /**
333
     * @param string $key
334
     * @param array $value
335
     * @param int|null $expire
336
     * @return void
337
     */
338
    private function set($key, $value, $expire = null)
339
    {
340
        Assert::string($key);
341
        Assert::nullOrInteger($expire);
342
        Assert::greaterThan($expire, 2592000);
343
344
        if (rand(0, 1000) < 10) {
345
            $this->cleanKVStore();
346
        }
347
348
        if (strlen($key) > 50) {
349
            $key = sha1($key);
350
        }
351
352
        if ($expire !== null) {
353
            $expire = gmdate('Y-m-d H:i:s', $expire);
354
        }
355
356
        $value = serialize($value);
357
        $value = rawurlencode($value);
358
359
        $data = [
360
            '_key' => $key,
361
            '_value' => $value,
362
            '_expire' => $expire,
363
        ];
364
365
        $this->insertOrUpdate($this->prefix.'_kvstore', ['_key'], $data);
366
    }
367
368
369
    /**
370
     * @param string $key
371
     * @return void
372
     */
373
    private function delete($key)
374
    {
375
        Assert::string($key);
376
377
        if (strlen($key) > 50) {
378
            $key = sha1($key);
379
        }
380
381
        $data = [
382
            '_key' => $key,
383
384
        ];
385
386
        $query = 'DELETE FROM '.$this->prefix.'_kvstore WHERE _key=:_key';
387
        $query = $this->pdo->prepare($query);
388
        $query->execute($data);
389
    }
390
}
391