Tasks.php 10.7 KB
Newer Older
1 2
<?php

3
namespace Drupal\Core\Database\Driver\pgsql\Install;
4

5
use Drupal\Core\Database\Database;
6
use Drupal\Core\Database\Install\Tasks as InstallTasks;
7
use Drupal\Core\Database\DatabaseNotFoundException;
8 9

/**
10
 * Specifies installation tasks for PostgreSQL databases.
11 12
 */
class Tasks extends InstallTasks {
13 14 15 16

  /**
   * {@inheritdoc}
   */
17 18
  protected $pdoDriver = 'pgsql';

19 20 21
  /**
   * Constructs a \Drupal\Core\Database\Driver\pgsql\Install\Tasks object.
   */
22
  public function __construct() {
23
    $this->tasks[] = [
24
      'function' => 'checkEncoding',
25 26 27
      'arguments' => [],
    ];
    $this->tasks[] = [
28
      'function' => 'checkBinaryOutput',
29 30 31
      'arguments' => [],
    ];
    $this->tasks[] = [
32
      'function' => 'checkStandardConformingStrings',
33 34 35
      'arguments' => [],
    ];
    $this->tasks[] = [
36
      'function' => 'initializeDatabase',
37 38
      'arguments' => [],
    ];
39 40
  }

41 42 43
  /**
   * {@inheritdoc}
   */
44
  public function name() {
45
    return t('PostgreSQL');
46 47
  }

48 49 50
  /**
   * {@inheritdoc}
   */
51
  public function minimumVersion() {
52
    return '9.1.2';
53 54
  }

55
  /**
56
   * {@inheritdoc}
57 58 59 60
   */
  protected function connect() {
    try {
      // This doesn't actually test the connection.
61
      Database::setActiveConnection();
62 63 64 65
      // Now actually do a check.
      Database::getConnection();
      $this->pass('Drupal can CONNECT to the database ok.');
    }
66
    catch (\Exception $e) {
67
      // Attempt to create the database if it is not found.
68
      if ($e instanceof DatabaseNotFoundException) {
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
        // Remove the database string from connection info.
        $connection_info = Database::getConnectionInfo();
        $database = $connection_info['default']['database'];
        unset($connection_info['default']['database']);

        // In order to change the Database::$databaseInfo array, need to remove
        // the active connection, then re-add it with the new info.
        Database::removeConnection('default');
        Database::addConnectionInfo('default', 'default', $connection_info['default']);

        try {
          // Now, attempt the connection again; if it's successful, attempt to
          // create the database.
          Database::getConnection()->createDatabase($database);
          Database::closeConnection();

          // Now, restore the database config.
          Database::removeConnection('default');
          $connection_info['default']['database'] = $database;
          Database::addConnectionInfo('default', 'default', $connection_info['default']);

          // Check the database connection.
          Database::getConnection();
          $this->pass('Drupal can CONNECT to the database ok.');
        }
        catch (DatabaseNotFoundException $e) {
          // Still no dice; probably a permission issue. Raise the error to the
          // installer.
97
          $this->fail(t('Database %database not found. The server reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()]));
98 99 100 101 102
        }
      }
      else {
        // Database connection failed for some other reason than the database
        // not existing.
103
        $this->fail(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname?</li></ul>', ['%error' => $e->getMessage()]));
104 105 106 107 108 109
        return FALSE;
      }
    }
    return TRUE;
  }

110 111 112 113 114 115
  /**
   * Check encoding is UTF8.
   */
  protected function checkEncoding() {
    try {
      if (db_query('SHOW server_encoding')->fetchField() == 'UTF8') {
116
        $this->pass(t('Database is encoded in UTF-8'));
117 118
      }
      else {
119
        $this->fail(t('The %driver database must use %encoding encoding to work with Drupal. Recreate the database with %encoding encoding. See <a href="INSTALL.pgsql.txt">INSTALL.pgsql.txt</a> for more details.', [
120 121
          '%encoding' => 'UTF8',
          '%driver' => $this->name(),
122
        ]));
123 124
      }
    }
125
    catch (\Exception $e) {
126
      $this->fail(t('Drupal could not determine the encoding of the database was set to UTF-8'));
127 128 129 130 131 132 133 134
    }
  }

  /**
   * Check Binary Output.
   *
   * Unserializing does not work on Postgresql 9 when bytea_output is 'hex'.
   */
135
  public function checkBinaryOutput() {
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
    // PostgreSQL < 9 doesn't support bytea_output, so verify we are running
    // at least PostgreSQL 9.
    $database_connection = Database::getConnection();
    if (version_compare($database_connection->version(), '9') >= 0) {
      if (!$this->checkBinaryOutputSuccess()) {
        // First try to alter the database. If it fails, raise an error telling
        // the user to do it themselves.
        $connection_options = $database_connection->getConnectionOptions();
        // It is safe to include the database name directly here, because this
        // code is only called when a connection to the database is already
        // established, thus the database name is guaranteed to be a correct
        // value.
        $query = "ALTER DATABASE \"" . $connection_options['database'] . "\" SET bytea_output = 'escape';";
        try {
          db_query($query);
        }
152
        catch (\Exception $e) {
153 154 155 156 157 158 159 160 161 162 163
          // Ignore possible errors when the user doesn't have the necessary
          // privileges to ALTER the database.
        }

        // Close the database connection so that the configuration parameter
        // is applied to the current connection.
        db_close();

        // Recheck, if it fails, finally just rely on the end user to do the
        // right thing.
        if (!$this->checkBinaryOutputSuccess()) {
164
          $replacements = [
165 166 167
            '%setting' => 'bytea_output',
            '%current_value' => 'hex',
            '%needed_value' => 'escape',
168
            '@query' => $query,
169
          ];
170
          $this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: <code>@query</code>", $replacements));
171 172 173 174 175 176 177 178 179
        }
      }
    }
  }

  /**
   * Verify that a binary data roundtrip returns the original string.
   */
  protected function checkBinaryOutputSuccess() {
180 181
    $bytea_output = db_query("SHOW bytea_output")->fetchField();
    return ($bytea_output == 'escape');
182 183
  }

184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
  /**
   * Ensures standard_conforming_strings setting is 'on'.
   *
   * When standard_conforming_strings setting is 'on' string literals ('...')
   * treat backslashes literally, as specified in the SQL standard. This allows
   * Drupal to convert between bytea, text and varchar columns.
   */
  public function checkStandardConformingStrings() {
    $database_connection = Database::getConnection();
    if (!$this->checkStandardConformingStringsSuccess()) {
      // First try to alter the database. If it fails, raise an error telling
      // the user to do it themselves.
      $connection_options = $database_connection->getConnectionOptions();
      // It is safe to include the database name directly here, because this
      // code is only called when a connection to the database is already
      // established, thus the database name is guaranteed to be a correct
      // value.
      $query = "ALTER DATABASE \"" . $connection_options['database'] . "\" SET standard_conforming_strings = 'on';";
      try {
        $database_connection->query($query);
      }
      catch (\Exception $e) {
        // Ignore possible errors when the user doesn't have the necessary
        // privileges to ALTER the database.
      }

      // Close the database connection so that the configuration parameter
      // is applied to the current connection.
      Database::closeConnection();

      // Recheck, if it fails, finally just rely on the end user to do the
      // right thing.
      if (!$this->checkStandardConformingStringsSuccess()) {
217
        $replacements = [
218 219 220
          '%setting' => 'standard_conforming_strings',
          '%current_value' => 'off',
          '%needed_value' => 'on',
221
          '@query' => $query,
222
        ];
223
        $this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: <code>@query</code>", $replacements));
224 225 226 227 228 229 230 231 232 233 234 235
      }
    }
  }

  /**
   * Verifies the standard_conforming_strings setting.
   */
  protected function checkStandardConformingStringsSuccess() {
    $standard_conforming_strings = Database::getConnection()->query("SHOW standard_conforming_strings")->fetchField();
    return ($standard_conforming_strings == 'on');
  }

236 237 238
  /**
   * Make PostgreSQL Drupal friendly.
   */
239
  public function initializeDatabase() {
240 241 242 243
    // We create some functions using global names instead of prefixing them
    // like we do with table names. This is so that we don't double up if more
    // than one instance of Drupal is running on a single database. We therefore
    // avoid trying to create them again in that case.
244 245
    // At the same time checking for the existence of the function fixes
    // concurrency issues, when both try to update at the same time.
246
    try {
247
      $connection = Database::getConnection();
248 249 250
      // When testing, two installs might try to run the CREATE FUNCTION queries
      // at the same time. Do not let that happen.
      $connection->query('SELECT pg_advisory_lock(1)');
251
      // Don't use {} around pg_proc table.
252 253
      if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) {
        $connection->query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS
254
          \'SELECT random();\'
255 256
          LANGUAGE \'sql\'',
          [],
257
          ['allow_delimiter_in_query' => TRUE]
258 259 260
        );
      }

261 262
      if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'substring_index'")->fetchField()) {
        $connection->query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS
263
          \'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\'
264 265
          LANGUAGE \'sql\'',
          [],
266
          ['allow_delimiter_in_query' => TRUE]
267 268
        );
      }
269
      $connection->query('SELECT pg_advisory_unlock(1)');
270

271
      $this->pass(t('PostgreSQL has initialized itself.'));
272
    }
273
    catch (\Exception $e) {
274
      $this->fail(t('Drupal could not be correctly setup with the existing database due to the following error: @error.', ['@error' => $e->getMessage()]));
275 276
    }
  }
277 278 279 280 281 282

  /**
   * {@inheritdoc}
   */
  public function getFormOptions(array $database) {
    $form = parent::getFormOptions($database);
283 284 285
    if (empty($form['advanced_options']['port']['#default_value'])) {
      $form['advanced_options']['port']['#default_value'] = '5432';
    }
286 287
    return $form;
  }
288

289
}