summaryrefslogtreecommitdiffstats
path: root/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php
blob: 70d9a333ce04b1f0073974527bc128e2bcabeca5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
<?php

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

use Drupal\Core\Database\Database;
use Drupal\Core\Database\Install\Tasks as InstallTasks;
use Drupal\Core\Database\DatabaseNotFoundException;

/**
 * Specifies installation tasks for PostgreSQL databases.
 */
class Tasks extends InstallTasks {

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

  /**
   * Constructs a \Drupal\Core\Database\Driver\pgsql\Install\Tasks object.
   */
  public function __construct() {
    $this->tasks[] = [
      'function' => 'checkEncoding',
      'arguments' => [],
    ];
    $this->tasks[] = [
      'function' => 'checkBinaryOutput',
      'arguments' => [],
    ];
    $this->tasks[] = [
      'function' => 'checkStandardConformingStrings',
      'arguments' => [],
    ];
    $this->tasks[] = [
      'function' => 'initializeDatabase',
      'arguments' => [],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function name() {
    return t('PostgreSQL');
  }

  /**
   * {@inheritdoc}
   */
  public function minimumVersion() {
    return '9.1.2';
  }

  /**
   * {@inheritdoc}
   */
  protected function connect() {
    try {
      // This doesn't actually test the connection.
      db_set_active();
      // Now actually do a check.
      Database::getConnection();
      $this->pass('Drupal can CONNECT to the database ok.');
    }
    catch (\Exception $e) {
      // Attempt to create the database if it is not found.
      if ($e instanceof DatabaseNotFoundException) {
        // 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.
          $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()]));
        }
      }
      else {
        // Database connection failed for some other reason than the database
        // not existing.
        $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()]));
        return FALSE;
      }
    }
    return TRUE;
  }

  /**
   * Check encoding is UTF8.
   */
  protected function checkEncoding() {
    try {
      if (db_query('SHOW server_encoding')->fetchField() == 'UTF8') {
        $this->pass(t('Database is encoded in UTF-8'));
      }
      else {
        $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.', [
          '%encoding' => 'UTF8',
          '%driver' => $this->name(),
        ]));
      }
    }
    catch (\Exception $e) {
      $this->fail(t('Drupal could not determine the encoding of the database was set to UTF-8'));
    }
  }

  /**
   * Check Binary Output.
   *
   * Unserializing does not work on Postgresql 9 when bytea_output is 'hex'.
   */
  public function checkBinaryOutput() {
    // 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);
        }
        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.
        db_close();

        // Recheck, if it fails, finally just rely on the end user to do the
        // right thing.
        if (!$this->checkBinaryOutputSuccess()) {
          $replacements = [
            '%setting' => 'bytea_output',
            '%current_value' => 'hex',
            '%needed_value' => 'escape',
            '@query' => $query,
          ];
          $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));
        }
      }
    }
  }

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

  /**
   * 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()) {
        $replacements = [
          '%setting' => 'standard_conforming_strings',
          '%current_value' => 'off',
          '%needed_value' => 'on',
          '@query' => $query,
        ];
        $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));
      }
    }
  }

  /**
   * 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');
  }

  /**
   * Make PostgreSQL Drupal friendly.
   */
  public function initializeDatabase() {
    // 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.
    // At the same time checking for the existence of the function fixes
    // concurrency issues, when both try to update at the same time.
    try {
      $connection = Database::getConnection();
      // 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)');
      // Don't use {} around pg_proc table.
      if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) {
        $connection->query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS
          \'SELECT random();\'
          LANGUAGE \'sql\'',
          [],
          ['allow_delimiter_in_query' => TRUE]
        );
      }

      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
          \'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\'
          LANGUAGE \'sql\'',
          [],
          ['allow_delimiter_in_query' => TRUE]
        );
      }
      $connection->query('SELECT pg_advisory_unlock(1)');

      $this->pass(t('PostgreSQL has initialized itself.'));
    }
    catch (\Exception $e) {
      $this->fail(t('Drupal could not be correctly setup with the existing database due to the following error: @error.', ['@error' => $e->getMessage()]));
    }
  }

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

}