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