Passed
Push — main ( a01029...30b22a )
by Andreas
02:09
created

PDOCrateDB::exec()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 7
ccs 0
cts 4
cp 0
crap 6
rs 10
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
        $statementClass = $this->attributes['statementClass'];
221 4
        if (is_string($statementClass)) {
222
            trigger_error(
223
                "Using bare class strings with `statementClass` is deprecated, " .
224
                "see also https://github.com/crate/crate-pdo/issues/191.",
225
                E_USER_DEPRECATED
226
            );
227
            $className = $statementClass;
228
            $constructorArgs = [];
229 4
        } elseif (is_array($statementClass)) {
230 4
            $className = $statementClass[0];
231 4
            $constructorArgs = $statementClass[1] ?? [];
232
        } else {
233
            throw new InvalidArgumentException(
234
                'Value provided to statementClass has invalid type'
235
            );
236
        }
237 4
        if ($className === PDOStatement::class) {
238 4
            $constructorArgs = [$this, $this->request, $statement, $options];
239
        }
240
241 4
        return new $className(...$constructorArgs);
242
    }
243
244
    /**
245
     * {@inheritDoc}
246
     */
247 2
    public function beginTransaction(): bool
248
    {
249 2
        return true;
250
    }
251
252
    /**
253
     * {@inheritDoc}
254
     */
255 2
    public function commit(): bool
256
    {
257 2
        return true;
258
    }
259
260
    /**
261
     * {@inheritDoc}
262
     */
263 2
    public function rollBack(): bool
264
    {
265 2
        throw new Exception\UnsupportedException;
266
    }
267
268
    /**
269
     * {@inheritDoc}
270
     */
271 2
    public function inTransaction(): bool
272
    {
273 2
        return false;
274
    }
275
276
    /**
277
     * {@inheritDoc}
278
     */
279
    #[\ReturnTypeWillChange]
280
    public function exec($statement)
281
    {
282
        $statement = $this->prepare($statement);
283
        $result    = $statement->execute();
284
285
        return $result === false ? false : $statement->rowCount();
286
    }
287
288
    /**
289
     * {@inheritDoc}
290
     */
291 2
    public function doQuery($statement, ?int $fetchMode = null, ...$fetchModeArgs)
292
    {
293 2
        $statement = $this->prepare($statement);
294 2
        if ($fetchMode !== null) {
295
            $statement->setFetchMode($fetchMode, ...$fetchModeArgs);
296
        }
297
298 2
        $result    = $statement->execute();
299
300 2
        return $result === false ? false : $statement;
301
    }
302
303
    /**
304
     * {@inheritDoc}
305
     */
306 2
    public function lastInsertId($name = null): string
307
    {
308 2
        throw new Exception\UnsupportedException;
309
    }
310
311
    /**
312
     * {@inheritDoc}
313
     */
314
    public function errorCode(): ?string
315
    {
316
        return $this->lastStatement === null ? null : $this->lastStatement->errorCode();
317
    }
318
319
    /**
320
     * {@inheritDoc}
321
     */
322
    public function errorInfo(): array
323
    {
324
        return $this->lastStatement === null ? ["00000", null, null] : $this->lastStatement->errorInfo();
325
    }
326
327
    /**
328
     * {@inheritDoc}
329
     *
330
     * @throws \Crate\PDO\Exception\PDOException
331
     * @throws \Crate\PDO\Exception\InvalidArgumentException
332
     */
333 32
    #[\ReturnTypeWillChange]
334
    public function setAttribute($attribute, $value)
335
    {
336
        switch ($attribute) {
337 32
            case self::ATTR_DEFAULT_FETCH_MODE:
338 2
                $this->attributes['defaultFetchMode'] = $value;
339 2
                break;
340
341 30
            case self::ATTR_ERRMODE:
342 2
                $this->attributes['errorMode'] = $value;
343 2
                break;
344
345 28
            case self::ATTR_STATEMENT_CLASS:
346
                // Previous versions accepted bare class strings on this mode's argument value,
347
                // while the PDO standard format is `[ClassName::class, [constructor_args]]`.
348
                // Let's modernize, upcycle, and propagate accordingly.
349 2
                if (is_string($value)) {
350 2
                    trigger_error(
351 2
                        "Using bare class strings with `ATTR_STATEMENT_CLASS` is deprecated, " .
352 2
                        "see also https://github.com/crate/crate-pdo/issues/191.",
353 2
                        E_USER_DEPRECATED
354 2
                    );
355 2
                    $this->attributes['statementClass'] = [$value, []];
356
                } elseif (is_array($value)) {
357
                    if (empty($value) || !is_string($value[0])) {
358
                        throw new InvalidArgumentException(
359
                            'ATTR_STATEMENT_CLASS array must contain a class name as first element'
360
                        );
361
                    }
362
                    $this->attributes['statementClass'] = $value;
363
                } else {
364
                    throw new InvalidArgumentException(
365
                        'Value provided to ATTR_STATEMENT_CLASS has invalid type'
366
                    );
367
                }
368 2
                break;
369
370 26
            case self::ATTR_TIMEOUT:
371 4
                $this->attributes['timeout'] = (int)$value;
372 4
                break;
373
374 24
            case self::CRATE_ATTR_HTTP_BASIC_AUTH:
375 4
                if (!is_array($value) && $value !== null) {
376
                    throw new InvalidArgumentException(
377
                        'Value provided to CRATE_ATTR_HTTP_BASIC_AUTH must be null or an array'
378
                    );
379
                }
380
381 4
                $this->attributes['auth'] = $value;
382 4
                break;
383
384 24
            case self::CRATE_ATTR_DEFAULT_SCHEMA:
385 4
                $this->attributes['defaultSchema'] = $value;
386 4
                break;
387
388 20
            case self::CRATE_ATTR_SSL_MODE:
389 18
                $this->attributes['sslMode'] = $value;
390 18
                break;
391
392 14
            case self::CRATE_ATTR_SSL_CA_PATH:
393 4
                $this->attributes['sslCa'] = $value;
394 4
                break;
395
396 12
            case self::CRATE_ATTR_SSL_CA_PASSWORD:
397 2
                $this->attributes['sslCaPassword'] = $value;
398 2
                break;
399
400 10
            case self::CRATE_ATTR_SSL_CERT_PATH:
401 4
                $this->attributes['sslCert'] = $value;
402 4
                break;
403
404 8
            case self::CRATE_ATTR_SSL_CERT_PASSWORD:
405 2
                $this->attributes['sslCertPassword'] = $value;
406 2
                break;
407
408 6
            case self::CRATE_ATTR_SSL_KEY_PATH:
409 4
                $this->attributes['sslKey'] = $value;
410 4
                break;
411
412 4
            case self::CRATE_ATTR_SSL_KEY_PASSWORD:
413 2
                $this->attributes['sslKeyPassword'] = $value;
414 2
                break;
415
416
            default:
417 2
                throw new Exception\PDOException('Unsupported driver attribute');
418
        }
419
420
        // A setting changed so we need to reconfigure the server pool
421 30
        $this->server->configure($this);
422 30
        return true;
423
    }
424
425
    /**
426
     * {@inheritDoc}
427
     *
428
     * @throws \Crate\PDO\Exception\PDOException
429
     */
430 52
    #[\ReturnTypeWillChange]
431
    public function getAttribute($attribute)
432
    {
433
        switch ($attribute) {
434 52
            case self::ATTR_PREFETCH:
435 52
            case self::ATTR_PERSISTENT:
436 4
                return false;
437
438 52
            case self::ATTR_CLIENT_VERSION:
439 2
                return self::VERSION;
440
441 52
            case self::ATTR_SERVER_VERSION:
442
                return $this->server->getServerVersion();
443
444 52
            case self::ATTR_SERVER_INFO:
445
                return $this->server->getServerInfo();
446
447 52
            case self::ATTR_TIMEOUT:
448 52
                return $this->attributes['timeout'];
449
450 52
            case self::CRATE_ATTR_HTTP_BASIC_AUTH:
451 52
                return $this->attributes['auth'];
452
453 52
            case self::ATTR_DEFAULT_FETCH_MODE:
454 4
                return $this->attributes['defaultFetchMode'];
455
456 52
            case self::ATTR_ERRMODE:
457 2
                return $this->attributes['errorMode'];
458
459 52
            case self::ATTR_DRIVER_NAME:
460 2
                return static::DRIVER_NAME;
461
462 52
            case self::ATTR_STATEMENT_CLASS:
463 2
                return $this->attributes['statementClass'];
464
465 52
            case self::CRATE_ATTR_DEFAULT_SCHEMA:
466 52
                return $this->attributes['defaultSchema'];
467
468 52
            case self::CRATE_ATTR_SSL_MODE:
469 52
                return $this->attributes['sslMode'];
470
471 52
            case self::CRATE_ATTR_SSL_CA_PATH:
472 52
                return $this->attributes['sslCa'] ?? null;
473
474 52
            case self::CRATE_ATTR_SSL_CA_PASSWORD:
475 52
                return $this->attributes['sslCaPassword'] ?? null;
476
477 52
            case self::CRATE_ATTR_SSL_CERT_PATH:
478 52
                return $this->attributes['sslCert'] ?? null;
479
480 52
            case self::CRATE_ATTR_SSL_CERT_PASSWORD:
481 52
                return $this->attributes['sslCertPassword'] ?? null;
482
483 52
            case self::CRATE_ATTR_SSL_KEY_PATH:
484 52
                return $this->attributes['sslKey'] ?? null;
485
486 52
            case self::CRATE_ATTR_SSL_KEY_PASSWORD:
487 52
                return $this->attributes['sslKeyPassword'] ?? null;
488
489
            default:
490
                // PHP Switch is a lose comparison
491 4
                if ($attribute === self::ATTR_AUTOCOMMIT) {
492 2
                    return true;
493
                }
494
495 2
                throw new Exception\PDOException(sprintf('Unsupported driver attribute: %s', $attribute));
496
        }
497
    }
498
499
    /**
500
     * {@inheritDoc}
501
     */
502 6
    #[\ReturnTypeWillChange]
503
    public function quote($string, $parameter_type = self::PARAM_STR)
504
    {
505
        switch ($parameter_type) {
506 6
            case self::PARAM_INT:
507 2
                return (int)$string;
508
509 6
            case self::PARAM_BOOL:
510 2
                return (bool)$string;
511
512 6
            case self::PARAM_NULL:
513 2
                return null;
514
515 6
            case self::PARAM_LOB:
516 2
                throw new Exception\UnsupportedException('This is not supported by crate.io');
517
518 4
            case self::PARAM_STR:
519 2
                trigger_error(
520 2
                    "Strongly consider using prepared statements (secure) " .
521 2
                    "instead of quoting strings manually (insecure), " .
522 2
                    "see also https://www.php.net/manual/en/pdo.quote.php.",
523 2
                    E_USER_DEPRECATED
524 2
                );
525 2
                return $this->quotePostgresql((string)$string);
526
527
            default:
528 2
                throw new Exception\InvalidArgumentException('Unknown param type');
529
        }
530
    }
531
532
    /**
533
     * Escape/quote strings for PostgreSQL when using prepared statements is not possible.
534
     *
535
     * https://github.com/ADOdb/ADOdb/blob/v5.22.10/adodb.inc.php
536
     * https://github.com/ADOdb/ADOdb/blob/v5.22.10/drivers/adodb-postgres64.inc.php
537
     *
538
     * @param string $string
539
     * @return string
540
     */
541 2
    private function quotePostgresql($value): string
542
    {
543 2
        if (is_bool($value)) {
544
            return $value ? 'true' : 'false';
545
        }
546 2
        $value = str_replace(
547 2
            array('\\', "\0"),
548 2
            array('\\\\', "\\\0"),
549 2
            $value
550 2
        );
551
        // CrateDB uses quote-doubling instead of backslash escaping.
552 2
        return str_replace("'", "''", $value);
553
    }
554
555
    /**
556
     * {@inheritDoc}
557
     */
558 2
    public static function getAvailableDrivers(): array
559
    {
560 2
        return array_merge(parent::getAvailableDrivers(), [static::DRIVER_NAME]);
561
    }
562
563
    public function getServerVersion(): string
564
    {
565
        return $this->server->getServerVersion();
566
    }
567
568
    public function getServerInfo(): string
569
    {
570
        return $this->getServerVersion();
571
    }
572
}
573