Rewrote database-setup to use PDO and be overall more readable

This commit is contained in:
Duncan Sommerville 2014-12-11 12:54:50 -05:00
parent e9a966a8a4
commit ccd515e7d6
2 changed files with 105 additions and 128 deletions

View File

@ -17,6 +17,7 @@
<div class="form-group"> <div class="form-group">
<label class="control-label" for="dbName">Name</label> <label class="control-label" for="dbName">Name</label>
<input required class="form-control" type="text" name="dbName" id="dbName" placeholder="Name" value="airtime"/> <input required class="form-control" type="text" name="dbName" id="dbName" placeholder="Name" value="airtime"/>
<span class="glyphicon glyphicon-remove form-control-feedback"></span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label" for="dbHost">Host</label> <label class="control-label" for="dbHost">Host</label>

View File

@ -18,197 +18,173 @@ class DatabaseSetup extends Setup {
DB_HOST = "dbHost"; DB_HOST = "dbHost";
// Form field values // Form field values
static $user, $pass, $name, $host; private $user, $pass, $name, $host;
// Array of key->value pairs for airtime.conf // Array of key->value pairs for airtime.conf
static $properties; static $properties;
// Message and error fields to return to the front-end static $dbh = null;
static $message = null;
static $errors = array();
function __construct($settings) { public function __construct($settings) {
self::$user = $settings[self::DB_USER]; $this->user = $settings[self::DB_USER];
self::$pass = $settings[self::DB_PASS]; $this->pass = $settings[self::DB_PASS];
self::$name = $settings[self::DB_NAME]; $this->name = $settings[self::DB_NAME];
self::$host = $settings[self::DB_HOST]; $this->host = $settings[self::DB_HOST];
self::$properties = array( self::$properties = array(
"host" => self::$host, "host" => $this->host,
"dbname" => self::$name, "dbname" => $this->name,
"dbuser" => self::$user, "dbuser" => $this->user,
"dbpass" => self::$pass, "dbpass" => $this->pass,
); );
} }
private function setNewDatabaseConnection($dbName) {
self::$dbh = new PDO("pgsql:host=" . $this->host . ";dbname=" . $dbName . ";port=5432"
. ";user=" . $this->user . ";password=" . $this->pass);
$err = self::$dbh->errorInfo();
if ($err[1] != null) {
throw new AirtimeDatabaseException("Couldn't establish a connection to the database!",
array(
self::DB_NAME,
self::DB_USER,
self::DB_PASS,
));
}
}
/** /**
* Runs various database checks against the given settings. If a database with the given name already exists, * Runs various database checks against the given settings. If a database with the given name already exists,
* we attempt to install the Airtime schema. If not, we first check if the user can create databases, then try * we attempt to install the Airtime schema. If not, we first check if the user can create databases, then try
* to create the database. If we encounter errors, the offending fields are returned in an array to the browser. * to create the database. If we encounter errors, the offending fields are returned in an array to the browser.
* @return array associative array containing a display message and fields with errors * @return array associative array containing a display message and fields with errors
* @throws AirtimeDatabaseException
*/ */
function runSetup() { public function runSetup() {
// Check the connection and user credentials $this->setNewDatabaseConnection("postgres");
if ($this->checkDatabaseConnection()) { if ($this->checkDatabaseExists()) {
// We know that the user credentials check out, so check if the database exists $this->installDatabaseTables();
if ($this->checkDatabaseExists()) { } else {
// The database already exists, check if the schema exists as well $this->checkUserCanCreateDb();
if ($this->checkSchemaExists()) { $this->createDatabase();
self::$message = "Airtime is already installed in this database!"; $this->installDatabaseTables();
} else {
if ($this->createDatabaseTables()) {
self::$message = "Successfully installed Airtime database to '" . self::$name . "'";
} else {
self::$message = "Something went wrong setting up the Airtime schema!";
self::$errors[] = self::DB_NAME;
}
}
} else {
// The database doesn't exist, so check if the user can create databases
if ($this->checkUserCanCreateDb()) {
// The user can create a database, do it
if ($this->createDatabase()) {
// Ensure that the database was installed in UTF8 (we only care about the Airtime database)
if ($this->checkDatabaseSchema()) {
if ($this->createDatabaseTables()) {
self::$message = "Successfully installed Airtime database to '" . self::$name . "'";
} else {
self::$message = "Something went wrong setting up the Airtime schema!";
self::$errors[] = self::DB_NAME;
}
} else {
self::$message = "The database was installed with an incorrect encoding type!";
self::$errors[] = self::DB_NAME;
}
} else {
self::$message = "There was an error installing the database!";
self::$errors[] = self::DB_NAME;
}
} // The user can't create databases, so we're done
else {
self::$message = "No database " . self::$name . " exists; user " . self::$user
. " does not have permission to create databases on " . self::$host;
self::$errors[] = self::DB_NAME;
}
}
} }
if (count(self::$errors) <= 0) { $this->writeToTemp();
$this->writeToTemp();
}
self::$dbh = null;
return array( return array(
"message" => self::$message, "message" => "Airtime database was created successfully!",
"errors" => self::$errors, "errors" => array(),
); );
} }
function writeToTemp() { protected function writeToTemp() {
parent::writeToTemp(self::SECTION, self::$properties); parent::writeToTemp(self::SECTION, self::$properties);
} }
private function installDatabaseTables() {
/** $this->checkDatabaseEncoding();
* Check if the user's database connection is valid $this->setNewDatabaseConnection($this->name);
* @return boolean true if the connection are valid $this->checkSchemaExists();
*/ $this->createDatabaseTables();
function checkDatabaseConnection() {
// This is pretty redundant, but we need to test both
// the existence and the validity of the given credentials
exec("export PGPASSWORD=" . self::$pass . " && psql -h "
. self::$host . " -U " . self::$user . " 2>&1", $out, $status);
foreach ($out as $o) {
if (strpos($o, "host name")) {
self::$message = "Invalid connection parameters!";
self::$errors[] = self::DB_HOST;
return false;
} else if (strpos($o, "authentication")) {
self::$message = "User credentials are invalid!";
self::$errors[] = self::DB_USER;
self::$errors[] = self::DB_PASS;
return false;
}
}
return $status == 0;
} }
/** /**
* Check if the database settings and credentials given are valid * Check if the database settings and credentials given are valid
* @return boolean true if the database given exists and the user is valid and can access it * @return boolean true if the database given exists and the user is valid and can access it
*/ */
function checkDatabaseExists() { private function checkDatabaseExists() {
exec("export PGPASSWORD=" . self::$pass . " && psql -lqt -h " . self::$host . " -U " . self::$user $statement = self::$dbh->prepare("SELECT datname FROM pg_database WHERE datname = :dbname");
. "| cut -d \\| -f 1 | grep -w " . self::$name, $out, $status); $statement->execute(array(":dbname" => $this->name));
return $status == 0; $result = $statement->fetch();
return isset($result[0]);
} }
/** /**
* Check if the database schema has already been set up * Check if the database schema has already been set up
* @return boolean true if the database schema exists * @throws AirtimeDatabaseException
*/ */
function checkSchemaExists() { private function checkSchemaExists() {
// Check for cc_pref to see if the schema is already installed in this database $statement = self::$dbh->prepare("SELECT EXISTS (SELECT relname FROM pg_class WHERE relname='cc_files')");
exec("export PGPASSWORD=" . self::$pass . " && psql -U " . self::$user . " -h " $statement->execute();
. self::$host . " -d " . self::$name . " -tAc \"SELECT * FROM cc_pref\"", $out, $status); $result = $statement->fetch();
return $status == 0; if (isset($result[0]) && $result[0] == "t") {
throw new AirtimeDatabaseException("Airtime is already installed in this database!", array());
}
} }
/** /**
* Check if the given user has access on the given host to create a new database * Check if the given user has access on the given host to create a new database
* @return boolean true if the given user has permission to create a database on the given host * @throws AirtimeDatabaseException
*/ */
function checkUserCanCreateDb() { private function checkUserCanCreateDb() {
exec("export PGPASSWORD=" . self::$pass . " && psql -h " . self::$host . " -U " . self::$user . " -tAc" $statement = self::$dbh->prepare("SELECT 1 FROM pg_roles WHERE rolname=:dbuser AND rolcreatedb='t'");
. "\"SELECT 1 FROM pg_roles WHERE rolname='" . self::$user . "' AND rolcreatedb='t'\"", $out, $status); $statement->execute(array(":dbuser" => $this->user));
return $status == 0; $result = $statement->fetch();
if (!isset($result[0])) {
throw new AirtimeDatabaseException("No database " . $this->name . " exists; user '" . $this->user
. "' does not have permission to create databases on " . $this->host,
array(
self::DB_NAME,
self::DB_USER,
self::DB_PASS,
));
}
} }
/** /**
* Creates the Airtime database using the given credentials * Creates the Airtime database using the given credentials
* @return boolean true if the database was created * @throws AirtimeDatabaseException
*/ */
function createDatabase() { private function createDatabase() {
exec("export PGPASSWORD=" . self::$pass . " && psql -h " . self::$host . " -U " . self::$user . " -tAc" $statement = self::$dbh->prepare("CREATE DATABASE " . pg_escape_string($this->name)
. "\"CREATE DATABASE " . self::$name . " WITH ENCODING 'UTF8' TEMPLATE template0 OWNER " . " WITH ENCODING 'UTF8' TEMPLATE template0"
. self::$user . "\"", $out, $status); . " OWNER " . pg_escape_string($this->user));
return $status == 0; if (!$statement->execute()) {
throw new AirtimeDatabaseException("There was an error creating the database!",
array(self::DB_NAME,));
}
} }
/** /**
* Creates the Airtime database schema using the given credentials * Creates the Airtime database schema using the given credentials
* @return boolean true if the database tables were created without error * @throws AirtimeDatabaseException
*/ */
function createDatabaseTables() { private function createDatabaseTables() {
$sqlDir = dirname(dirname(__DIR__)) . "/build/sql/"; $sqlDir = dirname(dirname(__DIR__)) . "/build/sql/";
$files = array("schema.sql", "sequences.sql", "views.sql", "triggers.sql", "defaultdata.sql"); $files = array("schema.sql", "sequences.sql", "views.sql", "triggers.sql", "defaultdata.sql");
foreach ($files as $f) { foreach ($files as $f) {
try { try {
exec("export PGPASSWORD=" . self::$pass . " && psql -U " . self::$user . " --dbname " /*
. self::$name . " -h " . self::$host . " -f $sqlDir$f 2>/dev/null", $out, $status); * Unfortunately, we need to use exec here due to PDO's lack of support for importing
* multi-line .sql files. PDO->exec() almost works, but any SQL errors stop the import,
* so the necessary DROPs on non-existent tables make it unusable. Prepared statements
* have multiple issues; they similarly die on any SQL errors, fail to read in multi-line
* commands, and fail on any unescaped ? or $ characters.
*/
exec("export PGPASSWORD=" . $this->pass . " && psql -U " . $this->user . " --dbname "
. $this->name . " -h " . $this->host . " -f $sqlDir$f 2>/dev/null", $out, $status);
} catch (Exception $e) { } catch (Exception $e) {
return false; throw new AirtimeDatabaseException("There was an error setting up the Airtime schema!",
array(self::DB_NAME,));
} }
} }
return true;
} }
/** /**
* Checks whether the newly-created database's encoding was properly set to UTF8 * Checks whether the newly-created database's encoding was properly set to UTF8
* @return boolean true if the database encoding is UTF8 * @throws AirtimeDatabaseException
*/ */
function checkDatabaseEncoding() { private function checkDatabaseEncoding() {
exec("export PGPASSWORD=" . self::$pass . " && psql -U " . self::$user . " -h " $statement = self::$dbh->prepare("SELECT pg_encoding_to_char(encoding) "
. self::$host . " -d " . self::$name . " -tAc \"SHOW SERVER_ENCODING\"", $out, $status); . "FROM pg_database WHERE datname = :dbname");
return $out && $out[0] == "UTF8"; $statement->execute(array(":dbname" => $this->name));
$encoding = $statement->fetch();
if (!($encoding && $encoding[0] == "UTF8")) {
throw new AirtimeDatabaseException("The database was installed with an incorrect encoding type!",
array(self::DB_NAME,));
}
} }
// TODO Since we already check the encoding, is there a purpose to verifying the schema? }
function checkDatabaseSchema() {
$outFile = "/tmp/tempSchema.xml";
exec("export PGPASSWORD=" . self::$pass . " && psql -U " . self::$user . " -h "
. self::$host . " -o ${outFile} -tAc \"SELECT database_to_xml(FALSE, FALSE, '"
. self::$name . "')\"", $out, $status);
}
}