Passed
Push — main ( 5f9dd5...61380c )
by Andreas
01:44
created

PDOCrateDB::quote()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 27
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 19
nc 6
nop 2
dl 0
loc 27
ccs 18
cts 18
cp 1
crap 6
rs 9.0111
c 0
b 0
f 0
1
<?php
2
/**
3
 * Licensed to CRATE Technology GmbH("Crate") under one or more contributor
4
 * license agreements.  See the NOTICE file distributed with this work for
5
 * additional information regarding copyright ownership.  Crate licenses
6
 * this file to you under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.  You may
8
 * obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
15
 * License for the specific language governing permissions and limitations
16
 * under the License.
17
 *
18
 * However, if you have executed another commercial license agreement
19
 * with Crate these terms will supersede the license and you may use the
20
 * software solely pursuant to the terms of the relevant commercial agreement.
21
 */
22
23
declare(strict_types=1);
24
25
namespace Crate\PDO;
26
27
use Crate\PDO\Exception\InvalidArgumentException;
28
use Crate\PDO\Exception\PDOException;
29
use Crate\PDO\Http\ServerInterface;
30
use Crate\PDO\Http\ServerPool;
31
use Crate\Stdlib\ArrayUtils;
32
use PDO as BasePDO;
33
34
use const PHP_VERSION_ID;
35
36
class PDOCrateDB extends BasePDO implements PDOInterface
37
{
38
    use PDOImplementation;
39
40
    public const VERSION     = '2.2.3';
41
    public const DRIVER_NAME = 'crate';
42
43
    public const DSN_REGEX = '/^(?:crate:)(?:((?:[\w\.-]+:\d+\,?)+))\/?([\w]+)?$/';
44
45
    public const CRATE_ATTR_HTTP_BASIC_AUTH = 1000;
46
    public const CRATE_ATTR_DEFAULT_SCHEMA  = 1001;
47
48
    public const CRATE_ATTR_SSL_MODE                                       = 1008;
49
    public const CRATE_ATTR_SSL_MODE_DISABLED                              = 1;
50
    public const CRATE_ATTR_SSL_MODE_ENABLED_BUT_WITHOUT_HOST_VERIFICATION = 2;
51
    public const CRATE_ATTR_SSL_MODE_REQUIRED                              = 3;
52
53
    public const CRATE_ATTR_SSL_KEY_PATH      = 1002;
54
    public const CRATE_ATTR_SSL_KEY_PASSWORD  = 1003;
55
    public const CRATE_ATTR_SSL_CERT_PATH     = 1004;
56
    public const CRATE_ATTR_SSL_CERT_PASSWORD = 1005;
57
    public const CRATE_ATTR_SSL_CA_PATH       = 1006;
58
    public const CRATE_ATTR_SSL_CA_PASSWORD   = 1007;
59
60
    public const PARAM_FLOAT     = 6;
61
    public const PARAM_DOUBLE    = 7;
62
    public const PARAM_LONG      = 8;
63
    public const PARAM_ARRAY     = 9;
64
    public const PARAM_OBJECT    = 10;
65
    public const PARAM_TIMESTAMP = 11;
66
    public const PARAM_IP        = 12;
67
68
    /**
69
     * @var array
70
     */
71
    private $attributes = [
72
        'defaultFetchMode' => self::FETCH_BOTH,
73
        'errorMode'        => self::ERRMODE_SILENT,
74
        'sslMode'          => self::CRATE_ATTR_SSL_MODE_DISABLED,
75
        'statementClass'   => PDOStatement::class,
76
        'timeout'          => 0.0,
77
        'auth'             => [],
78
        'defaultSchema'    => 'doc',
79
        'bulkMode'         => false,
80
    ];
81
82
    /**
83
     * @var Http\ServerInterface
84
     */
85
    private $server;
86
87
    /**
88
     * @var PDOStatement|null
89
     */
90
    private $lastStatement;
91
92
    /**
93
     * @var callable
94
     */
95
    private $request;
96
97
    /**
98
     * {@inheritDoc}
99
     *
100
     * @param string     $dsn      The HTTP endpoint to call
101
     * @param null       $username Username for basic auth
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $username is correct as it would always require null to be passed?
Loading history...
102
     * @param null       $passwd   Password for basic auth
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $passwd is correct as it would always require null to be passed?
Loading history...
103
     * @param null|array $options  Attributes to set on the PDO
104
     */
105 36
    public function __construct($dsn, $username = null, $passwd = null, $options = [])
106
    {
107
108 36
        if (PHP_VERSION_ID < 80000) {
109
            trigger_error(
110
                "`crate/crate-pdo` will stop supporting PHP7 on one of the upcoming " .
111
                "releases. Please upgrade to PHP8.",
112
                E_USER_DEPRECATED
113
            );
114
        }
115
116 36
        $dsnParts = self::parseDSN($dsn);
117 36
        $servers  = self::serversFromDsnParts($dsnParts);
118
119 36
        $this->setServer(new ServerPool($servers));
120
121 36
        foreach ((array)$options as $attribute => $value) {
122 2
            $this->setAttribute($attribute, $value);
123
        }
124
125 36
        if (!empty($username)) {
126 4
            $this->setAttribute(self::CRATE_ATTR_HTTP_BASIC_AUTH, [$username, $passwd]);
127
        }
128
129 36
        if (!empty($dsnParts[1])) {
130
            $this->setAttribute(self::CRATE_ATTR_DEFAULT_SCHEMA, $dsnParts[1]);
131
        }
132
133
        // Define a callback that will be used in the PDOStatements
134
        // This way we don't expose this as a public api to the end users.
135 36
        $this->request = function (PDOStatement $statement, $sql, array $parameters) {
136
137 2
            $this->lastStatement = $statement;
138
139
            try {
140 2
                if ($statement->isBulkMode()) {
141
                    return $this->server->executeBulk($sql, $parameters);
142
                } else {
143 2
                    return $this->server->execute($sql, $parameters);
144
                }
145
            } catch (Exception\RuntimeException $e) {
146
                if ($this->getAttribute(self::ATTR_ERRMODE) === self::ERRMODE_EXCEPTION) {
147
                    throw new Exception\PDOException($e->getMessage(), $e->getCode());
148
                }
149
150
                if ($this->getAttribute(self::ATTR_ERRMODE) === self::ERRMODE_WARNING) {
151
                    trigger_error(sprintf('[%d] %s', $e->getCode(), $e->getMessage()), E_USER_WARNING);
152
                }
153
154
                // should probably wrap this in a error object ?
155
                return [
156
                    'code'    => $e->getCode(),
157
                    'message' => $e->getMessage(),
158
                ];
159
            }
160 36
        };
161
    }
162
163
    /**
164
     * Change the server implementation
165
     *
166
     * @param ServerInterface $server
167
     */
168 30
    public function setServer(ServerInterface $server): void
169
    {
170 30
        $this->server = $server;
171 30
        $this->server->configure($this);
172
    }
173
174
    /**
175
     * Extract servers and optional custom schema from DSN string
176
     *
177
     * @param string $dsn The DSN string
178
     *
179
     * @throws \Crate\PDO\Exception\PDOException on an invalid DSN string
180
     *
181
     * @return array An array of ['host:post,host:port,...', 'schema']
182
     */
183 110
    private static function parseDSN($dsn)
184
    {
185 110
        $matches = [];
186
187 110
        if (!preg_match(static::DSN_REGEX, $dsn, $matches)) {
188 8
            throw new PDOException(sprintf('Invalid DSN %s', $dsn));
189
        }
190
191 102
        return array_slice($matches, 1);
192
    }
193
194
    /**
195
     * Extract host:port pairs out of the DSN parts
196
     *
197
     * @param array $dsnParts The parts of the parsed DSN string
198
     *
199
     * @return array An array of host:port strings
200
     */
201 100
    private static function serversFromDsnParts($dsnParts)
202
    {
203 100
        return explode(',', trim($dsnParts[0], ','));
204
    }
205
206
    /**
207
     * {@inheritDoc}
208
     */
209 4
    #[\ReturnTypeWillChange]
210
    public function prepare($statement, $options = null)
211
    {
212 4
        $options = ArrayUtils::toArray($options);
213
214 4
        if (isset($options[self::ATTR_CURSOR])) {
215
            trigger_error(sprintf('%s not supported', __METHOD__), E_USER_WARNING);
216
217
            return true;
218
        }
219
220 4
        $className = $this->attributes['statementClass'];
221
222 4
        return new $className($this, $this->request, $statement, $options);
223
    }
224
225
    /**
226
     * {@inheritDoc}
227
     */
228 2
    public function beginTransaction(): bool
229
    {
230 2
        return true;
231
    }
232
233
    /**
234
     * {@inheritDoc}
235
     */
236 2
    public function commit(): bool
237
    {
238 2
        return true;
239
    }
240
241
    /**
242
     * {@inheritDoc}
243
     */
244 2
    public function rollBack(): bool
245
    {
246 2
        throw new Exception\UnsupportedException;
247
    }
248
249
    /**
250
     * {@inheritDoc}
251
     */
252 2
    public function inTransaction(): bool
253
    {
254 2
        return false;
255
    }
256
257
    /**
258
     * {@inheritDoc}
259
     */
260
    #[\ReturnTypeWillChange]
261
    public function exec($statement)
262
    {
263
        $statement = $this->prepare($statement);
264
        $result    = $statement->execute();
265
266
        return $result === false ? false : $statement->rowCount();
267
    }
268
269
    /**
270
     * {@inheritDoc}
271
     */
272 2
    public function doQuery($statement, ?int $fetchMode = null, ...$fetchModeArgs)
273
    {
274 2
        $statement = $this->prepare($statement);
275 2
        if ($fetchMode !== null) {
276
            $statement->setFetchMode($fetchMode, ...$fetchModeArgs);
277
        }
278
279 2
        $result    = $statement->execute();
280
281 2
        return $result === false ? false : $statement;
282
    }
283
284
    /**
285
     * {@inheritDoc}
286
     */
287 2
    public function lastInsertId($name = null): string
288
    {
289 2
        throw new Exception\UnsupportedException;
290
    }
291
292
    /**
293
     * {@inheritDoc}
294
     */
295
    public function errorCode(): ?string
296
    {
297
        return $this->lastStatement === null ? null : $this->lastStatement->errorCode();
298
    }
299
300
    /**
301
     * {@inheritDoc}
302
     */
303
    public function errorInfo(): array
304
    {
305
        return $this->lastStatement === null ? ["00000", null, null] : $this->lastStatement->errorInfo();
306
    }
307
308
    /**
309
     * {@inheritDoc}
310
     *
311
     * @throws \Crate\PDO\Exception\PDOException
312
     * @throws \Crate\PDO\Exception\InvalidArgumentException
313
     */
314 32
    #[\ReturnTypeWillChange]
315
    public function setAttribute($attribute, $value)
316
    {
317
        switch ($attribute) {
318 32
            case self::ATTR_DEFAULT_FETCH_MODE:
319 2
                $this->attributes['defaultFetchMode'] = $value;
320 2
                break;
321
322 30
            case self::ATTR_ERRMODE:
323 2
                $this->attributes['errorMode'] = $value;
324 2
                break;
325
326 28
            case self::ATTR_STATEMENT_CLASS:
327 2
                $this->attributes['statementClass'] = $value;
328 2
                break;
329
330 26
            case self::ATTR_TIMEOUT:
331 4
                $this->attributes['timeout'] = (int)$value;
332 4
                break;
333
334 24
            case self::CRATE_ATTR_HTTP_BASIC_AUTH:
335 4
                if (!is_array($value) && $value !== null) {
336
                    throw new InvalidArgumentException(
337
                        'Value probided to CRATE_ATTR_HTTP_BASIC_AUTH must be null or an array'
338
                    );
339
                }
340
341 4
                $this->attributes['auth'] = $value;
342 4
                break;
343
344 24
            case self::CRATE_ATTR_DEFAULT_SCHEMA:
345 4
                $this->attributes['defaultSchema'] = $value;
346 4
                break;
347
348 20
            case self::CRATE_ATTR_SSL_MODE:
349 18
                $this->attributes['sslMode'] = $value;
350 18
                break;
351
352 14
            case self::CRATE_ATTR_SSL_CA_PATH:
353 4
                $this->attributes['sslCa'] = $value;
354 4
                break;
355
356 12
            case self::CRATE_ATTR_SSL_CA_PASSWORD:
357 2
                $this->attributes['sslCaPassword'] = $value;
358 2
                break;
359
360 10
            case self::CRATE_ATTR_SSL_CERT_PATH:
361 4
                $this->attributes['sslCert'] = $value;
362 4
                break;
363
364 8
            case self::CRATE_ATTR_SSL_CERT_PASSWORD:
365 2
                $this->attributes['sslCertPassword'] = $value;
366 2
                break;
367
368 6
            case self::CRATE_ATTR_SSL_KEY_PATH:
369 4
                $this->attributes['sslKey'] = $value;
370 4
                break;
371
372 4
            case self::CRATE_ATTR_SSL_KEY_PASSWORD:
373 2
                $this->attributes['sslKeyPassword'] = $value;
374 2
                break;
375
376
            default:
377 2
                throw new Exception\PDOException('Unsupported driver attribute');
378
        }
379
380
        // A setting changed so we need to reconfigure the server pool
381 30
        $this->server->configure($this);
382 30
        return true;
383
    }
384
385
    /**
386
     * {@inheritDoc}
387
     *
388
     * @throws \Crate\PDO\Exception\PDOException
389
     */
390 52
    #[\ReturnTypeWillChange]
391
    public function getAttribute($attribute)
392
    {
393
        switch ($attribute) {
394 52
            case self::ATTR_PREFETCH:
395 52
            case self::ATTR_PERSISTENT:
396 4
                return false;
397
398 52
            case self::ATTR_CLIENT_VERSION:
399 2
                return self::VERSION;
400
401 52
            case self::ATTR_SERVER_VERSION:
402
                return $this->server->getServerVersion();
403
404 52
            case self::ATTR_SERVER_INFO:
405
                return $this->server->getServerInfo();
406
407 52
            case self::ATTR_TIMEOUT:
408 52
                return $this->attributes['timeout'];
409
410 52
            case self::CRATE_ATTR_HTTP_BASIC_AUTH:
411 52
                return $this->attributes['auth'];
412
413 52
            case self::ATTR_DEFAULT_FETCH_MODE:
414 4
                return $this->attributes['defaultFetchMode'];
415
416 52
            case self::ATTR_ERRMODE:
417 2
                return $this->attributes['errorMode'];
418
419 52
            case self::ATTR_DRIVER_NAME:
420 2
                return static::DRIVER_NAME;
421
422 52
            case self::ATTR_STATEMENT_CLASS:
423 2
                return [$this->attributes['statementClass']];
424
425 52
            case self::CRATE_ATTR_DEFAULT_SCHEMA:
426 52
                return $this->attributes['defaultSchema'];
427
428 52
            case self::CRATE_ATTR_SSL_MODE:
429 52
                return $this->attributes['sslMode'];
430
431 52
            case self::CRATE_ATTR_SSL_CA_PATH:
432 52
                return $this->attributes['sslCa'] ?? null;
433
434 52
            case self::CRATE_ATTR_SSL_CA_PASSWORD:
435 52
                return $this->attributes['sslCaPassword'] ?? null;
436
437 52
            case self::CRATE_ATTR_SSL_CERT_PATH:
438 52
                return $this->attributes['sslCert'] ?? null;
439
440 52
            case self::CRATE_ATTR_SSL_CERT_PASSWORD:
441 52
                return $this->attributes['sslCertPassword'] ?? null;
442
443 52
            case self::CRATE_ATTR_SSL_KEY_PATH:
444 52
                return $this->attributes['sslKey'] ?? null;
445
446 52
            case self::CRATE_ATTR_SSL_KEY_PASSWORD:
447 52
                return $this->attributes['sslKeyPassword'] ?? null;
448
449
            default:
450
                // PHP Switch is a lose comparison
451 4
                if ($attribute === self::ATTR_AUTOCOMMIT) {
452 2
                    return true;
453
                }
454
455 2
                throw new Exception\PDOException(sprintf('Unsupported driver attribute: %s', $attribute));
456
        }
457
    }
458
459
    /**
460
     * {@inheritDoc}
461
     */
462 6
    #[\ReturnTypeWillChange]
463
    public function quote($string, $parameter_type = self::PARAM_STR)
464
    {
465
        switch ($parameter_type) {
466 6
            case self::PARAM_INT:
467 2
                return (int)$string;
468
469 6
            case self::PARAM_BOOL:
470 2
                return (bool)$string;
471
472 6
            case self::PARAM_NULL:
473 2
                return null;
474
475 6
            case self::PARAM_LOB:
476 2
                throw new Exception\UnsupportedException('This is not supported by crate.io');
477
478 4
            case self::PARAM_STR:
479 2
                trigger_error(
480 2
                    "Strongly consider using prepared statements (secure) " .
481 2
                    "instead of quoting strings manually (insecure), " .
482 2
                    "see also https://www.php.net/manual/en/pdo.quote.php.",
483 2
                    E_USER_DEPRECATED
484 2
                );
485 2
                return $this->quotePostgresql((string)$string);
486
487
            default:
488 2
                throw new Exception\InvalidArgumentException('Unknown param type');
489
        }
490
    }
491
492
    /**
493
     * Escape/quote strings for PostgreSQL when using prepared statements is not possible.
494
     *
495
     * https://github.com/ADOdb/ADOdb/blob/v5.22.10/adodb.inc.php
496
     * https://github.com/ADOdb/ADOdb/blob/v5.22.10/drivers/adodb-postgres64.inc.php
497
     *
498
     * @param string $string
499
     * @return string
500
     */
501 2
    private function quotePostgresql($value): string
502
    {
503 2
        if (is_bool($value)) {
504
            return $value ? 'true' : 'false';
505
        }
506 2
        $value = str_replace(
507 2
            array('\\', "\0"),
508 2
            array('\\\\', "\\\0"),
509 2
            $value
510 2
        );
511
        // CrateDB uses quote-doubling instead of backslash escaping.
512 2
        return str_replace("'", "''", $value);
513
    }
514
515
    /**
516
     * {@inheritDoc}
517
     */
518 2
    public static function getAvailableDrivers(): array
519
    {
520 2
        return array_merge(parent::getAvailableDrivers(), [static::DRIVER_NAME]);
521
    }
522
523
    public function getServerVersion(): string
524
    {
525
        return $this->server->getServerVersion();
526
    }
527
528
    public function getServerInfo(): string
529
    {
530
        return $this->getServerVersion();
531
    }
532
}
533