database-setup.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. <?php
  2. /**
  3. * User: sourcefabric
  4. * Date: 02/12/14
  5. * Class DatabaseSetup
  6. * Wrapper class for validating and installing the Airtime database during the installation process
  7. */
  8. class DatabaseSetup extends Setup {
  9. // airtime.conf section header
  10. const SECTION = "[database]";
  11. // Constant form field names for passing errors back to the front-end
  12. const DB_USER = "dbUser",
  13. DB_PASS = "dbPass",
  14. DB_NAME = "dbName",
  15. DB_HOST = "dbHost";
  16. // Form field values
  17. private $user, $pass, $name, $host;
  18. // Array of key->value pairs for airtime.conf
  19. static $properties;
  20. static $dbh = null;
  21. public function __construct($settings) {
  22. $this->user = $settings[self::DB_USER];
  23. $this->pass = $settings[self::DB_PASS];
  24. $this->name = $settings[self::DB_NAME];
  25. $this->host = $settings[self::DB_HOST];
  26. self::$properties = array(
  27. "host" => $this->host,
  28. "dbname" => $this->name,
  29. "dbuser" => $this->user,
  30. "dbpass" => $this->pass,
  31. );
  32. }
  33. private function setNewDatabaseConnection($dbName) {
  34. self::$dbh = new PDO("pgsql:host=" . $this->host . ";dbname=" . $dbName . ";port=5432"
  35. . ";user=" . $this->user . ";password=" . $this->pass);
  36. $err = self::$dbh->errorInfo();
  37. if ($err[1] != null) {
  38. throw new PDOException();
  39. }
  40. }
  41. /**
  42. * Runs various database checks against the given settings. If a database with the given name already exists,
  43. * we attempt to install the Airtime schema. If not, we first check if the user can create databases, then try
  44. * to create the database. If we encounter errors, the offending fields are returned in an array to the browser.
  45. * @return array associative array containing a display message and fields with errors
  46. * @throws AirtimeDatabaseException
  47. */
  48. public function runSetup() {
  49. try {
  50. $this->setNewDatabaseConnection("postgres");
  51. if ($this->checkDatabaseExists()) {
  52. $this->installDatabaseTables();
  53. } else {
  54. $this->checkUserCanCreateDb();
  55. $this->createDatabase();
  56. $this->installDatabaseTables();
  57. }
  58. } catch (PDOException $e) {
  59. throw new AirtimeDatabaseException("Couldn't establish a connection to the database! "
  60. . "Please check your credentials and try again. "
  61. . "PDO Exception: " . $e->getMessage(),
  62. array(
  63. self::DB_NAME,
  64. self::DB_USER,
  65. self::DB_PASS,
  66. ));
  67. }
  68. $this->writeToTemp();
  69. self::$dbh = null;
  70. return array(
  71. "message" => "Airtime database was created successfully!",
  72. "errors" => array(),
  73. );
  74. }
  75. protected function writeToTemp() {
  76. parent::writeToTemp(self::SECTION, self::$properties);
  77. }
  78. private function installDatabaseTables() {
  79. $this->checkDatabaseEncoding();
  80. $this->setNewDatabaseConnection($this->name);
  81. $this->checkSchemaExists();
  82. $this->createDatabaseTables();
  83. }
  84. /**
  85. * Check if the database settings and credentials given are valid
  86. * @return boolean true if the database given exists and the user is valid and can access it
  87. */
  88. private function checkDatabaseExists() {
  89. $statement = self::$dbh->prepare("SELECT datname FROM pg_database WHERE datname = :dbname");
  90. $statement->execute(array(":dbname" => $this->name));
  91. $result = $statement->fetch();
  92. return isset($result[0]);
  93. }
  94. /**
  95. * Check if the database schema has already been set up
  96. * @throws AirtimeDatabaseException
  97. */
  98. private function checkSchemaExists() {
  99. $statement = self::$dbh->prepare("SELECT EXISTS (SELECT relname FROM pg_class WHERE relname='cc_files')");
  100. $statement->execute();
  101. $result = $statement->fetch();
  102. if (isset($result[0]) && $result[0] == "t") {
  103. throw new AirtimeDatabaseException("Airtime is already installed in this database!", array());
  104. }
  105. }
  106. /**
  107. * Check if the given user has access on the given host to create a new database
  108. * @throws AirtimeDatabaseException
  109. */
  110. private function checkUserCanCreateDb() {
  111. $statement = self::$dbh->prepare("SELECT 1 FROM pg_roles WHERE rolname=:dbuser AND rolcreatedb='t'");
  112. $statement->execute(array(":dbuser" => $this->user));
  113. $result = $statement->fetch();
  114. if (!isset($result[0])) {
  115. throw new AirtimeDatabaseException("No database " . $this->name . " exists; user '" . $this->user
  116. . "' does not have permission to create databases on " . $this->host,
  117. array(
  118. self::DB_NAME,
  119. self::DB_USER,
  120. self::DB_PASS,
  121. ));
  122. }
  123. }
  124. /**
  125. * Creates the Airtime database using the given credentials
  126. * @throws AirtimeDatabaseException
  127. */
  128. private function createDatabase() {
  129. $statement = self::$dbh->prepare("CREATE DATABASE " . pg_escape_string($this->name)
  130. . " WITH ENCODING 'UTF8' TEMPLATE template0"
  131. . " OWNER " . pg_escape_string($this->user));
  132. if (!$statement->execute()) {
  133. throw new AirtimeDatabaseException("There was an error creating the database!",
  134. array(self::DB_NAME,));
  135. }
  136. }
  137. /**
  138. * Creates the Airtime database schema using the given credentials
  139. * @throws AirtimeDatabaseException
  140. */
  141. private function createDatabaseTables() {
  142. $sqlDir = dirname(dirname(__DIR__)) . "/build/sql/";
  143. $files = array("schema.sql", "sequences.sql", "views.sql", "triggers.sql", "defaultdata.sql");
  144. foreach ($files as $f) {
  145. try {
  146. /*
  147. * Unfortunately, we need to use exec here due to PDO's lack of support for importing
  148. * multi-line .sql files. PDO->exec() almost works, but any SQL errors stop the import,
  149. * so the necessary DROPs on non-existent tables make it unusable. Prepared statements
  150. * have multiple issues; they similarly die on any SQL errors, fail to read in multi-line
  151. * commands, and fail on any unescaped ? or $ characters.
  152. */
  153. exec("export PGPASSWORD=" . $this->pass . " && psql -U " . $this->user . " --dbname "
  154. . $this->name . " -h " . $this->host . " -f $sqlDir$f 2>/dev/null", $out, $status);
  155. } catch (Exception $e) {
  156. throw new AirtimeDatabaseException("There was an error setting up the Airtime schema!",
  157. array(self::DB_NAME,));
  158. }
  159. }
  160. }
  161. /**
  162. * Checks whether the newly-created database's encoding was properly set to UTF8
  163. * @throws AirtimeDatabaseException
  164. */
  165. private function checkDatabaseEncoding() {
  166. $statement = self::$dbh->prepare("SELECT pg_encoding_to_char(encoding) "
  167. . "FROM pg_database WHERE datname = :dbname");
  168. $statement->execute(array(":dbname" => $this->name));
  169. $encoding = $statement->fetch();
  170. if (!($encoding && $encoding[0] == "UTF8")) {
  171. throw new AirtimeDatabaseException("The database was installed with an incorrect encoding type!",
  172. array(self::DB_NAME,));
  173. }
  174. }
  175. }