config = $config; $this->dbms = $this->get_dbms_data($this->config['dbms']); } /** * Return the current PDO instance */ public function get_pdo() { return $this->pdo; } /** * Creates a PDO connection for the configured database. * * @param bool $use_db Whether the DSN should be tied to a * particular database making it impossible * to delete that database. */ public function connect($use_db = true) { $dsn = $this->dbms['PDO'] . ':'; switch ($this->dbms['PDO']) { case 'sqlite2': $dsn .= $this->config['dbhost']; break; case 'sqlsrv': // prefix the hostname (or DSN) with Server= so using just (local)\SQLExpress // works for example, further parameters can still be appended using ;x=y $dsn .= 'Server='; // no break -> rest like ODBC case 'odbc': // for ODBC assume dbhost is a suitable DSN // e.g. Driver={SQL Server Native Client 10.0};Server=(local)\SQLExpress; $dsn .= $this->config['dbhost']; // Primarily for MSSQL Native/Azure as ODBC needs it in $dbhost, attached to the Server param if ($this->config['dbport']) { $port_delimiter = (defined('PHP_OS') && substr(PHP_OS, 0, 3) === 'WIN') ? ',' : ':'; $dsn .= $port_delimiter . $this->config['dbport']; } if ($use_db) { $dsn .= ';Database=' . $this->config['dbname']; } break; default: $dsn .= 'host=' . $this->config['dbhost']; if ($this->config['dbport']) { $dsn .= ';port=' . $this->config['dbport']; } if ($use_db) { $dsn .= ';dbname=' . $this->config['dbname']; } else if ($this->dbms['PDO'] == 'pgsql') { // Postgres always connects to a // database. If the database is not // specified here, but the username // is specified, then connection // will be to the database named // as the username. // // For greater compatibility, connect // instead to postgres database which // should always exist: // http://www.postgresql.org/docs/9.0/static/manage-ag-templatedbs.html $dsn .= ';dbname=postgres'; } break; } // These require different connection strings on the phpBB side than they do in PDO // so you must provide a DSN string for ODBC separately if (!empty($this->config['custom_dsn']) && ($this->config['dbms'] == 'mssql' || $this->config['dbms'] == 'firebird')) { $dsn = 'odbc:' . $this->config['custom_dsn']; } try { switch ($this->config['dbms']) { case 'mssql': case 'mssql_odbc': $this->pdo = new phpbb_database_connection_odbc_pdo_wrapper('mssql', 0, $dsn, $this->config['dbuser'], $this->config['dbpasswd']); break; case 'firebird': if (!empty($this->config['custom_dsn'])) { $this->pdo = new phpbb_database_connection_odbc_pdo_wrapper('firebird', 0, $dsn, $this->config['dbuser'], $this->config['dbpasswd']); break; } // Fall through if they're using the firebird PDO driver and not the generic ODBC driver default: $this->pdo = new PDO($dsn, $this->config['dbuser'], $this->config['dbpasswd']); break; } } catch (PDOException $e) { $cleaned_dsn = str_replace($this->config['dbpasswd'], '*password*', $dsn); throw new Exception("Unable do connect to $cleaned_dsn using PDO with error: {$e->getMessage()}"); } $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } /** * Load the phpBB database schema into the database */ public function load_schema() { $this->ensure_connected(__METHOD__); $directory = dirname(__FILE__) . '/../../phpBB/install/schemas/'; $this->load_schema_from_file($directory); } /** * Drop the database if it exists and re-create it * * Note: This does not load the schema, and it is suggested * to re-connect after calling to get use_db isolation. */ public function recreate_db() { switch ($this->config['dbms']) { case 'sqlite': case 'firebird': $this->connect(); // Drop all of the tables foreach ($this->get_tables() as $table) { $this->pdo->exec('DROP TABLE ' . $table); } $this->purge_extras(); break; case 'oracle': $this->connect(); // Drop all of the tables foreach ($this->get_tables() as $table) { $this->pdo->exec('DROP TABLE ' . $table . ' CASCADE CONSTRAINTS'); } $this->purge_extras(); break; default: $this->connect(false); try { $this->pdo->exec('DROP DATABASE ' . $this->config['dbname']); try { $this->pdo->exec('CREATE DATABASE ' . $this->config['dbname']); } catch (PDOException $e) { throw new Exception("Unable to re-create database: {$e->getMessage()}"); } } catch (PDOException $e) { // try to delete all tables if dropping the database was not possible. foreach ($this->get_tables() as $table) { $this->pdo->exec('DROP TABLE ' . $table); } $this->purge_extras(); } break; } } /** * Retrieves a list of all tables from the database. * * @return array(string) */ public function get_tables() { $this->ensure_connected(__METHOD__); switch ($this->config['dbms']) { case 'mysql': case 'mysql4': case 'mysqli': $sql = 'SHOW TABLES'; break; case 'sqlite': $sql = 'SELECT name FROM sqlite_master WHERE type = "table"'; break; case 'mssql': case 'mssql_odbc': case 'mssqlnative': $sql = "SELECT name FROM sysobjects WHERE type='U'"; break; case 'postgres': $sql = 'SELECT relname FROM pg_stat_user_tables'; break; case 'firebird': $sql = 'SELECT rdb$relation_name FROM rdb$relations WHERE rdb$view_source is null AND rdb$system_flag = 0'; break; case 'oracle': $sql = 'SELECT table_name FROM USER_TABLES'; break; } $result = $this->pdo->query($sql); $tables = array(); while ($row = $result->fetch(PDO::FETCH_NUM)) { $tables[] = current($row); } return $tables; } /** * Throw an exception if not connected */ protected function ensure_connected($method_name) { if (null === $this->pdo) { throw new Exception(sprintf('You must connect before calling %s', $method_name)); } } /** * Compile the correct schema filename (as per create_schema_files) and * load it into the database. */ protected function load_schema_from_file($directory) { $schema = $this->dbms['SCHEMA']; if ($this->config['dbms'] == 'mysql') { $sth = $this->pdo->query('SELECT VERSION() AS version'); $row = $sth->fetch(PDO::FETCH_ASSOC); if (version_compare($row['version'], '4.1.3', '>=')) { $schema .= '_41'; } else { $schema .= '_40'; } } $filename = $directory . $schema . '_schema.sql'; $queries = file_get_contents($filename); $sql = phpbb_remove_comments($queries); $sql = split_sql_file($sql, $this->dbms['DELIM']); foreach ($sql as $query) { $this->pdo->exec($query); } } /** * Map a phpBB dbms driver name to dbms data array */ protected function get_dbms_data($dbms) { $available_dbms = array( 'firebird' => array( 'SCHEMA' => 'firebird', 'DELIM' => ';;', 'PDO' => 'firebird', ), 'mysqli' => array( 'SCHEMA' => 'mysql_41', 'DELIM' => ';', 'PDO' => 'mysql', ), 'mysql' => array( 'SCHEMA' => 'mysql', 'DELIM' => ';', 'PDO' => 'mysql', ), 'mssql' => array( 'SCHEMA' => 'mssql', 'DELIM' => 'GO', 'PDO' => 'odbc', ), 'mssql_odbc'=> array( 'SCHEMA' => 'mssql', 'DELIM' => 'GO', 'PDO' => 'odbc', ), 'mssqlnative' => array( 'SCHEMA' => 'mssql', 'DELIM' => 'GO', 'PDO' => 'sqlsrv', ), 'oracle' => array( 'SCHEMA' => 'oracle', 'DELIM' => '/', 'PDO' => 'oci', ), 'postgres' => array( 'SCHEMA' => 'postgres', 'DELIM' => ';', 'PDO' => 'pgsql', ), 'sqlite' => array( 'SCHEMA' => 'sqlite', 'DELIM' => ';', 'PDO' => 'sqlite2', ), ); if (isset($available_dbms[$dbms])) { return $available_dbms[$dbms]; } else { $message = "Supplied dbms \"$dbms\" is not a valid phpBB dbms, must be one of: "; $message .= implode(', ', array_keys($available_dbms)); throw new Exception($message); } } /** * Removes extra objects from a database. This is for cases where dropping the database fails. */ public function purge_extras() { $this->ensure_connected(__METHOD__); $queries = array(); switch ($this->config['dbms']) { case 'firebird': $sql = 'SELECT RDB$GENERATOR_NAME FROM RDB$GENERATORS WHERE RDB$SYSTEM_FLAG = 0'; $result = $this->pdo->query($sql); while ($row = $result->fetch(PDO::FETCH_NUM)) { $queries[] = 'DROP GENERATOR ' . current($row); } break; case 'oracle': $sql = 'SELECT sequence_name FROM USER_SEQUENCES'; $result = $this->pdo->query($sql); while ($row = $result->fetch(PDO::FETCH_NUM)) { $queries[] = 'DROP SEQUENCE ' . current($row); } break; } foreach ($queries as $query) { $this->pdo->exec($query); } } /** * Performs synchronisations on the database after a fixture has been loaded */ public function post_setup_synchronisation() { $this->ensure_connected(__METHOD__); $queries = array(); switch ($this->config['dbms']) { case 'oracle': // Get all of the information about the sequences $sql = "SELECT t.table_name, tc.column_name, d.referenced_name as sequence_name FROM USER_TRIGGERS t JOIN USER_DEPENDENCIES d on d.name = t.trigger_name JOIN USER_TRIGGER_COLS tc on (tc.trigger_name = t.trigger_name) WHERE d.referenced_type = 'SEQUENCE' AND d.type = 'TRIGGER'"; $result = $this->pdo->query($sql); while ($row = $result->fetch(PDO::FETCH_ASSOC)) { // Get the current max value of the table $sql = "SELECT MAX({$row['COLUMN_NAME']}) + 1 FROM {$row['TABLE_NAME']}"; $max_result = $this->pdo->query($sql); $max_row = $max_result->fetch(PDO::FETCH_NUM); if (!$max_row) { continue; } $maxval = current($max_row); if ($maxval == null) { $maxval = 1; } // Get the sequence's next value $sql = "SELECT {$row['SEQUENCE_NAME']}.nextval FROM dual"; try { $nextval_result = $this->pdo->query($sql); } catch (PDOException $e) { // If we catch an exception here it's because the sequencer hasn't been initialized yet. // If the table hasn't been used, then there's nothing to do. continue; } $nextval_row = $nextval_result->fetch(PDO::FETCH_NUM); if ($nextval_row) { $nextval = current($nextval_row); // Make sure we aren't setting the new increment to zero. if ($maxval != $nextval) { // This is a multi-step process. First we need to get the sequence back into position. // That means either advancing it or moving it backwards. Sequences have a minimum value // of 1, so you cannot go past that. Once the offset it determined, you have to request // the next sequence value to actually move the pointer into position. So if you're at 21 // and need to be back at 1, set the incrementer to -20. When it's requested, it'll give // you 1. Then we have to set the increment amount back to 1 to resume normal behavior. // Move the sequence to the correct position. $sql = "ALTER SEQUENCE {$row['SEQUENCE_NAME']} INCREMENT BY " . ($maxval - $nextval); $this->pdo->exec($sql); // Select the next value to actually update the sequence values $sql = "SELECT {$row['SEQUENCE_NAME']}.nextval FROM dual"; $this->pdo->query($sql); // Reset the sequence's increment amount $sql = "ALTER SEQUENCE {$row['SEQUENCE_NAME']} INCREMENT BY 1"; $this->pdo->exec($sql); } } } break; case 'postgres': // First get the sequences $sequences = array(); $sql = "SELECT relname FROM pg_class WHERE relkind = 'S'"; $result = $this->pdo->query($sql); while ($row = $result->fetch(PDO::FETCH_ASSOC)) { $sequences[] = $row['relname']; } // Now get the name of the column using it foreach ($sequences as $sequence) { $table = str_replace('_seq', '', $sequence); $sql = "SELECT column_name FROM information_schema.columns WHERE table_name = '$table' AND column_default = 'nextval(''$sequence''::regclass)'"; $result = $this->pdo->query($sql); $row = $result->fetch(PDO::FETCH_ASSOC); // Finally, set the new sequence value if ($row) { $column = $row['column_name']; // Get the old value if it exists, or use 1 if it doesn't $sql = "SELECT COALESCE((SELECT MAX({$column}) + 1 FROM {$table}), 1) AS val"; $result = $this->pdo->query($sql); $row = $result->fetch(PDO::FETCH_ASSOC); if ($row) { // The last parameter is false so that the system doesn't increment it again $queries[] = "SELECT SETVAL('{$sequence}', {$row['val']}, false)"; } } } break; } foreach ($queries as $query) { $this->pdo->exec($query); } } }