2018-12-20 23:06:28 +00:00
< ? php
/** @ license MIT
* Copyright 2017 J . King , Dustin Wilson et al .
* See LICENSE and AUTHORS files for details */
declare ( strict_types = 1 );
2021-04-14 15:17:01 +00:00
2018-12-20 23:06:28 +00:00
namespace JKingWeb\Arsse\Db\MySQL ;
use JKingWeb\Arsse\Arsse ;
use JKingWeb\Arsse\Db\Exception ;
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
2019-01-11 00:01:32 +00:00
use ExceptionBuilder ;
2020-03-01 23:32:01 +00:00
protected const SQL_MODE = " ANSI_QUOTES,HIGH_NOT_PRECEDENCE,NO_BACKSLASH_ESCAPES,NO_ENGINE_SUBSTITUTION,PIPES_AS_CONCAT,STRICT_ALL_TABLES " ;
protected const TRANSACTIONAL_LOCKS = false ;
2018-12-20 23:06:28 +00:00
2019-01-23 21:31:54 +00:00
/** @var \mysqli */
2018-12-20 23:06:28 +00:00
protected $db ;
protected $transStart = 0 ;
2019-01-11 00:01:32 +00:00
protected $packetSize = 4194304 ;
2018-12-20 23:06:28 +00:00
public function __construct () {
// check to make sure required extension is loaded
if ( ! static :: requirementsMet ()) {
throw new Exception ( " extMissing " , static :: driverName ()); // @codeCoverageIgnore
}
2019-01-21 03:40:49 +00:00
$host = strtolower ( ! strlen ( Arsse :: $conf -> dbMySQLHost ) ? " localhost " : Arsse :: $conf -> dbMySQLHost );
$socket = strlen ( Arsse :: $conf -> dbMySQLSocket ) ? Arsse :: $conf -> dbMySQLSocket : ini_get ( " mysqli.default_socket " );
$user = Arsse :: $conf -> dbMySQLUser ;
$pass = Arsse :: $conf -> dbMySQLPass ;
$port = Arsse :: $conf -> dbMySQLPort ;
$db = Arsse :: $conf -> dbMySQLDb ;
2019-01-11 00:01:32 +00:00
// make the connection
2019-01-15 15:51:55 +00:00
$this -> makeConnection ( $db , $user , $pass , $host , $port , $socket );
2019-01-11 00:01:32 +00:00
// set session variables
foreach ( static :: makeSetupQueries () as $q ) {
$this -> exec ( $q );
}
// get the maximum packet size; parameter strings larger than this size need to be chunked
2019-03-09 21:23:56 +00:00
$this -> packetSize = ( int ) $this -> query ( " SELECT variable_value from performance_schema.session_variables where variable_name = 'max_allowed_packet' " ) -> getValue ();
2019-01-11 00:01:32 +00:00
}
public static function makeSetupQueries () : array {
return [
" SET sql_mode = ' " . self :: SQL_MODE . " ' " ,
" SET time_zone = '+00:00' " ,
2019-01-23 21:31:54 +00:00
" SET lock_wait_timeout = " . self :: lockTimeout (),
" SET max_execution_time = " . ceil ( Arsse :: $conf -> dbTimeoutExec * 1000 ),
2019-01-11 00:01:32 +00:00
];
2018-12-20 23:06:28 +00:00
}
/** @codeCoverageIgnore */
public static function create () : \JKingWeb\Arsse\Db\Driver {
if ( self :: requirementsMet ()) {
return new self ;
} elseif ( PDODriver :: requirementsMet ()) {
return new PDODriver ;
} else {
throw new Exception ( " extMissing " , self :: driverName ());
}
}
public static function schemaID () : string {
return " MySQL " ;
}
public function charsetAcceptable () : bool {
return true ;
}
public function schemaVersion () : int {
if ( $this -> query ( " SELECT count(*) from information_schema.tables where table_name = 'arsse_meta' " ) -> getValue ()) {
return ( int ) $this -> query ( " SELECT value from arsse_meta where `key` = 'schema_version' " ) -> getValue ();
} else {
return 0 ;
}
}
public function sqlToken ( string $token ) : string {
switch ( strtolower ( $token )) {
case " nocase " :
2018-12-21 01:50:56 +00:00
return '"utf8mb4_unicode_ci"' ;
2021-02-02 15:00:08 +00:00
case " asc " :
return " " ;
2018-12-20 23:06:28 +00:00
default :
return $token ;
}
}
public function savepointCreate ( bool $lock = false ) : int {
if ( ! $this -> transStart && ! $lock ) {
$this -> exec ( " BEGIN " );
$this -> transStart = parent :: savepointCreate ( $lock );
return $this -> transStart ;
} else {
return parent :: savepointCreate ( $lock );
}
}
public function savepointRelease ( int $index = null ) : bool {
$index = $index ? ? $this -> transDepth ;
$out = parent :: savepointRelease ( $index );
if ( $index == $this -> transStart ) {
$this -> exec ( " COMMIT " );
$this -> transStart = 0 ;
}
return $out ;
}
public function savepointUndo ( int $index = null ) : bool {
$index = $index ? ? $this -> transDepth ;
$out = parent :: savepointUndo ( $index );
if ( $index == $this -> transStart ) {
$this -> exec ( " ROLLBACK " );
$this -> transStart = 0 ;
}
return $out ;
}
protected function lock () : bool {
$tables = $this -> query ( " SELECT table_name as name from information_schema.tables where table_schema = database() and table_name like 'arsse_%' " ) -> getAll ();
if ( $tables ) {
$tables = array_column ( $tables , " name " );
$tables = array_map ( function ( $table ) {
$table = str_replace ( '"' , '""' , $table );
return " \" $table\ " write " ;
}, $tables );
$tables = implode ( " , " , $tables );
try {
$this -> exec ( " SET lock_wait_timeout = 1; LOCK TABLES $tables " );
} finally {
2019-01-23 21:31:54 +00:00
$this -> exec ( " SET lock_wait_timeout = " . self :: lockTimeout ());
2018-12-20 23:06:28 +00:00
}
}
return true ;
}
protected function unlock ( bool $rollback = false ) : bool {
$this -> exec ( " UNLOCK TABLES " );
return true ;
}
2019-01-23 21:31:54 +00:00
protected static function lockTimeout () : int {
return ( int ) max ( min ( ceil ( Arsse :: $conf -> dbTimeoutLock ? ? 31536000 ), 31536000 ), 1 );
}
2018-12-20 23:06:28 +00:00
public function __destruct () {
if ( isset ( $this -> db )) {
$this -> db -> close ();
unset ( $this -> db );
}
}
public static function driverName () : string {
return Arsse :: $lang -> msg ( " Driver.Db.MySQL.Name " );
}
public static function requirementsMet () : bool {
2019-01-14 04:17:19 +00:00
return class_exists ( " mysqli " );
2018-12-20 23:06:28 +00:00
}
2020-01-20 18:34:03 +00:00
protected function makeConnection ( string $db , string $user , string $password , string $host , int $port , string $socket ) : void {
2022-01-11 22:54:02 +00:00
$drv = new \mysqli_driver ;
$drv -> report_mode = \MYSQLI_REPORT_OFF ;
2019-01-23 21:31:54 +00:00
$this -> db = mysqli_init ();
2021-03-02 16:04:21 +00:00
$this -> db -> options ( \MYSQLI_SET_CHARSET_NAME , " utf8mb4 " );
$this -> db -> options ( \MYSQLI_OPT_INT_AND_FLOAT_NATIVE , false );
2019-01-23 21:31:54 +00:00
$this -> db -> options ( \MYSQLI_OPT_CONNECT_TIMEOUT , ceil ( Arsse :: $conf -> dbTimeoutConnect ));
@ $this -> db -> real_connect ( $host , $user , $password , $db , $port , $socket );
2019-01-15 15:51:55 +00:00
if ( $this -> db -> connect_errno ) {
2020-03-01 20:16:50 +00:00
[ $excClass , $excMsg , $excData ] = $this -> buildConnectionException ( $this -> db -> connect_errno , $this -> db -> connect_error );
2019-01-15 15:51:55 +00:00
throw new $excClass ( $excMsg , $excData );
2019-01-11 00:01:32 +00:00
}
2019-01-15 15:51:55 +00:00
$this -> db -> set_charset ( " utf8mb4 " );
2018-12-20 23:06:28 +00:00
}
2019-01-11 00:01:32 +00:00
public function exec ( string $query ) : bool {
2019-01-14 04:17:19 +00:00
$this -> dispatch ( $query , true );
2019-01-11 00:01:32 +00:00
return true ;
2018-12-20 23:06:28 +00:00
}
2019-01-14 04:17:19 +00:00
protected function dispatch ( string $query , bool $multi = false ) {
if ( $multi ) {
$this -> db -> multi_query ( $query );
} else {
$this -> db -> real_query ( $query );
}
$e = null ;
do {
if ( $this -> db -> sqlstate !== " 00000 " ) {
if ( $this -> db -> sqlstate === " HY000 " ) {
2020-03-01 20:16:50 +00:00
[ $excClass , $excMsg , $excData ] = $this -> buildEngineException ( $this -> db -> errno , $this -> db -> error );
2019-01-14 04:17:19 +00:00
} else {
2020-03-01 20:16:50 +00:00
[ $excClass , $excMsg , $excData ] = $this -> buildStandardException ( $this -> db -> sqlstate , $this -> db -> error );
2019-01-14 04:17:19 +00:00
}
2020-03-01 20:16:50 +00:00
$e = new $excClass ( $excMsg , $excData , $e );
2019-01-11 00:01:32 +00:00
}
2019-01-14 04:17:19 +00:00
$r = $this -> db -> store_result ();
} while ( $this -> db -> more_results () && $this -> db -> next_result ());
if ( $e ) {
throw $e ;
} else {
return $r ;
2019-01-11 00:01:32 +00:00
}
2018-12-20 23:06:28 +00:00
}
public function query ( string $query ) : \JKingWeb\Arsse\Db\Result {
2019-01-11 00:01:32 +00:00
$r = $this -> dispatch ( $query );
$rows = ( int ) $this -> db -> affected_rows ;
$id = ( int ) $this -> db -> insert_id ;
2019-01-14 04:17:19 +00:00
return new Result ( $r , [ $rows , $id ]);
2018-12-20 23:06:28 +00:00
}
public function prepareArray ( string $query , array $paramTypes ) : \JKingWeb\Arsse\Db\Statement {
2019-01-11 00:01:32 +00:00
return new Statement ( $this -> db , $query , $paramTypes , $this -> packetSize );
2018-12-20 23:06:28 +00:00
}
2019-03-02 03:36:25 +00:00
public function literalString ( string $str ) : string {
return " ' " . $this -> db -> real_escape_string ( $str ) . " ' " ;
}
2019-07-26 13:37:51 +00:00
public function maintenance () : bool {
// with MySQL each table must be analyzed separately, so we first have to get a list of tables
2022-04-25 13:56:13 +00:00
foreach ( $this -> query ( " SHOW TABLES like 'arsse%' " ) as $table ) {
2019-07-26 13:37:51 +00:00
$table = array_pop ( $table );
2021-06-24 15:58:50 +00:00
if ( ! preg_match ( " /^arsse_[a-z_]+ $ /D " , $table )) {
2019-07-26 13:37:51 +00:00
// table is not one of ours
continue ; // @codeCoverageIgnore
}
$this -> query ( " ANALYZE TABLE $table " );
}
return true ;
}
2018-12-20 23:06:28 +00:00
}