<?php /** * CodeIgniter * * An open source application development framework for PHP * * This content is released under the MIT License (MIT) * * Copyright (c) 2014-2019 British Columbia Institute of Technology * Copyright (c) 2019-2020 CodeIgniter Foundation * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * @package CodeIgniter * @author CodeIgniter Dev Team * @copyright 2019-2020 CodeIgniter Foundation * @license https://opensource.org/licenses/MIT MIT License * @link https://codeigniter.com * @since Version 4.0.0 * @filesource */ namespace CodeIgniter\Database; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Events\Events; /** * Class BaseConnection */ abstract class BaseConnection implements ConnectionInterface { /** * Data Source Name / Connect string * * @var string */ protected $DSN; /** * Database port * * @var integer */ protected $port = ''; /** * Hostname * * @var string */ protected $hostname; /** * Username * * @var string */ protected $username; /** * Password * * @var string */ protected $password; /** * Database name * * @var string */ protected $database; /** * Database driver * * @var string */ protected $DBDriver = 'MySQLi'; /** * Sub-driver * * @used-by CI_DB_pdo_driver * @var string */ protected $subdriver; /** * Table prefix * * @var string */ protected $DBPrefix = ''; /** * Persistent connection flag * * @var boolean */ protected $pConnect = false; /** * Debug flag * * Whether to display error messages. * * @var boolean */ protected $DBDebug = false; /** * Should we cache results? * * @var boolean */ protected $cacheOn = false; /** * Path to store cache files. * * @var string */ protected $cacheDir; /** * Character set * * @var string */ protected $charset = 'utf8'; /** * Collation * * @var string */ protected $DBCollat = 'utf8_general_ci'; /** * Swap Prefix * * @var string */ protected $swapPre = ''; /** * Encryption flag/data * * @var mixed */ protected $encrypt = false; /** * Compression flag * * @var boolean */ protected $compress = false; /** * Strict ON flag * * Whether we're running in strict SQL mode. * * @var boolean */ protected $strictOn; /** * Settings for a failover connection. * * @var array */ protected $failover = []; //-------------------------------------------------------------------- /** * The last query object that was executed * on this connection. * * @var array */ protected $lastQuery; /** * Connection ID * * @var object|resource */ public $connID = false; /** * Result ID * * @var object|resource */ public $resultID = false; /** * Protect identifiers flag * * @var boolean */ public $protectIdentifiers = true; /** * List of reserved identifiers * * Identifiers that must NOT be escaped. * * @var array */ protected $reservedIdentifiers = ['*']; /** * Identifier escape character * * @var string */ public $escapeChar = '"'; /** * ESCAPE statement string * * @var string */ public $likeEscapeStr = " ESCAPE '%s' "; /** * ESCAPE character * * @var string */ public $likeEscapeChar = '!'; /** * Holds previously looked up data * for performance reasons. * * @var array */ public $dataCache = []; /** * Microtime when connection was made * * @var float */ protected $connectTime; /** * How long it took to establish connection. * * @var float */ protected $connectDuration; /** * If true, no queries will actually be * ran against the database. * * @var boolean */ protected $pretend = false; /** * Transaction enabled flag * * @var boolean */ public $transEnabled = true; /** * Strict transaction mode flag * * @var boolean */ public $transStrict = true; /** * Transaction depth level * * @var integer */ protected $transDepth = 0; /** * Transaction status flag * * Used with transactions to determine if a rollback should occur. * * @var boolean */ protected $transStatus = true; /** * Transaction failure flag * * Used with transactions to determine if a transaction has failed. * * @var boolean */ protected $transFailure = false; /** * Array of table aliases. * * @var array */ protected $aliasedTables = []; //-------------------------------------------------------------------- /** * Saves our connection settings. * * @param array $params */ public function __construct(array $params) { foreach ($params as $key => $value) { if (property_exists($this, $key)) { $this->$key = $value; } } } //-------------------------------------------------------------------- /** * Initializes the database connection/settings. * * @return mixed|void * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function initialize() { /* If an established connection is available, then there's * no need to connect and select the database. * * Depending on the database driver, conn_id can be either * boolean TRUE, a resource or an object. */ if ($this->connID) { return; } //-------------------------------------------------------------------- $this->connectTime = microtime(true); // Connect to the database and set the connection ID $this->connID = $this->connect($this->pConnect); // No connection resource? Check if there is a failover else throw an error if (! $this->connID) { // Check if there is a failover set if (! empty($this->failover) && is_array($this->failover)) { // Go over all the failovers foreach ($this->failover as $failover) { // Replace the current settings with those of the failover foreach ($failover as $key => $val) { if (property_exists($this, $key)) { $this->$key = $val; } } // Try to connect $this->connID = $this->connect($this->pConnect); // If a connection is made break the foreach loop if ($this->connID) { break; } } } // We still don't have a connection? if (! $this->connID) { throw new DatabaseException('Unable to connect to the database.'); } } $this->connectDuration = microtime(true) - $this->connectTime; } //-------------------------------------------------------------------- /** * Connect to the database. * * @param boolean $persistent * @return mixed */ abstract public function connect(bool $persistent = false); //-------------------------------------------------------------------- /** * Close the database connection. * * @return void */ public function close() { if ($this->connID) { $this->_close(); $this->connID = false; } } //-------------------------------------------------------------------- /** * Platform dependent way method for closing the connection. * * @return mixed */ abstract protected function _close(); //-------------------------------------------------------------------- /** * Create a persistent database connection. * * @return mixed */ public function persistentConnect() { return $this->connect(true); } //-------------------------------------------------------------------- /** * Keep or establish the connection if no queries have been sent for * a length of time exceeding the server's idle timeout. * * @return mixed */ abstract public function reconnect(); //-------------------------------------------------------------------- /** * Returns the actual connection object. If both a 'read' and 'write' * connection has been specified, you can pass either term in to * get that connection. If you pass either alias in and only a single * connection is present, it must return the sole connection. * * @param string|null $alias * * @return mixed */ public function getConnection(string $alias = null) { //@todo work with read/write connections return $this->connID; } //-------------------------------------------------------------------- /** * Select a specific database table to use. * * @param string $databaseName * * @return mixed */ abstract public function setDatabase(string $databaseName); //-------------------------------------------------------------------- /** * Returns the name of the current database being used. * * @return string */ public function getDatabase(): string { return empty($this->database) ? '' : $this->database; } //-------------------------------------------------------------------- /** * Set DB Prefix * * Set's the DB Prefix to something new without needing to reconnect * * @param string $prefix The prefix * * @return string */ public function setPrefix(string $prefix = ''): string { return $this->DBPrefix = $prefix; } //-------------------------------------------------------------------- /** * Returns the database prefix. * * @return string */ public function getPrefix(): string { return $this->DBPrefix; } //-------------------------------------------------------------------- /** * Returns the last error encountered by this connection. * * @return mixed */ public function getError() { } //-------------------------------------------------------------------- /** * The name of the platform in use (MySQLi, mssql, etc) * * @return string */ public function getPlatform(): string { return $this->DBDriver; } //-------------------------------------------------------------------- /** * Returns a string containing the version of the database being used. * * @return string */ abstract public function getVersion(): string; //-------------------------------------------------------------------- /** * Sets the Table Aliases to use. These are typically * collected during use of the Builder, and set here * so queries are built correctly. * * @param array $aliases * * @return $this */ public function setAliasedTables(array $aliases) { $this->aliasedTables = $aliases; return $this; } //-------------------------------------------------------------------- /** * Add a table alias to our list. * * @param string $table * * @return $this */ public function addTableAlias(string $table) { if (! in_array($table, $this->aliasedTables)) { $this->aliasedTables[] = $table; } return $this; } /** * Executes the query against the database. * * @param $sql * * @return mixed */ abstract protected function execute(string $sql); //-------------------------------------------------------------------- /** * Orchestrates a query against the database. Queries must use * Database\Statement objects to store the query and build it. * This method works with the cache. * * Should automatically handle different connections for read/write * queries if needed. * * @param string $sql * @param mixed ...$binds * @param boolean $setEscapeFlags * @param string $queryClass * * @return BaseResult|Query|false */ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = 'CodeIgniter\\Database\\Query') { if (empty($this->connID)) { $this->initialize(); } $resultClass = str_replace('Connection', 'Result', get_class($this)); /** * @var Query $query */ $query = new $queryClass($this); $query->setQuery($sql, $binds, $setEscapeFlags); if (! empty($this->swapPre) && ! empty($this->DBPrefix)) { $query->swapPrefix($this->DBPrefix, $this->swapPre); } $startTime = microtime(true); // Always save the last query so we can use // the getLastQuery() method. $this->lastQuery = $query; // Run the query for real if (! $this->pretend && false === ($this->resultID = $this->simpleQuery($query->getQuery()))) { $query->setDuration($startTime, $startTime); // This will trigger a rollback if transactions are being used if ($this->transDepth !== 0) { $this->transStatus = false; } if ($this->DBDebug) { // We call this function in order to roll-back queries // if transactions are enabled. If we don't call this here // the error message will trigger an exit, causing the // transactions to remain in limbo. while ($this->transDepth !== 0) { $transDepth = $this->transDepth; $this->transComplete(); if ($transDepth === $this->transDepth) { log_message('error', 'Database: Failure during an automated transaction commit/rollback!'); break; } } return false; } if (! $this->pretend) { // Let others do something with this query. Events::trigger('DBQuery', $query); } return new $resultClass($this->connID, $this->resultID); } $query->setDuration($startTime); if (! $this->pretend) { // Let others do something with this query Events::trigger('DBQuery', $query); } // If $pretend is true, then we just want to return // the actual query object here. There won't be // any results to return. return $this->pretend ? $query : new $resultClass($this->connID, $this->resultID); } //-------------------------------------------------------------------- /** * Performs a basic query against the database. No binding or caching * is performed, nor are transactions handled. Simply takes a raw * query string and returns the database-specific result id. * * @param string $sql * * @return mixed */ public function simpleQuery(string $sql) { if (empty($this->connID)) { $this->initialize(); } return $this->execute($sql); } //-------------------------------------------------------------------- /** * Disable Transactions * * This permits transactions to be disabled at run-time. * * @return void */ public function transOff() { $this->transEnabled = false; } //-------------------------------------------------------------------- /** * Enable/disable Transaction Strict Mode * * When strict mode is enabled, if you are running multiple groups of * transactions, if one group fails all subsequent groups will be * rolled back. * * If strict mode is disabled, each group is treated autonomously, * meaning a failure of one group will not affect any others * * @param boolean $mode = true * * @return $this */ public function transStrict(bool $mode = true) { $this->transStrict = $mode; return $this; } //-------------------------------------------------------------------- /** * Start Transaction * * @param boolean $test_mode = FALSE * @return boolean */ public function transStart(bool $test_mode = false): bool { if (! $this->transEnabled) { return false; } return $this->transBegin($test_mode); } //-------------------------------------------------------------------- /** * Complete Transaction * * @return boolean */ public function transComplete(): bool { if (! $this->transEnabled) { return false; } // The query() function will set this flag to FALSE in the event that a query failed if ($this->transStatus === false || $this->transFailure === true) { $this->transRollback(); // If we are NOT running in strict mode, we will reset // the _trans_status flag so that subsequent groups of // transactions will be permitted. if ($this->transStrict === false) { $this->transStatus = true; } // log_message('debug', 'DB Transaction Failure'); return false; } return $this->transCommit(); } //-------------------------------------------------------------------- /** * Lets you retrieve the transaction flag to determine if it has failed * * @return boolean */ public function transStatus(): bool { return $this->transStatus; } //-------------------------------------------------------------------- /** * Begin Transaction * * @param boolean $test_mode * @return boolean */ public function transBegin(bool $test_mode = false): bool { if (! $this->transEnabled) { return false; } // When transactions are nested we only begin/commit/rollback the outermost ones elseif ($this->transDepth > 0) { $this->transDepth ++; return true; } if (empty($this->connID)) { $this->initialize(); } // Reset the transaction failure flag. // If the $test_mode flag is set to TRUE transactions will be rolled back // even if the queries produce a successful result. $this->transFailure = ($test_mode === true); if ($this->_transBegin()) { $this->transDepth ++; return true; } return false; } //-------------------------------------------------------------------- /** * Commit Transaction * * @return boolean */ public function transCommit(): bool { if (! $this->transEnabled || $this->transDepth === 0) { return false; } // When transactions are nested we only begin/commit/rollback the outermost ones elseif ($this->transDepth > 1 || $this->_transCommit()) { $this->transDepth --; return true; } return false; } //-------------------------------------------------------------------- /** * Rollback Transaction * * @return boolean */ public function transRollback(): bool { if (! $this->transEnabled || $this->transDepth === 0) { return false; } // When transactions are nested we only begin/commit/rollback the outermost ones elseif ($this->transDepth > 1 || $this->_transRollback()) { $this->transDepth --; return true; } return false; } //-------------------------------------------------------------------- /** * Begin Transaction * * @return boolean */ abstract protected function _transBegin(): bool; //-------------------------------------------------------------------- /** * Commit Transaction * * @return boolean */ abstract protected function _transCommit(): bool; //-------------------------------------------------------------------- /** * Rollback Transaction * * @return boolean */ abstract protected function _transRollback(): bool; //-------------------------------------------------------------------- /** * Returns an instance of the query builder for this connection. * * @param string|array $tableName * * @return BaseBuilder * @throws DatabaseException */ public function table($tableName) { if (empty($tableName)) { throw new DatabaseException('You must set the database table to be used with your query.'); } $className = str_replace('Connection', 'Builder', get_class($this)); return new $className($tableName, $this); } //-------------------------------------------------------------------- /** * Creates a prepared statement with the database that can then * be used to execute multiple statements against. Within the * closure, you would build the query in any normal way, though * the Query Builder is the expected manner. * * Example: * $stmt = $db->prepare(function($db) * { * return $db->table('users') * ->where('id', 1) * ->get(); * }) * * @param \Closure $func * @param array $options Passed to the prepare() method * * @return BasePreparedQuery|null */ public function prepare(\Closure $func, array $options = []) { if (empty($this->connID)) { $this->initialize(); } $this->pretend(); $sql = $func($this); $this->pretend(false); if ($sql instanceof QueryInterface) { $sql = $sql->getOriginalQuery(); } $class = str_ireplace('Connection', 'PreparedQuery', get_class($this)); /** * @var BasePreparedQuery $class */ $class = new $class($this); return $class->prepare($sql, $options); } //-------------------------------------------------------------------- /** * Returns the last query's statement object. * * @return mixed */ public function getLastQuery() { return $this->lastQuery; } //-------------------------------------------------------------------- /** * Returns a string representation of the last query's statement object. * * @return string */ public function showLastQuery(): string { return (string) $this->lastQuery; } //-------------------------------------------------------------------- /** * Returns the time we started to connect to this database in * seconds with microseconds. * * Used by the Debug Toolbar's timeline. * * @return float|null */ public function getConnectStart(): ?float { return $this->connectTime; } //-------------------------------------------------------------------- /** * Returns the number of seconds with microseconds that it took * to connect to the database. * * Used by the Debug Toolbar's timeline. * * @param integer $decimals * * @return string */ public function getConnectDuration(int $decimals = 6): string { return number_format($this->connectDuration, $decimals); } //-------------------------------------------------------------------- /** * Protect Identifiers * * This function is used extensively by the Query Builder class, and by * a couple functions in this class. * It takes a column or table name (optionally with an alias) and inserts * the table prefix onto it. Some logic is necessary in order to deal with * column names that include the path. Consider a query like this: * * SELECT hostname.database.table.column AS c FROM hostname.database.table * * Or a query with aliasing: * * SELECT m.member_id, m.member_name FROM members AS m * * Since the column name can include up to four segments (host, DB, table, column) * or also have an alias prefix, we need to do a bit of work to figure this out and * insert the table prefix (if it exists) in the proper position, and escape only * the correct identifiers. * * @param string|array $item * @param boolean $prefixSingle * @param boolean $protectIdentifiers * @param boolean $fieldExists * * @return string|array */ public function protectIdentifiers($item, bool $prefixSingle = false, bool $protectIdentifiers = null, bool $fieldExists = true) { if (! is_bool($protectIdentifiers)) { $protectIdentifiers = $this->protectIdentifiers; } if (is_array($item)) { $escaped_array = []; foreach ($item as $k => $v) { $escaped_array[$this->protectIdentifiers($k)] = $this->protectIdentifiers($v, $prefixSingle, $protectIdentifiers, $fieldExists); } return $escaped_array; } // This is basically a bug fix for queries that use MAX, MIN, etc. // If a parenthesis is found we know that we do not need to // escape the data or add a prefix. There's probably a more graceful // way to deal with this, but I'm not thinking of it // // Added exception for single quotes as well, we don't want to alter // literal strings. if (strcspn($item, "()'") !== strlen($item)) { return $item; } // Convert tabs or multiple spaces into single spaces $item = preg_replace('/\s+/', ' ', trim($item)); // If the item has an alias declaration we remove it and set it aside. // Note: strripos() is used in order to support spaces in table names if ($offset = strripos($item, ' AS ')) { $alias = ($protectIdentifiers) ? substr($item, $offset, 4) . $this->escapeIdentifiers(substr($item, $offset + 4)) : substr($item, $offset); $item = substr($item, 0, $offset); } elseif ($offset = strrpos($item, ' ')) { $alias = ($protectIdentifiers) ? ' ' . $this->escapeIdentifiers(substr($item, $offset + 1)) : substr($item, $offset); $item = substr($item, 0, $offset); } else { $alias = ''; } // Break the string apart if it contains periods, then insert the table prefix // in the correct location, assuming the period doesn't indicate that we're dealing // with an alias. While we're at it, we will escape the components if (strpos($item, '.') !== false) { $parts = explode('.', $item); // Does the first segment of the exploded item match // one of the aliases previously identified? If so, // we have nothing more to do other than escape the item // // NOTE: The ! empty() condition prevents this method // from breaking when QB isn't enabled. if (! empty($this->aliasedTables) && in_array($parts[0], $this->aliasedTables)) { if ($protectIdentifiers === true) { foreach ($parts as $key => $val) { if (! in_array($val, $this->reservedIdentifiers)) { $parts[$key] = $this->escapeIdentifiers($val); } } $item = implode('.', $parts); } return $item . $alias; } // Is there a table prefix defined in the config file? If not, no need to do anything if ($this->DBPrefix !== '') { // We now add the table prefix based on some logic. // Do we have 4 segments (hostname.database.table.column)? // If so, we add the table prefix to the column name in the 3rd segment. if (isset($parts[3])) { $i = 2; } // Do we have 3 segments (database.table.column)? // If so, we add the table prefix to the column name in 2nd position elseif (isset($parts[2])) { $i = 1; } // Do we have 2 segments (table.column)? // If so, we add the table prefix to the column name in 1st segment else { $i = 0; } // This flag is set when the supplied $item does not contain a field name. // This can happen when this function is being called from a JOIN. if ($fieldExists === false) { $i++; } // Verify table prefix and replace if necessary if ($this->swapPre !== '' && strpos($parts[$i], $this->swapPre) === 0) { $parts[$i] = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $parts[$i]); } // We only add the table prefix if it does not already exist elseif (strpos($parts[$i], $this->DBPrefix) !== 0) { $parts[$i] = $this->DBPrefix . $parts[$i]; } // Put the parts back together $item = implode('.', $parts); } if ($protectIdentifiers === true) { $item = $this->escapeIdentifiers($item); } return $item . $alias; } // In some cases, especially 'from', we end up running through // protect_identifiers twice. This algorithm won't work when // it contains the escapeChar so strip it out. $item = trim($item, $this->escapeChar); // Is there a table prefix? If not, no need to insert it if ($this->DBPrefix !== '') { // Verify table prefix and replace if necessary if ($this->swapPre !== '' && strpos($item, $this->swapPre) === 0) { $item = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $item); } // Do we prefix an item with no segments? elseif ($prefixSingle === true && strpos($item, $this->DBPrefix) !== 0) { $item = $this->DBPrefix . $item; } } if ($protectIdentifiers === true && ! in_array($item, $this->reservedIdentifiers)) { $item = $this->escapeIdentifiers($item); } return $item . $alias; } //-------------------------------------------------------------------- /** * Escape the SQL Identifiers * * This function escapes column and table names * * @param mixed $item * * @return mixed */ public function escapeIdentifiers($item) { if ($this->escapeChar === '' || empty($item) || in_array($item, $this->reservedIdentifiers)) { return $item; } elseif (is_array($item)) { foreach ($item as $key => $value) { $item[$key] = $this->escapeIdentifiers($value); } return $item; } // Avoid breaking functions and literal values inside queries elseif (ctype_digit($item) || $item[0] === "'" || ( $this->escapeChar !== '"' && $item[0] === '"') || strpos($item, '(') !== false ) { return $item; } static $preg_ec = []; if (empty($preg_ec)) { if (is_array($this->escapeChar)) { $preg_ec = [ preg_quote($this->escapeChar[0], '/'), preg_quote($this->escapeChar[1], '/'), $this->escapeChar[0], $this->escapeChar[1], ]; } else { $preg_ec[0] = $preg_ec[1] = preg_quote($this->escapeChar, '/'); $preg_ec[2] = $preg_ec[3] = $this->escapeChar; } } foreach ($this->reservedIdentifiers as $id) { if (strpos($item, '.' . $id) !== false) { return preg_replace('/' . $preg_ec[0] . '?([^' . $preg_ec[1] . '\.]+)' . $preg_ec[1] . '?\./i', $preg_ec[2] . '$1' . $preg_ec[3] . '.', $item); } } return preg_replace('/' . $preg_ec[0] . '?([^' . $preg_ec[1] . '\.]+)' . $preg_ec[1] . '?(\.)?/i', $preg_ec[2] . '$1' . $preg_ec[3] . '$2', $item); } //-------------------------------------------------------------------- /** * DB Prefix * * Prepends a database prefix if one exists in configuration * * @param string $table the table * * @return string * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function prefixTable(string $table = ''): string { if ($table === '') { throw new DatabaseException('A table name is required for that operation.'); } return $this->DBPrefix . $table; } //-------------------------------------------------------------------- /** * Returns the total number of rows affected by this query. * * @return mixed */ abstract public function affectedRows(): int; //-------------------------------------------------------------------- /** * "Smart" Escape String * * Escapes data based on type. * Sets boolean and null types * * @param mixed $str * * @return mixed */ public function escape($str) { if (is_array($str)) { return array_map([&$this, 'escape'], $str); } else if (is_string($str) || ( is_object($str) && method_exists($str, '__toString'))) { return "'" . $this->escapeString($str) . "'"; } else if (is_bool($str)) { return ($str === false) ? 0 : 1; } else if (is_numeric($str) && $str < 0) { return "'{$str}'"; } else if ($str === null) { return 'NULL'; } return $str; } //-------------------------------------------------------------------- /** * Escape String * * @param string|string[] $str Input string * @param boolean $like Whether or not the string will be used in a LIKE condition * @return string|string[] */ public function escapeString($str, bool $like = false) { if (is_array($str)) { foreach ($str as $key => $val) { $str[$key] = $this->escapeString($val, $like); } return $str; } $str = $this->_escapeString($str); // escape LIKE condition wildcards if ($like === true) { return str_replace([ $this->likeEscapeChar, '%', '_', ], [ $this->likeEscapeChar . $this->likeEscapeChar, $this->likeEscapeChar . '%', $this->likeEscapeChar . '_', ], $str ); } return $str; } //-------------------------------------------------------------------- /** * Escape LIKE String * * Calls the individual driver for platform * specific escaping for LIKE conditions * * @param string|string[] * @return string|string[] */ public function escapeLikeString($str) { return $this->escapeString($str, true); } //-------------------------------------------------------------------- /** * Platform independent string escape. * * Will likely be overridden in child classes. * * @param string $str * * @return string */ protected function _escapeString(string $str): string { return str_replace("'", "''", remove_invisible_characters($str, false)); } //-------------------------------------------------------------------- /** * This function enables you to call PHP database functions that are not natively included * in CodeIgniter, in a platform independent manner. * * @param string $functionName * @param array ...$params * * @return boolean * @throws DatabaseException */ public function callFunction(string $functionName, ...$params): bool { $driver = ($this->DBDriver === 'postgre' ? 'pg' : strtolower($this->DBDriver)) . '_'; if (false === strpos($driver, $functionName)) { $functionName = $driver . $functionName; } if (! function_exists($functionName)) { if ($this->DBDebug) { throw new DatabaseException('This feature is not available for the database you are using.'); } return false; } return $functionName(...$params); } //-------------------------------------------------------------------- //-------------------------------------------------------------------- // META Methods //-------------------------------------------------------------------- /** * Returns an array of table names * * @param boolean $constrainByPrefix = FALSE * @return boolean|array * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function listTables(bool $constrainByPrefix = false) { // Is there a cached result? if (isset($this->dataCache['table_names']) && $this->dataCache['table_names']) { return $constrainByPrefix ? preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names']) : $this->dataCache['table_names']; } if (false === ($sql = $this->_listTables($constrainByPrefix))) { if ($this->DBDebug) { throw new DatabaseException('This feature is not available for the database you are using.'); } return false; } $this->dataCache['table_names'] = []; $query = $this->query($sql); foreach ($query->getResultArray() as $row) { // Do we know from which column to get the table name? if (! isset($key)) { if (isset($row['table_name'])) { $key = 'table_name'; } elseif (isset($row['TABLE_NAME'])) { $key = 'TABLE_NAME'; } else { /* We have no other choice but to just get the first element's key. * Due to array_shift() accepting its argument by reference, if * E_STRICT is on, this would trigger a warning. So we'll have to * assign it first. */ $key = array_keys($row); $key = array_shift($key); } } $this->dataCache['table_names'][] = $row[$key]; } return $this->dataCache['table_names']; } //-------------------------------------------------------------------- /** * Determine if a particular table exists * * @param string $tableName * @return boolean */ public function tableExists(string $tableName): bool { return in_array($this->protectIdentifiers($tableName, true, false, false), $this->listTables()); } //-------------------------------------------------------------------- /** * Fetch Field Names * * @param string $table Table name * * @return array|false * @throws DatabaseException */ public function getFieldNames(string $table) { // Is there a cached result? if (isset($this->dataCache['field_names'][$table])) { return $this->dataCache['field_names'][$table]; } if (empty($this->connID)) { $this->initialize(); } if (false === ($sql = $this->_listColumns($table))) { if ($this->DBDebug) { throw new DatabaseException('This feature is not available for the database you are using.'); } return false; } $query = $this->query($sql); $this->dataCache['field_names'][$table] = []; foreach ($query->getResultArray() as $row) { // Do we know from where to get the column's name? if (! isset($key)) { if (isset($row['column_name'])) { $key = 'column_name'; } elseif (isset($row['COLUMN_NAME'])) { $key = 'COLUMN_NAME'; } else { // We have no other choice but to just get the first element's key. $key = key($row); } } $this->dataCache['field_names'][$table][] = $row[$key]; } return $this->dataCache['field_names'][$table]; } //-------------------------------------------------------------------- /** * Determine if a particular field exists * * @param string $fieldName * @param string $tableName * @return boolean */ public function fieldExists(string $fieldName, string $tableName): bool { return in_array($fieldName, $this->getFieldNames($tableName)); } //-------------------------------------------------------------------- /** * Returns an object with field data * * @param string $table the table name * @return array|false */ public function getFieldData(string $table) { $fields = $this->_fieldData($this->protectIdentifiers($table, true, false, false)); return $fields ?? false; } //-------------------------------------------------------------------- /** * Returns an object with key data * * @param string $table the table name * @return array|false */ public function getIndexData(string $table) { $fields = $this->_indexData($this->protectIdentifiers($table, true, false, false)); return $fields ?? false; } //-------------------------------------------------------------------- /** * Returns an object with foreign key data * * @param string $table the table name * @return array|false */ public function getForeignKeyData(string $table) { $fields = $this->_foreignKeyData($this->protectIdentifiers($table, true, false, false)); return $fields ?? false; } //-------------------------------------------------------------------- /** * Disables foreign key checks temporarily. */ public function disableForeignKeyChecks() { $sql = $this->_disableForeignKeyChecks(); return $this->query($sql); } //-------------------------------------------------------------------- /** * Enables foreign key checks temporarily. */ public function enableForeignKeyChecks() { $sql = $this->_enableForeignKeyChecks(); return $this->query($sql); } //-------------------------------------------------------------------- /** * Allows the engine to be set into a mode where queries are not * actually executed, but they are still generated, timed, etc. * * This is primarily used by the prepared query functionality. * * @param boolean $pretend * * @return $this */ public function pretend(bool $pretend = true) { $this->pretend = $pretend; return $this; } //-------------------------------------------------------------------- /** * Empties our data cache. Especially helpful during testing. * * @return $this */ public function resetDataCache() { $this->dataCache = []; return $this; } //-------------------------------------------------------------------- /** * Returns the last error code and message. * * Must return an array with keys 'code' and 'message': * * return ['code' => null, 'message' => null); * * @return array */ abstract public function error(): array; //-------------------------------------------------------------------- /** * Insert ID * * @return integer */ abstract public function insertID(): int; //-------------------------------------------------------------------- /** * Generates the SQL for listing tables in a platform-dependent manner. * * @param boolean $constrainByPrefix * * @return string */ abstract protected function _listTables(bool $constrainByPrefix = false): string; //-------------------------------------------------------------------- /** * Generates a platform-specific query string so that the column names can be fetched. * * @param string $table * * @return string */ abstract protected function _listColumns(string $table = ''): string; //-------------------------------------------------------------------- /** * Platform-specific field data information. * * @param string $table * @see getFieldData() * @return array */ abstract protected function _fieldData(string $table): array; //-------------------------------------------------------------------- /** * Platform-specific index data. * * @param string $table * @see getIndexData() * @return array */ abstract protected function _indexData(string $table): array; //-------------------------------------------------------------------- /** * Platform-specific foreign keys data. * * @param string $table * @see getForeignKeyData() * @return array */ abstract protected function _foreignKeyData(string $table): array; //-------------------------------------------------------------------- /** * Accessor for properties if they exist. * * @param string $key * * @return mixed */ public function __get(string $key) { if (property_exists($this, $key)) { return $this->$key; } return null; } //-------------------------------------------------------------------- /** * Checker for properties existence. * * @param string $key * * @return boolean */ public function __isset(string $key): bool { return property_exists($this, $key); } //-------------------------------------------------------------------- }