write; 0 => read * * @var int */ protected $last_query_type = 0; /** * Used to store row offsets for queries when seeking. * * @var array */ private $resultSeekPositions = array(); /** * The last result, used to get the number of affected rows in {@see AbstractPdoDbDriver::affected_rows()}. * * @var PDOStatement|null */ private $lastResult = null; /** * The table prefix used for simple select, update, insert and delete queries * * @var string */ public $table_prefix; /** * The current version of the DBMS. * * Note that this is the version used by the {@see AbstractPdoDbDriver::$read_link}. * * @var string */ public $version; /** * A list of the performed queries. * * @var array */ public $querylist = array(); /** * The engine used to run the SQL database. * * @var string */ public $engine = "pdo"; /** * Whether or not this engine can use the search functionality. * * @var boolean */ public $can_search = true; /** * Build a DSN string using the given configuration. * * @param string $hostname The hostname of the database serer to connect to. * @param string $db The name of the database to connect to. * @param int|null The optional port to use to connect to the database server. * @param string|null The character encoding to use for the connection. * * @return string The DSN string, including the driver prefix. */ protected abstract function getDsn($hostname, $db, $port, $encoding); /** * Connect to the database server. * * @param array $config Array of DBMS connection details. * * @return bool Whether opening the connection was successful. */ public function connect($config) { $connections = array( 'read' => array(), 'write' => array(), ); if (isset($config['hostname'])) { // simple connection, with single DB server $connections['read'][] = $config; } else { if (!isset($config['read'])) { // multiple servers, but no specific read/write servers foreach ($config as $key => $settings) { if (is_int($key)) { $connections['read'][] = $settings; } } } else { // both read and write servers $connections = $config; } } if (isset($config['encoding'])) { $this->db_encoding = $config['encoding']; } // Actually connect to the specified servers foreach (array('read', 'write') as $type) { if (!isset($connections[$type]) || !is_array($connections[$type])){ break; } if (isset($connections[$type]['hostname'])) { $details = $connections[$type]; unset($connections[$type]); $connections[$type][] = $details; } // shuffle the connections shuffle($connections[$type]); // loop through the connections foreach($connections[$type] as $singleConnection) { $flags = array( PDO::ATTR_PERSISTENT => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false, ); if (!empty($singleConnection['pconnect'])) { $flags[PDO::ATTR_PERSISTENT] = true; } $link = "{$type}_link"; get_execution_time(); list($hostname, $port) = self::parseHostname($singleConnection['hostname']); $dsn = $this->getDsn( $hostname, $config['database'], $port, $this->db_encoding ); try { $this->$link = new PDO( $dsn, $singleConnection['username'], $singleConnection['password'], $flags ); $this->lastPdoException = null; } catch (PDOException $e) { $this->$link = null; $this->lastPdoException = $e; } $time_spent = get_execution_time(); $this->query_time += $time_spent; // Successful connection? break down brother! if ($this->$link !== null) { $this->connections[] = "[".strtoupper($type)."] {$singleConnection['username']}@{$singleConnection['hostname']} (Connected in ".format_time_duration($time_spent).")"; break; } else { $this->connections[] = "[FAILED] [".strtoupper($type)."] {$singleConnection['username']}@{$singleConnection['hostname']}"; } } } // No write server was specified (simple connection or just multiple servers) - mirror write link if (empty($connections['write'])) { $this->write_link = $this->read_link; } // Have no read connection? if ($this->read_link === null) { $this->error("[READ] Unable to connect to database server"); return false; } else if($this->write_link === null) { $this->error("[WRITE] Unable to connect to database server"); return false; } $this->database = $config['database']; if (version_compare(PHP_VERSION, '5.3.6', '<') === true) { // character set in DSN was ignored before PHP 5.3.6, so we must SET NAMES $this->setCharacterSet($this->db_encoding); } $this->current_link = $this->read_link; return true; } /** * Parse a hostname and possible port combination. * * @param string $hostname The hostname string. Can be any of the following formats: * - `127.0.0.1` - IPv4 address. * - `[::1]` - IPv6 address. * - `localhost` - hostname. * - `127.0.0.1:3306` - IPv4 address and port combination. * `[::1]:3306` - IPv6 address and port combination. * - `localhost:3306` - hostname and port combination. * * @return array Array of host and port. * * @throws InvalidArgumentException Thrown if {@see $hostname} is an IPv6 address which lacks a closing square bracket. */ private static function parseHostname($hostname) { // first, check for an IPv6 address - IPv6 addresses always start with `[` $openingSquareBracket = strpos($hostname, '['); if ($openingSquareBracket === 0) { // find ending `]` $closingSquareBracket = strpos($hostname, ']', $openingSquareBracket); if ($closingSquareBracket !== false) { $portSeparator = strpos($hostname, ':', $closingSquareBracket); // there is no port specified if ($portSeparator === false) { return array($hostname, null); } else { $host = substr($hostname, $openingSquareBracket, $closingSquareBracket + 1); $port = (int) substr($hostname, $portSeparator + 1); return array($host, $port); } } else { throw new InvalidArgumentException("Hostname is missing a closing square bracket for IPv6 address: {$hostname}"); } } // either an IPv4 address or a hostname $portSeparator = strpos($hostname, ':', 0); if ($portSeparator === false) { return array($hostname, null); } else { $host = substr($hostname, 0, $portSeparator); $port = (int) substr($hostname, $portSeparator + 1); return array($host, $port); } } /** * Set the character set to use. This issues a `SET NAMES` query to both the read and write links. * * @param string $characterSet The character set to use. * * @return void */ public function setCharacterSet($characterSet) { $query = "SET NAMES '{$characterSet}'"; self::execIgnoreError($this->read_link, $query); if ($this->write_link !== $this->read_link) { self::execIgnoreError($this->write_link, $query); } } /** * Execute a query, ignoring any errors. * * @param PDO $connection The connection to execute the query on. * @param string $query The query to execute. */ private static function execIgnoreError($connection, $query) { try { $connection->exec($query); } catch (PDOException $e) { // ignored on purpose } } /** * Output a database error. * * @param string $string The string to present as an error. * * @return bool Whether error reporting is enabled or not */ public function error($string = '') { if ($this->error_reporting) { if (class_exists("errorHandler")) { global $error_handler; if(!is_object($error_handler)) { require_once MYBB_ROOT."inc/class_error.php"; $error_handler = new errorHandler(); } $error = array( "error_no" => $this->error_number(), "error" => $this->error_string(), "query" => $string ); $error_handler->error(MYBB_SQL, $error); } else { trigger_error("[SQL] [". $this->error_number() ."]" . $this->error_string() . "
{$string}", E_USER_ERROR); } return true; } else { return false; } } /** * Return the error code for the last error that occurred. * * @return string|null The error code for the last error that occurred, or null if no error occurred. */ public function error_number() { if ($this->lastPdoException !== null) { return $this->lastPdoException->getCode(); } return null; } /** * Return athe error message for the last error that occurred. * * @return string|null The error message for the last error that occurred, or null if no error occurred. */ public function error_string() { if ($this->lastPdoException !== null && isset($this->lastPdoException->errorInfo[2])) { return $this->lastPdoException->errorInfo[2]; } return null; } /** * Query the database. * * @param string $string The query SQL. * @param boolean|int $hideErrors Whether to hide any errors that occur. * @param boolean|int $writeQuery Whether to run the query on the write connection rather than the read connection. * * @return PDOStatement|null The result of the query, or null if an error occurred and {@see $hideErrors} was set. */ public function query($string, $hideErrors = false, $writeQuery = false) { global $mybb; get_execution_time(); // Only execute write queries on master server if (($writeQuery || $this->last_query_type) && $this->write_link) { $this->current_link = &$this->write_link; } else { $this->current_link = &$this->read_link; } /** @var PDOStatement|null $query */ $query = null; try { if (preg_match('/^\\s*SELECT\\b/i', $string) === 1) { // NOTE: we use prepare + execute here rather than just query so that we may request a scrollable cursor... $query = $this->current_link->prepare($string, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL)); $query->execute(); $this->lastPdoException = null; } else { $query = $this->current_link->query($string); $this->lastPdoException = null; } } catch (PDOException $e) { $this->lastPdoException = $e; $query = null; if (!$hideErrors) { $this->error($string); exit; } } if ($writeQuery) { $this->last_query_type = 1; } else { $this->last_query_type = 0; } $query_time = get_execution_time(); $this->query_time += $query_time; $this->query_count++; $this->lastResult = $query; if ($mybb->debug_mode) { $this->explain_query($string, $query_time); } return $query; } /** * Execute a write query on the master database * * @param string $query The query SQL. * @param boolean|int $hideErrors Whether to hide any errors that occur. * * @return PDOStatement|null The result of the query, or null if an error occurred and {@see $hideErrors} was set. */ public function write_query($query, $hideErrors = false) { return $this->query($query, $hideErrors, true); } /** * Return a result array for a query. * * @param PDOStatement $query The query to retrieve a result for. * @param int $resultType The type of array to return. Can be any of the following values: * - {@see PDO::FETCH_ASSOC} Fetch an array of results keyed by column name. This is the default. * - {@see PDO::FETCH_NUM} Fetch an array of results keyed by column number, starting at 0. * - {@see PDO::FETCH_BOTH} Fetch an array of results keyed by both column name and number. * * @return array|bool The array of results, or false if there are no more results. */ public function fetch_array($query, $resultType = PDO::FETCH_ASSOC) { if (is_null($query) || !($query instanceof PDOStatement)) { return false; } switch($resultType) { case PDO::FETCH_NUM: case PDO::FETCH_BOTH: break; default: $resultType = PDO::FETCH_ASSOC; break; } $hash = spl_object_hash($query); if (isset($this->resultSeekPositions[$hash])) { return $query->fetch($resultType, PDO::FETCH_ORI_ABS, $this->resultSeekPositions[$hash]); } return $query->fetch($resultType); } /** * Return a specific field from a query. * * @param PDOStatement $query The query to retrieve a result for. * @param string $field The name of the field to return. * @param int|bool $row The number of the row to fetch it from, or false to fetch from the next row in the result set. * * @return mixed The resulting field, of false if no more rows are in th result set. * Note that when querying fields that have a boolean value, this method should not be used. */ public function fetch_field($query, $field, $row = false) { if (is_null($query) || !($query instanceof PDOStatement)) { return false; } if ($row !== false) { $this->data_seek($query, (int) $row); } // NOTE: PDOStatement::fetchColumn only operates on numbered columns, so we must fetch the array result $array = $this->fetch_array($query, PDO::FETCH_ASSOC); if ($array === false) { return false; } return $array[$field]; } /** * Move the internal row pointer to the specified row. * * @param PDOStatement $query The query to move the row pointer for. * @param int $row The row to move to. Rows are numbered from 0. * * @return bool Whether seeking was successful. */ public function data_seek($query, $row) { if (is_null($query) || !($query instanceof PDOStatement)) { return false; } $hash = spl_object_hash($query); // NOTE: PDO numbers rows from 1, but all other drivers are 0 based. We add 1 to the row number for compatibility $this->resultSeekPositions[$hash] = ((int) $row) + 1; return true; } /** * Return the number of rows resulting from a query. * * @param PDOStatement $query The query data. * @return int|bool The number of rows in the result, or false on failure. */ public function num_rows($query) { if (is_null($query) || !($query instanceof PDOStatement)) { return false; } if (preg_match('/^\\s*SELECT\\b/i', $query->queryString) === 1) { // rowCount does not return the number of rows in a select query on most DBMS, so we instead fetch all results then count them // TODO: how do we handle the case where we issued a prepared statement with parameters..? $countQuery = $this->read_link->query($query->queryString); $result = $countQuery->fetchAll(PDO::FETCH_COLUMN, 0); return count($result); } else { return $query->rowCount(); } } /** * Return the last id number of inserted data. * * @return string The id number. */ public function insert_id() { return $this->current_link->lastInsertId(); } /** * Close the connection with the DBMS. */ public function close() { $this->read_link = $this->write_link = $this->current_link = null; } /** * Returns the number of affected rows in a query. * * @return int The number of affected rows. */ public function affected_rows() { if ($this->lastResult === null) { return 0; } return $this->lastResult->rowCount(); } /** * Return the number of fields. * * @param PDOStatement $query The query result to get the number of fields for. * * @return int|bool The number of fields, or false if the number of fields could not be retrieved. */ public function num_fields($query) { if (is_null($query) || !($query instanceof PDOStatement)) { return false; } return $query->columnCount(); } public function shutdown_query($query, $name = '') { global $shutdown_queries; if($name) { $shutdown_queries[$name] = $query; } else { $shutdown_queries[] = $query; } } public function escape_string($string) { $string = $this->read_link->quote($string); // Remove ' from the beginning of the string and at the end of the string, because we already quote parameters $string = substr($string, 1); $string = substr($string, 0, -1); return $string; } public function free_result($query) { if (is_object($query) && $query instanceof PDOStatement) { return $query->closeCursor(); } return false; } public function escape_string_like($string) { return $this->escape_string(str_replace(array('\\', '%', '_') , array('\\\\', '\\%' , '\\_') , $string)); } public function get_version() { if ($this->version) { return $this->version; } $this->version = $this->read_link->getAttribute(PDO::ATTR_SERVER_VERSION); return $this->version; } public function set_table_prefix($prefix) { $this->table_prefix = $prefix; } public function get_execution_time() { return get_execution_time(); } }