xref: /webtrees/app/DB.php (revision 52550490b7095dd69811f3ec21ed5a3ca1a8968d)
16f4ec3caSGreg Roach<?php
26f4ec3caSGreg Roach
36f4ec3caSGreg Roach/**
46f4ec3caSGreg Roach * webtrees: online genealogy
56f4ec3caSGreg Roach * Copyright (C) 2023 webtrees development team
66f4ec3caSGreg Roach * This program is free software: you can redistribute it and/or modify
76f4ec3caSGreg Roach * it under the terms of the GNU General Public License as published by
86f4ec3caSGreg Roach * the Free Software Foundation, either version 3 of the License, or
96f4ec3caSGreg Roach * (at your option) any later version.
106f4ec3caSGreg Roach * This program is distributed in the hope that it will be useful,
116f4ec3caSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
126f4ec3caSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
136f4ec3caSGreg Roach * GNU General Public License for more details.
146f4ec3caSGreg Roach * You should have received a copy of the GNU General Public License
156f4ec3caSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
166f4ec3caSGreg Roach */
176f4ec3caSGreg Roach
186f4ec3caSGreg Roachdeclare(strict_types=1);
196f4ec3caSGreg Roach
206f4ec3caSGreg Roachnamespace Fisharebest\Webtrees;
216f4ec3caSGreg Roach
226f4ec3caSGreg Roachuse Illuminate\Database\Capsule\Manager;
236bd19c8cSGreg Roachuse Illuminate\Database\Query\Builder;
24*52550490SGreg Roachuse Illuminate\Database\Query\Expression;
25211018abSGreg Roachuse PDO;
26*52550490SGreg Roachuse PDOException;
274c96e13dSGreg Roachuse RuntimeException;
28*52550490SGreg Roachuse SensitiveParameter;
296f4ec3caSGreg Roach
306f4ec3caSGreg Roach/**
316f4ec3caSGreg Roach * Database abstraction
326f4ec3caSGreg Roach */
336f4ec3caSGreg Roachclass DB extends Manager
346f4ec3caSGreg Roach{
35*52550490SGreg Roach    // Supported drivers
36*52550490SGreg Roach    public const MYSQL      = 'mysql';
37*52550490SGreg Roach    public const POSTGRES   = 'pgsql';
38*52550490SGreg Roach    public const SQLITE     = 'sqlite';
39*52550490SGreg Roach    public const SQL_SERVER = 'sqlsrv';
40*52550490SGreg Roach
41*52550490SGreg Roach    private const COLLATION_ASCII = [
42*52550490SGreg Roach        self::MYSQL      => 'ascii_bin',
43*52550490SGreg Roach        self::POSTGRES   => 'C',
44*52550490SGreg Roach        self::SQLITE     => 'C',
45*52550490SGreg Roach        self::SQL_SERVER => 'Latin1_General_Bin',
46*52550490SGreg Roach    ];
47*52550490SGreg Roach
48*52550490SGreg Roach    private const COLLATION_UTF8 = [
49*52550490SGreg Roach        self::MYSQL      => 'utf8mb4_unicode_ci',
50*52550490SGreg Roach        self::POSTGRES   => 'und-x-icu',
51*52550490SGreg Roach        self::SQLITE     => 'nocase',
52*52550490SGreg Roach        self::SQL_SERVER => 'utf8_CI_AI',
53*52550490SGreg Roach    ];
54*52550490SGreg Roach
55*52550490SGreg Roach    private const REGEX_OPERATOR = [
56*52550490SGreg Roach        self::MYSQL      => 'REGEXP',
57*52550490SGreg Roach        self::POSTGRES   => '~',
58*52550490SGreg Roach        self::SQLITE     => 'REGEXP',
59*52550490SGreg Roach        self::SQL_SERVER => 'REGEXP',
60*52550490SGreg Roach    ];
61*52550490SGreg Roach
62*52550490SGreg Roach    private const DRIVER_INITIALIZATION = [
63*52550490SGreg Roach        self::MYSQL      => "SET NAMES utf8mb4, sql_mode := 'ANSI,STRICT_ALL_TABLES', TIME_ZONE := '+00:00', SQL_BIG_SELECTS := 1, GROUP_CONCAT_MAX_LEN := 1048576",
64*52550490SGreg Roach        self::POSTGRES   => '',
65*52550490SGreg Roach        self::SQLITE     => 'PRAGMA foreign_keys = ON',
66*52550490SGreg Roach        self::SQL_SERVER => 'SET language us_english', // For timestamp columns
67*52550490SGreg Roach    ];
68*52550490SGreg Roach
69*52550490SGreg Roach    public static function connect(
70*52550490SGreg Roach        #[SensitiveParameter]
71*52550490SGreg Roach        string $driver,
72*52550490SGreg Roach        #[SensitiveParameter]
73*52550490SGreg Roach        string $host,
74*52550490SGreg Roach        #[SensitiveParameter]
75*52550490SGreg Roach        string $port,
76*52550490SGreg Roach        #[SensitiveParameter]
77*52550490SGreg Roach        string $database,
78*52550490SGreg Roach        #[SensitiveParameter]
79*52550490SGreg Roach        string $username,
80*52550490SGreg Roach        #[SensitiveParameter]
81*52550490SGreg Roach        string $password,
82*52550490SGreg Roach        #[SensitiveParameter]
83*52550490SGreg Roach        string $prefix,
84*52550490SGreg Roach        #[SensitiveParameter]
85*52550490SGreg Roach        string $key,
86*52550490SGreg Roach        #[SensitiveParameter]
87*52550490SGreg Roach        string $certificate,
88*52550490SGreg Roach        #[SensitiveParameter]
89*52550490SGreg Roach        string $ca,
90*52550490SGreg Roach        #[SensitiveParameter]
91*52550490SGreg Roach        bool $verify_certificate,
92*52550490SGreg Roach    ): void {
93*52550490SGreg Roach        $options = [
94*52550490SGreg Roach            // Some drivers do this and some don't. Make them consistent.
95*52550490SGreg Roach            PDO::ATTR_STRINGIFY_FETCHES => true,
96*52550490SGreg Roach        ];
97*52550490SGreg Roach
98*52550490SGreg Roach        // MySQL/MariaDB support encrypted connections
99*52550490SGreg Roach        if ($driver === self::MYSQL && $key !== '' && $certificate !== '' && $ca !== '') {
100*52550490SGreg Roach            $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $verify_certificate;
101*52550490SGreg Roach            $options[PDO::MYSQL_ATTR_SSL_KEY]                = Webtrees::ROOT_DIR . 'data/' . $key;
102*52550490SGreg Roach            $options[PDO::MYSQL_ATTR_SSL_CERT]               = Webtrees::ROOT_DIR . 'data/' . $certificate;
103*52550490SGreg Roach            $options[PDO::MYSQL_ATTR_SSL_CA]                 = Webtrees::ROOT_DIR . 'data/' . $ca;
104*52550490SGreg Roach        }
105*52550490SGreg Roach
106*52550490SGreg Roach        if ($driver === self::SQLITE && $database !== ':memory:') {
107*52550490SGreg Roach            $database = Webtrees::ROOT_DIR . 'data/' . $database . '.sqlite';
108*52550490SGreg Roach        }
109*52550490SGreg Roach
110*52550490SGreg Roach        $capsule = new self();
111*52550490SGreg Roach        $capsule->addConnection([
112*52550490SGreg Roach            'driver'         => $driver,
113*52550490SGreg Roach            'host'           => $host,
114*52550490SGreg Roach            'port'           => $port,
115*52550490SGreg Roach            'database'       => $database,
116*52550490SGreg Roach            'username'       => $username,
117*52550490SGreg Roach            'password'       => $password,
118*52550490SGreg Roach            'prefix'         => $prefix,
119*52550490SGreg Roach            'prefix_indexes' => true,
120*52550490SGreg Roach            'options'        => $options,
121*52550490SGreg Roach        ]);
122*52550490SGreg Roach        $capsule->setAsGlobal();
123*52550490SGreg Roach
124*52550490SGreg Roach        // Eager-load the connection, to prevent database credentials appearing in error logs.
125*52550490SGreg Roach        try {
126*52550490SGreg Roach            self::pdo();
127*52550490SGreg Roach        } catch (PDOException $exception) {
128*52550490SGreg Roach            throw new RuntimeException($exception->getMessage());
129*52550490SGreg Roach        }
130*52550490SGreg Roach
131*52550490SGreg Roach        $sql = self::DRIVER_INITIALIZATION[$driver];
132*52550490SGreg Roach
133*52550490SGreg Roach        if ($sql !== '') {
134*52550490SGreg Roach            self::exec($sql);
135*52550490SGreg Roach        }
136*52550490SGreg Roach    }
137*52550490SGreg Roach
138211018abSGreg Roach    public static function driverName(): string
139211018abSGreg Roach    {
140*52550490SGreg Roach        return self::pdo()->getAttribute(PDO::ATTR_DRIVER_NAME);
141*52550490SGreg Roach    }
142*52550490SGreg Roach
143*52550490SGreg Roach    public static function exec(string $sql): int|false
144*52550490SGreg Roach    {
145*52550490SGreg Roach        return self::pdo()->exec($sql);
146*52550490SGreg Roach    }
147*52550490SGreg Roach
148*52550490SGreg Roach    public static function lastInsertId(): int
149*52550490SGreg Roach    {
150*52550490SGreg Roach        $return = self::pdo()->lastInsertId();
151*52550490SGreg Roach
152*52550490SGreg Roach        if ($return === false) {
153*52550490SGreg Roach            throw new RuntimeException('Unable to retrieve last insert ID');
154*52550490SGreg Roach        }
155*52550490SGreg Roach
156*52550490SGreg Roach        // All IDs are integers in our schema.
157*52550490SGreg Roach        return (int) $return;
158*52550490SGreg Roach    }
159*52550490SGreg Roach
160*52550490SGreg Roach    private static function pdo(): PDO
161*52550490SGreg Roach    {
162*52550490SGreg Roach        return parent::connection()->getPdo();
163211018abSGreg Roach    }
164211018abSGreg Roach
165211018abSGreg Roach    public static function prefix(string $identifier = ''): string
166211018abSGreg Roach    {
167211018abSGreg Roach        return parent::connection()->getTablePrefix() . $identifier;
168211018abSGreg Roach    }
169211018abSGreg Roach
170*52550490SGreg Roach    public static function rollBack(): void
171*52550490SGreg Roach    {
172*52550490SGreg Roach        parent::connection()->rollBack();
173*52550490SGreg Roach    }
174*52550490SGreg Roach
1756f4ec3caSGreg Roach    /**
1766f4ec3caSGreg Roach     * @internal
1776f4ec3caSGreg Roach     */
178*52550490SGreg Roach    public static function iLike(): string
1796f4ec3caSGreg Roach    {
180*52550490SGreg Roach        if (self::driverName() === self::POSTGRES) {
1816f4ec3caSGreg Roach            return 'ILIKE';
1826f4ec3caSGreg Roach        }
1836f4ec3caSGreg Roach
184*52550490SGreg Roach        if (self::driverName() === self::SQL_SERVER) {
1856f4ec3caSGreg Roach            return 'COLLATE SQL_UTF8_General_CI_AI LIKE';
1866f4ec3caSGreg Roach        }
1876f4ec3caSGreg Roach
1886f4ec3caSGreg Roach        return 'LIKE';
1896f4ec3caSGreg Roach    }
190859d33aaSGreg Roach
191859d33aaSGreg Roach    /**
192859d33aaSGreg Roach     * @internal
193859d33aaSGreg Roach     */
194859d33aaSGreg Roach    public static function groupConcat(string $column): string
195859d33aaSGreg Roach    {
196211018abSGreg Roach        switch (self::driverName()) {
197*52550490SGreg Roach            case self::POSTGRES:
198*52550490SGreg Roach            case self::SQL_SERVER:
199859d33aaSGreg Roach                return 'STRING_AGG(' . $column . ", ',')";
200859d33aaSGreg Roach
201*52550490SGreg Roach            case self::MYSQL:
202*52550490SGreg Roach            case self::SQLITE:
203859d33aaSGreg Roach            default:
204859d33aaSGreg Roach                return 'GROUP_CONCAT(' . $column . ')';
205859d33aaSGreg Roach        }
206859d33aaSGreg Roach    }
2076bd19c8cSGreg Roach
208*52550490SGreg Roach    public static function binaryColumn(string $column, string|null $alias = null): Expression
2094c96e13dSGreg Roach    {
210*52550490SGreg Roach        if (self::driverName() === self::MYSQL) {
211*52550490SGreg Roach            $sql = 'CAST(' . $column . ' AS binary)';
212*52550490SGreg Roach        } else {
213*52550490SGreg Roach            $sql = $column;
2144c96e13dSGreg Roach        }
2154c96e13dSGreg Roach
216*52550490SGreg Roach        if ($alias !== null) {
217*52550490SGreg Roach            $sql .= ' AS ' . $alias;
218*52550490SGreg Roach        }
219*52550490SGreg Roach
220*52550490SGreg Roach        return new Expression($sql);
221*52550490SGreg Roach    }
222*52550490SGreg Roach
223*52550490SGreg Roach    public static function regexOperator(): string
224*52550490SGreg Roach    {
225*52550490SGreg Roach        return self::REGEX_OPERATOR[self::driverName()];
2264c96e13dSGreg Roach    }
2274c96e13dSGreg Roach
2286bd19c8cSGreg Roach    /**
2296bd19c8cSGreg Roach     * PHPSTAN can't detect the magic methods in the parent class.
2306bd19c8cSGreg Roach     */
2316bd19c8cSGreg Roach    public static function query(): Builder
2326bd19c8cSGreg Roach    {
233211018abSGreg Roach        return parent::connection()->query();
2346bd19c8cSGreg Roach    }
2356f4ec3caSGreg Roach}
236