Passed
Pull Request — master (#5)
by Tim
01:39
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
                $this->insertOrUpdateFallback($table, $keys, $data);
227
                return;
228
        }
229
    }
230
231
232
    /**
233
     * @param string $table
234
     * @param array $keys
235
     * @param array $data
236
     * @return void
237
     */
238
    private function insertOrUpdateFallback($table, array $keys, array $data)
239
    {
240
        $insertQuery = 'INSERT INTO '.$table.' '.$colNames.' '.$values;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $values seems to be never defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable $colNames seems to be never defined.
Loading history...
241
        $insertQuery = $this->pdo->prepare($insertQuery);
242
        try {
243
            $insertQuery->execute($data);
244
            return;
245
        } catch (\PDOException $e) {
246
            $ecode = strval($e->getCode());
247
            switch ($ecode) {
248
                case '23505': /* PostgreSQL */
249
                    break;
250
                default:
251
                    \SimpleSAML\Logger::error('casserver: Error while saving data: '.$e->getMessage());
252
                    throw $e;
253
            }
254
        }
255
256
        $updateCols = [];
257
        $condCols = [];
258
259
        foreach ($data as $col => $value) {
260
261
            $tmp = $col.' = :'.$col;
262
263
            if (in_array($col, $keys, true)) {
264
                $condCols[] = $tmp;
265
            } else {
266
                $updateCols[] = $tmp;
267
            }
268
        }
269
270
        $updateQuery = 'UPDATE '.$table.' SET '.implode(',', $updateCols).' WHERE '.
271
            implode(' AND ', $condCols);
272
        $updateQuery = $this->pdo->prepare($updateQuery);
273
        $updateQuery->execute($data);
274
    }
275
276
277
    /**
278
     * @return void
279
     */
280
    private function cleanKVStore()
281
    {
282
        $query = 'DELETE FROM '.$this->prefix.'_kvstore WHERE _expire < :now';
283
        $params = ['now' => gmdate('Y-m-d H:i:s')];
284
285
        $query = $this->pdo->prepare($query);
286
        $query->execute($params);
287
    }
288
289
290
    /**
291
     * @param string $key
292
     * @return array|null
293
     */
294
    private function get($key)
295
    {
296
        Assert::string($key);
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($key, $value, $expire = null)
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($key)
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