1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2023 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees; 21 22use Illuminate\Database\Capsule\Manager; 23use Illuminate\Database\Query\Builder; 24use Illuminate\Database\Query\Expression; 25use PDO; 26use PDOException; 27use RuntimeException; 28use SensitiveParameter; 29 30/** 31 * Database abstraction 32 */ 33class DB extends Manager 34{ 35 // Supported drivers 36 public const string MYSQL = 'mysql'; 37 public const string POSTGRES = 'pgsql'; 38 public const string SQLITE = 'sqlite'; 39 public const string SQL_SERVER = 'sqlsrv'; 40 41 private const array COLLATION_ASCII = [ 42 self::MYSQL => 'ascii_bin', 43 self::POSTGRES => 'C', 44 self::SQLITE => 'C', 45 self::SQL_SERVER => 'Latin1_General_Bin', 46 ]; 47 48 private const array COLLATION_UTF8 = [ 49 self::MYSQL => 'utf8mb4_unicode_ci', 50 self::POSTGRES => 'und-x-icu', 51 self::SQLITE => 'nocase', 52 self::SQL_SERVER => 'utf8_CI_AI', 53 ]; 54 55 private const array REGEX_OPERATOR = [ 56 self::MYSQL => 'REGEXP', 57 self::POSTGRES => '~', 58 self::SQLITE => 'REGEXP', 59 self::SQL_SERVER => 'REGEXP', 60 ]; 61 62 private const array DRIVER_INITIALIZATION = [ 63 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 self::POSTGRES => '', 65 self::SQLITE => 'PRAGMA foreign_keys = ON', 66 self::SQL_SERVER => 'SET language us_english', // For timestamp columns 67 ]; 68 69 public static function connect( 70 #[SensitiveParameter] 71 string $driver, 72 #[SensitiveParameter] 73 string $host, 74 #[SensitiveParameter] 75 string $port, 76 #[SensitiveParameter] 77 string $database, 78 #[SensitiveParameter] 79 string $username, 80 #[SensitiveParameter] 81 string $password, 82 #[SensitiveParameter] 83 string $prefix, 84 #[SensitiveParameter] 85 string $key, 86 #[SensitiveParameter] 87 string $certificate, 88 #[SensitiveParameter] 89 string $ca, 90 #[SensitiveParameter] 91 bool $verify_certificate, 92 ): void { 93 $options = [ 94 // Some drivers do this and some don't. Make them consistent. 95 PDO::ATTR_STRINGIFY_FETCHES => true, 96 ]; 97 98 // MySQL/MariaDB support encrypted connections 99 if ($driver === self::MYSQL && $key !== '' && $certificate !== '' && $ca !== '') { 100 $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $verify_certificate; 101 $options[PDO::MYSQL_ATTR_SSL_KEY] = Webtrees::ROOT_DIR . 'data/' . $key; 102 $options[PDO::MYSQL_ATTR_SSL_CERT] = Webtrees::ROOT_DIR . 'data/' . $certificate; 103 $options[PDO::MYSQL_ATTR_SSL_CA] = Webtrees::ROOT_DIR . 'data/' . $ca; 104 } 105 106 if ($driver === self::SQLITE && $database !== ':memory:') { 107 $database = Webtrees::ROOT_DIR . 'data/' . $database . '.sqlite'; 108 } 109 110 $capsule = new self(); 111 $capsule->addConnection([ 112 'driver' => $driver, 113 'host' => $host, 114 'port' => $port, 115 'database' => $database, 116 'username' => $username, 117 'password' => $password, 118 'prefix' => $prefix, 119 'prefix_indexes' => true, 120 'options' => $options, 121 ]); 122 $capsule->setAsGlobal(); 123 124 // Eager-load the connection, to prevent database credentials appearing in error logs. 125 try { 126 self::pdo(); 127 } catch (PDOException $exception) { 128 throw new RuntimeException($exception->getMessage()); 129 } 130 131 $sql = self::DRIVER_INITIALIZATION[$driver]; 132 133 if ($sql !== '') { 134 self::exec($sql); 135 } 136 } 137 138 public static function driverName(): string 139 { 140 return self::pdo()->getAttribute(PDO::ATTR_DRIVER_NAME); 141 } 142 143 public static function exec(string $sql): int|false 144 { 145 return self::pdo()->exec($sql); 146 } 147 148 public static function lastInsertId(): int 149 { 150 $return = self::pdo()->lastInsertId(); 151 152 if ($return === false) { 153 throw new RuntimeException('Unable to retrieve last insert ID'); 154 } 155 156 // All IDs are integers in our schema. 157 return (int) $return; 158 } 159 160 private static function pdo(): PDO 161 { 162 return parent::connection()->getPdo(); 163 } 164 165 public static function prefix(string $identifier = ''): string 166 { 167 return parent::connection()->getTablePrefix() . $identifier; 168 } 169 170 public static function rollBack(): void 171 { 172 parent::connection()->rollBack(); 173 } 174 175 /** 176 * @internal 177 */ 178 public static function iLike(): string 179 { 180 if (self::driverName() === self::POSTGRES) { 181 return 'ILIKE'; 182 } 183 184 if (self::driverName() === self::SQL_SERVER) { 185 return 'COLLATE SQL_UTF8_General_CI_AI LIKE'; 186 } 187 188 return 'LIKE'; 189 } 190 191 /** 192 * @internal 193 */ 194 public static function groupConcat(string $column): string 195 { 196 switch (self::driverName()) { 197 case self::POSTGRES: 198 case self::SQL_SERVER: 199 return 'STRING_AGG(' . $column . ", ',')"; 200 201 case self::MYSQL: 202 case self::SQLITE: 203 default: 204 return 'GROUP_CONCAT(' . $column . ')'; 205 } 206 } 207 208 public static function binaryColumn(string $column, string|null $alias = null): Expression 209 { 210 if (self::driverName() === self::MYSQL) { 211 $sql = 'CAST(' . $column . ' AS binary)'; 212 } else { 213 $sql = $column; 214 } 215 216 if ($alias !== null) { 217 $sql .= ' AS ' . $alias; 218 } 219 220 return new Expression($sql); 221 } 222 223 public static function regexOperator(): string 224 { 225 return self::REGEX_OPERATOR[self::driverName()]; 226 } 227 228 /** 229 * PHPSTAN can't detect the magic methods in the parent class. 230 */ 231 public static function query(): Builder 232 { 233 return parent::connection()->query(); 234 } 235} 236