1<?php 2 3/** 4 * The phpMyFAQ\Database\Mysqli class provides methods and functions for MySQL and 5 * MariaDB databases. 6 * 7 * This Source Code Form is subject to the terms of the Mozilla Public License, 8 * v. 2.0. If a copy of the MPL was not distributed with this file, You can 9 * obtain one at http://mozilla.org/MPL/2.0/. 10 * 11 * @package phpMyFAQ 12 * @author Thorsten Rinne <thorsten@phpmyfaq.de> 13 * @author David Soria Parra <dsoria@gmx.net> 14 * @copyright 2005-2020 phpMyFAQ Team 15 * @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 16 * @link https://www.phpmyfaq.de 17 */ 18 19namespace phpMyFAQ\Database; 20 21use mysqli_result; 22use phpMyFAQ\Database; 23use phpMyFAQ\Exception; 24use phpMyFAQ\Utils; 25 26/** 27 * Class Mysqli 28 * 29 * @package phpMyFAQ\Database 30 */ 31class Mysqli implements DatabaseDriver 32{ 33 /** 34 * Tables. 35 * 36 * @var array 37 */ 38 public $tableNames = []; 39 40 /** 41 * The connection object. 42 * 43 * @var \mysqli 44 */ 45 private $conn = false; 46 47 /** 48 * The query log string. 49 * 50 * @var string 51 */ 52 private $sqllog = ''; 53 54 /** 55 * Connects to the database. 56 * 57 * @param string $host Hostname or path to socket 58 * @param string $user Username 59 * @param string $password Password 60 * @param string $database Database name 61 * @param int|null $port 62 * @return null|bool true, if connected, otherwise false 63 * @throws Exception 64 */ 65 public function connect(string $host, string $user, string $password, $database = '', $port = 3306): ?bool 66 { 67 if (substr($host, 0, 1) === '/') { 68 // Connect to MySQL via socket 69 $this->conn = new \mysqli(null, $user, $password, null, $port, $host); 70 } else { 71 // Connect to MySQL via network 72 $this->conn = new \mysqli($host, $user, $password, null, $port); 73 } 74 75 if ($this->conn->connect_error) { 76 Database::errorPage($this->conn->connect_errno . ': ' . $this->conn->connect_error); 77 die(); 78 } 79 80 // change character set to UTF-8 81 if (!$this->conn->set_charset('utf8')) { 82 Database::errorPage($this->error()); 83 } 84 85 if ('' !== $database) { 86 if (!$this->conn->select_db($database)) { 87 throw new Exception('Cannot connect to database ' . $database); 88 } 89 } 90 91 return true; 92 } 93 94 /** 95 * Returns the error string. 96 * 97 * @return string 98 */ 99 public function error(): string 100 { 101 return $this->conn->error; 102 } 103 104 /** 105 * Escapes a string for use in a query. 106 * 107 * @param string 108 * 109 * @return string 110 */ 111 public function escape($string): string 112 { 113 return $this->conn->real_escape_string($string); 114 } 115 116 /** 117 * Fetch a result row as an object. 118 * 119 * This function fetches a result as an associative array. 120 * 121 * @param mixed $result 122 * 123 * @return array 124 */ 125 public function fetchArray($result): ?array 126 { 127 return $result->fetch_assoc(); 128 } 129 130 /** 131 * Fetches a complete result as an object. 132 * 133 * @param mysqli_result $result Result set 134 * 135 * @return array 136 * @throws Exception 137 */ 138 public function fetchAll($result): array 139 { 140 $ret = []; 141 if (false === $result) { 142 throw new Exception('Error while fetching result: ' . $this->error()); 143 } 144 145 while ($row = $this->fetchObject($result)) { 146 $ret[] = $row; 147 } 148 149 return $ret; 150 } 151 152 /** 153 * Fetch a result row as an object. 154 * 155 * This function fetches a result row as an object. 156 * 157 * @param mysqli_result $result 158 * 159 * @return mixed 160 * @throws Exception 161 */ 162 public function fetchObject($result) 163 { 164 if ($result instanceof mysqli_result) { 165 return $result->fetch_object(); 166 } 167 168 throw new Exception($this->error()); 169 } 170 171 /** 172 * Number of rows in a result. 173 * 174 * @param mysqli_result $result 175 * 176 * @return int 177 */ 178 public function numRows($result): int 179 { 180 if ($result instanceof mysqli_result) { 181 return $result->num_rows; 182 } else { 183 return 0; 184 } 185 } 186 187 /** 188 * Logs the queries. 189 * 190 * @return string 191 */ 192 public function log(): string 193 { 194 return $this->sqllog; 195 } 196 197 /** 198 * This function returns the table status. 199 * 200 * @param string $prefix Table prefix 201 * 202 * @return array 203 */ 204 public function getTableStatus($prefix = ''): array 205 { 206 $status = []; 207 foreach ($this->getTableNames($prefix) as $table) { 208 $status[$table] = $this->getOne('SELECT count(*) FROM ' . $table); 209 } 210 211 return $status; 212 } 213 214 /** 215 * Returns an array with all table names. 216 * 217 * @todo Have to be refactored because of https://github.com/thorsten/phpMyFAQ/issues/965 218 * 219 * @param string $prefix Table prefix 220 * 221 * @return string[] 222 */ 223 public function getTableNames($prefix = ''): array 224 { 225 return $this->tableNames = [ 226 $prefix . 'faqadminlog', 227 $prefix . 'faqattachment', 228 $prefix . 'faqattachment_file', 229 $prefix . 'faqcaptcha', 230 $prefix . 'faqcategories', 231 $prefix . 'faqcategory_group', 232 $prefix . 'faqcategory_news', 233 $prefix . 'faqcategory_user', 234 $prefix . 'faqcategoryrelations', 235 $prefix . 'faqchanges', 236 $prefix . 'faqcomments', 237 $prefix . 'faqconfig', 238 $prefix . 'faqdata', 239 $prefix . 'faqdata_group', 240 $prefix . 'faqdata_revisions', 241 $prefix . 'faqdata_tags', 242 $prefix . 'faqdata_user', 243 $prefix . 'faqglossary', 244 $prefix . 'faqgroup', 245 $prefix . 'faqgroup_right', 246 $prefix . 'faqinstances', 247 $prefix . 'faqinstances_config', 248 $prefix . 'faqmeta', 249 $prefix . 'faqnews', 250 $prefix . 'faqquestions', 251 $prefix . 'faqright', 252 $prefix . 'faqsearches', 253 $prefix . 'faqsections', 254 $prefix . 'faqsection_group', 255 $prefix . 'faqsection_news', 256 $prefix . 'faqsessions', 257 $prefix . 'faqstopwords', 258 $prefix . 'faqtags', 259 $prefix . 'faquser', 260 $prefix . 'faquser_group', 261 $prefix . 'faquser_right', 262 $prefix . 'faquserdata', 263 $prefix . 'faquserlogin', 264 $prefix . 'faqvisits', 265 $prefix . 'faqvoting', 266 ]; 267 } 268 269 /** 270 * Returns just one row. 271 * 272 * @param string $query 273 * 274 * @return string 275 */ 276 private function getOne($query): string 277 { 278 $row = $this->conn->query($query)->fetch_row(); 279 280 return $row[0]; 281 } 282 283 /** 284 * This function is a replacement for MySQL's auto-increment so that 285 * we don't need it anymore. 286 * 287 * @param string $table The name of the table 288 * @param string $id The name of the ID column 289 * 290 * @return int 291 */ 292 public function nextId($table, $id): int 293 { 294 $select = sprintf( 295 ' 296 SELECT 297 MAX(%s) AS current_id 298 FROM 299 %s', 300 $id, 301 $table 302 ); 303 304 $result = $this->query($select); 305 306 if ($result instanceof mysqli_result) { 307 $current = $result->fetch_row(); 308 } else { 309 $current = [0]; 310 } 311 312 return $current[0] + 1; 313 } 314 315 /** 316 * This function sends a query to the database. 317 * 318 * @param string $query 319 * @param int $offset 320 * @param int $rowcount 321 * 322 * @return mysqli_result $result 323 */ 324 public function query(string $query, $offset = 0, $rowcount = 0) 325 { 326 if (DEBUG) { 327 $this->sqllog .= Utils::debug($query); 328 } 329 330 if (0 < $rowcount) { 331 $query .= sprintf(' LIMIT %d,%d', $offset, $rowcount); 332 } 333 334 $result = $this->conn->query($query); 335 336 if (false === $result) { 337 $this->sqllog .= $this->conn->errno . ': ' . $this->error(); 338 } 339 340 return $result; 341 } 342 343 /** 344 * Returns the client version string. 345 * 346 * @return string 347 */ 348 public function clientVersion(): string 349 { 350 return $this->conn->get_client_info(); 351 } 352 353 /** 354 * Returns the server version string. 355 * 356 * @return string 357 */ 358 public function serverVersion(): string 359 { 360 return $this->conn->server_info; 361 } 362 363 /** 364 * Closes the connection to the database. 365 */ 366 public function close() 367 { 368 if (is_resource($this->conn)) { 369 $this->conn->close(); 370 } 371 } 372 373 /** 374 * Destructor. 375 */ 376 public function __destruct() 377 { 378 if (is_resource($this->conn)) { 379 $this->conn->close(); 380 } 381 } 382 383 /** 384 * @return string 385 */ 386 public function now(): string 387 { 388 return 'NOW()'; 389 } 390} 391