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

SQLTicketStore::addTicket()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
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
                $insertQuery = 'INSERT INTO '.$table.' '.$colNames.' '.$values;
227
                $insertQuery = $this->pdo->prepare($insertQuery);
228
229
                $this->insertOrUpdateFallback($table, $keys, $data, $insertQuery);
230
                return;
231
        }
232
    }
233
234
235
    /**
236
     * @param string $table
237
     * @param array $keys
238
     * @param array $data
239
     * @param \PDOStatement|bool $colNames
240
     * @return void
241
     */
242
    private function insertOrUpdateFallback($table, array $keys, array $data, $insertQuery)
243
    {
244
        try {
245
            $insertQuery->execute($data);
246
            return;
247
        } catch (\PDOException $e) {
248
            $ecode = strval($e->getCode());
249
            switch ($ecode) {
250
                case '23505': /* PostgreSQL */
251
                    break;
252
                default:
253
                    \SimpleSAML\Logger::error('casserver: Error while saving data: '.$e->getMessage());
254
                    throw $e;
255
            }
256
        }
257
258
        $updateCols = [];
259
        $condCols = [];
260
261
        foreach ($data as $col => $value) {
262
263
            $tmp = $col.' = :'.$col;
264
265
            if (in_array($col, $keys, true)) {
266
                $condCols[] = $tmp;
267
            } else {
268
                $updateCols[] = $tmp;
269
            }
270
        }
271
272
        $updateQuery = 'UPDATE '.$table.' SET '.implode(',', $updateCols).' WHERE '.
273
            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()
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($key)
297
    {
298
        Assert::string($key);
299
300
        if (strlen($key) > 50) {
301
            $key = sha1($key);
302
        }
303
304
        $query = 'SELECT _value FROM '.$this->prefix.
305
            '_kvstore WHERE _key = :key AND (_expire IS NULL OR _expire > :now)';
306
        $params = ['key' => $key, 'now' => gmdate('Y-m-d H:i:s')];
307
308
        $query = $this->pdo->prepare($query);
309
        $query->execute($params);
310
311
        $row = $query->fetch(\PDO::FETCH_ASSOC);
312
        if ($row === false) {
313
            return null;
314
        }
315
316
        $value = $row['_value'];
317
        if (is_resource($value)) {
318
            $value = stream_get_contents($value);
319
        }
320
        $value = urldecode($value);
321
        $value = unserialize($value);
322
323
        if ($value === false) {
324
            return null;
325
        }
326
327
        return $value;
328
    }
329
330
331
    /**
332
     * @param string $key
333
     * @param array $value
334
     * @param int|null $expire
335
     * @return void
336
     */
337
    private function set($key, $value, $expire = null)
338
    {
339
        Assert::string($key);
340
        Assert::nullOrInteger($expire);
341
        Assert::greaterThan($expire, 2592000);
342
343
        if (rand(0, 1000) < 10) {
344
            $this->cleanKVStore();
345
        }
346
347
        if (strlen($key) > 50) {
348
            $key = sha1($key);
349
        }
350
351
        if ($expire !== null) {
352
            $expire = gmdate('Y-m-d H:i:s', $expire);
353
        }
354
355
        $value = serialize($value);
356
        $value = rawurlencode($value);
357
358
        $data = [
359
            '_key' => $key,
360
            '_value' => $value,
361
            '_expire' => $expire,
362
        ];
363
364
        $this->insertOrUpdate($this->prefix.'_kvstore', ['_key'], $data);
365
    }
366
367
368
    /**
369
     * @param string $key
370
     * @return void
371
     */
372
    private function delete($key)
373
    {
374
        Assert::string($key);
375
376
        if (strlen($key) > 50) {
377
            $key = sha1($key);
378
        }
379
380
        $data = [
381
            '_key' => $key,
382
383
        ];
384
385
        $query = 'DELETE FROM '.$this->prefix.'_kvstore WHERE _key=:_key';
386
        $query = $this->pdo->prepare($query);
387
        $query->execute($data);
388
    }
389
}
390