cachedStatements = []; } /** * Retrieve a prepared statement for a given SQL string, or return an already prepared version if * one exists for the given query * * @param string $sql * @return PDOStatementHandle|false */ public function getOrPrepareStatement($sql) { // Return cached statements if (isset($this->cachedStatements[$sql])) { return $this->cachedStatements[$sql]; } // Generate new statement $statement = $this->pdoConnection->prepare( $sql, [PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY] ); // Wrap in a PDOStatementHandle, to cache column metadata $statementHandle = ($statement === false) ? false : new PDOStatementHandle($statement); // Only cache select statements if (preg_match('/^(\s*)select\b/i', $sql)) { $this->cachedStatements[$sql] = $statementHandle; } return $statementHandle; } /** * Is PDO running in emulated mode * * @return boolean */ public static function is_emulate_prepare() { return self::config()->get('emulate_prepare'); } public function connect($parameters, $selectDB = false) { Deprecation::notice('4.5', 'Use native database drivers in favour of PDO. ' . 'https://github.com/silverstripe/silverstripe-framework/issues/8598'); $this->flushStatements(); // Note that we don't select the database here until explicitly // requested via selectDatabase $this->driver = $parameters['driver']; // Build DSN string $dsn = []; // Typically this is false, but some drivers will request this if ($selectDB) { // Specify complete file path immediately following driver (SQLLite3) if (!empty($parameters['filepath'])) { $dsn[] = $parameters['filepath']; } elseif (!empty($parameters['database'])) { // Some databases require a selected database at connection (SQLite3, Azure) if ($parameters['driver'] === 'sqlsrv') { $dsn[] = "Database={$parameters['database']}"; } else { $dsn[] = "dbname={$parameters['database']}"; } } } // Syntax for sql server is slightly different if ($parameters['driver'] === 'sqlsrv') { $server = $parameters['server']; if (!empty($parameters['port'])) { $server .= ",{$parameters['port']}"; } $dsn[] = "Server=$server"; } elseif ($parameters['driver'] === 'dblib') { $server = $parameters['server']; if (!empty($parameters['port'])) { $server .= ":{$parameters['port']}"; } $dsn[] = "host={$server}"; } else { if (!empty($parameters['server'])) { // Use Server instead of host for sqlsrv $dsn[] = "host={$parameters['server']}"; } if (!empty($parameters['port'])) { $dsn[] = "port={$parameters['port']}"; } } // Connection charset and collation $connCharset = Config::inst()->get(MySQLDatabase::class, 'connection_charset'); $connCollation = Config::inst()->get(MySQLDatabase::class, 'connection_collation'); // Set charset if given and not null. Can explicitly set to empty string to omit if (!in_array($parameters['driver'], ['sqlsrv', 'pgsql'])) { $charset = isset($parameters['charset']) ? $parameters['charset'] : $connCharset; if (!empty($charset)) { $dsn[] = "charset=$charset"; } } // Connection commands to be run on every re-connection if (!isset($charset)) { $charset = $connCharset; } $options = []; if ($parameters['driver'] === 'mysql') { $options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES ' . $charset . ' COLLATE ' . $connCollation; } // Set SSL options if they are defined if (array_key_exists('ssl_key', $parameters) && array_key_exists('ssl_cert', $parameters) ) { $options[PDO::MYSQL_ATTR_SSL_KEY] = $parameters['ssl_key']; $options[PDO::MYSQL_ATTR_SSL_CERT] = $parameters['ssl_cert']; if (array_key_exists('ssl_ca', $parameters)) { $options[PDO::MYSQL_ATTR_SSL_CA] = $parameters['ssl_ca']; } // use default cipher if not provided $options[PDO::MYSQL_ATTR_SSL_CIPHER] = array_key_exists('ssl_cipher', $parameters) ? $parameters['ssl_cipher'] : self::config()->get('ssl_cipher_default'); } if (static::config()->get('legacy_types')) { $options[PDO::ATTR_STRINGIFY_FETCHES] = true; $options[PDO::ATTR_EMULATE_PREPARES] = true; } else { // Set emulate prepares (unless null / default) $isEmulatePrepares = self::is_emulate_prepare(); if (isset($isEmulatePrepares)) { $options[PDO::ATTR_EMULATE_PREPARES] = (bool)$isEmulatePrepares; } // Disable stringified fetches $options[PDO::ATTR_STRINGIFY_FETCHES] = false; } // May throw a PDOException if fails $this->pdoConnection = new PDO( $this->driver . ':' . implode(';', $dsn), empty($parameters['username']) ? '' : $parameters['username'], empty($parameters['password']) ? '' : $parameters['password'], $options ); // Show selected DB if requested if ($this->pdoConnection && $selectDB && !empty($parameters['database'])) { $this->databaseName = $parameters['database']; } } /** * Return the driver for this connector * E.g. 'mysql', 'sqlsrv', 'pgsql' * * @return string */ public function getDriver() { return $this->driver; } public function getVersion() { return $this->pdoConnection->getAttribute(PDO::ATTR_SERVER_VERSION); } public function escapeString($value) { $value = $this->quoteString($value); // Since the PDO library quotes the value, we should remove this to maintain // consistency with MySQLDatabase::escapeString if (preg_match('/^\'(?.*)\'$/', $value, $matches)) { $value = $matches['value']; } return $value; } public function quoteString($value) { return $this->pdoConnection->quote($value); } /** * Invoked before any query is executed * * @param string $sql */ protected function beforeQuery($sql) { // Reset state $this->rowCount = 0; $this->lastStatementError = null; // Flush if necessary if ($this->isQueryDDL($sql)) { $this->flushStatements(); } } /** * Executes a query that doesn't return a resultset * * @param string $sql The SQL query to execute * @param integer $errorLevel For errors to this query, raise PHP errors * using this error level. * @return int */ public function exec($sql, $errorLevel = E_USER_ERROR) { $this->beforeQuery($sql); // Directly exec this query $result = $this->pdoConnection->exec($sql); // Check for errors if ($result !== false) { return $this->rowCount = $result; } $this->databaseError($this->getLastError(), $errorLevel, $sql); return null; } public function query($sql, $errorLevel = E_USER_ERROR) { $this->beforeQuery($sql); // Directly query against connection $statement = $this->pdoConnection->query($sql); // Generate results if ($statement === false) { $this->databaseError($this->getLastError(), $errorLevel, $sql); } else { return $this->prepareResults(new PDOStatementHandle($statement), $errorLevel, $sql); } } /** * Determines the PDO::PARAM_* type for a given PHP type string * @param string $phpType Type of object in PHP * @return integer PDO Parameter constant value */ public function getPDOParamType($phpType) { switch ($phpType) { case 'boolean': return PDO::PARAM_BOOL; case 'NULL': return PDO::PARAM_NULL; case 'integer': return PDO::PARAM_INT; case 'object': // Allowed if the object or resource has a __toString method case 'resource': case 'float': // Not actually returnable from get_type case 'double': case 'string': return PDO::PARAM_STR; case 'blob': return PDO::PARAM_LOB; case 'array': case 'unknown type': default: throw new InvalidArgumentException("Cannot bind parameter as it is an unsupported type ($phpType)"); } } /** * Bind all parameters to a PDOStatement * * @param PDOStatement $statement * @param array $parameters */ public function bindParameters(PDOStatement $statement, $parameters) { // Bind all parameters $parameterCount = count($parameters); for ($index = 0; $index < $parameterCount; $index++) { $value = $parameters[$index]; $phpType = gettype($value); // Allow overriding of parameter type using an associative array if ($phpType === 'array') { $phpType = $value['type']; $value = $value['value']; } // Check type of parameter $type = $this->getPDOParamType($phpType); if ($type === PDO::PARAM_STR) { $value = (string) $value; } // Bind this value $statement->bindValue($index+1, $value, $type); } } public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) { $this->beforeQuery($sql); // Fetch cached statement, or create it $statementHandle = $this->getOrPrepareStatement($sql); // Error handling if ($statementHandle === false) { $this->databaseError($this->getLastError(), $errorLevel, $sql, $this->parameterValues($parameters)); return null; } // Bind parameters $this->bindParameters($statementHandle->getPDOStatement(), $parameters); $statementHandle->execute($parameters); // Generate results return $this->prepareResults($statementHandle, $errorLevel, $sql); } /** * Given a PDOStatement that has just been executed, generate results * and report any errors * * @param PDOStatementHandle $statement * @param int $errorLevel * @param string $sql * @param array $parameters * @return PDOQuery */ protected function prepareResults(PDOStatementHandle $statement, $errorLevel, $sql, $parameters = []) { // Catch error if ($this->hasError($statement)) { $this->lastStatementError = $statement->errorInfo(); $statement->closeCursor(); $this->databaseError($this->getLastError(), $errorLevel, $sql, $this->parameterValues($parameters)); return null; } // Count and return results $this->rowCount = $statement->rowCount(); return new PDOQuery($statement); } /** * Determine if a resource has an attached error * * @param PDOStatement|PDO $resource the resource to check * @return boolean Flag indicating true if the resource has an error */ protected function hasError($resource) { // No error if no resource if (empty($resource)) { return false; } // If the error code is empty the statement / connection has not been run yet $code = $resource->errorCode(); if (empty($code)) { return false; } // Skip 'ok' and undefined 'warning' types. // @see http://docstore.mik.ua/orelly/java-ent/jenut/ch08_06.htm return $code !== '00000' && $code !== '01000'; } public function getLastError() { $error = null; if ($this->lastStatementError) { $error = $this->lastStatementError; } elseif ($this->hasError($this->pdoConnection)) { $error = $this->pdoConnection->errorInfo(); } if ($error) { return sprintf("%s-%s: %s", $error[0], $error[1], $error[2]); } return null; } public function getGeneratedID($table) { return (int) $this->pdoConnection->lastInsertId(); } public function affectedRows() { return $this->rowCount; } public function selectDatabase($name) { $this->exec("USE \"{$name}\""); $this->databaseName = $name; return true; } public function getSelectedDatabase() { return $this->databaseName; } public function unloadDatabase() { $this->databaseName = null; } public function isActive() { return $this->databaseName && $this->pdoConnection; } public function transactionStart($transactionMode = false, $sessionCharacteristics = false) { $this->inTransaction = true; if ($transactionMode) { $this->query("SET TRANSACTION $transactionMode"); } if ($this->pdoConnection->beginTransaction()) { if ($sessionCharacteristics) { $this->query("SET SESSION CHARACTERISTICS AS TRANSACTION $sessionCharacteristics"); } return true; } return false; } public function transactionEnd() { $this->inTransaction = false; return $this->pdoConnection->commit(); } public function transactionRollback($savepoint = null) { if ($savepoint) { if ($this->supportsSavepoints()) { $this->exec("ROLLBACK TO SAVEPOINT $savepoint"); } else { throw new DatabaseException("Savepoints not supported on this PDO connection"); } } $this->inTransaction = false; return $this->pdoConnection->rollBack(); } public function transactionDepth() { return (int)$this->inTransaction; } public function transactionSavepoint($savepoint = null) { if ($this->supportsSavepoints()) { $this->exec("SAVEPOINT $savepoint"); } else { throw new DatabaseException("Savepoints not supported on this PDO connection"); } } public function supportsSavepoints() { return static::config()->get('legacy_types'); } }