diff --git a/core/core.services.yml b/core/core.services.yml index 5877a38..88f026f 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1169,6 +1169,11 @@ services: tags: - { name: event_subscriber } arguments: ['@config.manager', '@config.storage', '@config.storage.snapshot'] + exception.needs_installer: + class: Drupal\Core\EventSubscriber\ExceptionDetectNeedsInstallSubscriber + arguments: ['@database'] + tags: + - { name: event_subscriber } exception.default_json: class: Drupal\Core\EventSubscriber\ExceptionJsonSubscriber tags: diff --git a/core/lib/Drupal/Core/Database/DatabaseNotFoundException.php b/core/lib/Drupal/Core/Database/DatabaseAccessDeniedException.php similarity index 52% copy from core/lib/Drupal/Core/Database/DatabaseNotFoundException.php copy to core/lib/Drupal/Core/Database/DatabaseAccessDeniedException.php index 63eb9a3..a797fea 100644 --- a/core/lib/Drupal/Core/Database/DatabaseNotFoundException.php +++ b/core/lib/Drupal/Core/Database/DatabaseAccessDeniedException.php @@ -5,4 +5,4 @@ /** * Exception thrown if specified database is not found. */ -class DatabaseNotFoundException extends \RuntimeException {} +class DatabaseAccessDeniedException extends \RuntimeException implements DatabaseException {} diff --git a/core/lib/Drupal/Core/Database/DatabaseNotFoundException.php b/core/lib/Drupal/Core/Database/DatabaseNotFoundException.php index 63eb9a3..1c5b5eb 100644 --- a/core/lib/Drupal/Core/Database/DatabaseNotFoundException.php +++ b/core/lib/Drupal/Core/Database/DatabaseNotFoundException.php @@ -5,4 +5,4 @@ /** * Exception thrown if specified database is not found. */ -class DatabaseNotFoundException extends \RuntimeException {} +class DatabaseNotFoundException extends \RuntimeException implements DatabaseException {} diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php index df51067..fd46438 100644 --- a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Database\Driver\mysql; +use Drupal\Core\Database\DatabaseAccessDeniedException; use Drupal\Core\Database\DatabaseExceptionWrapper; use Drupal\Core\Database\Database; @@ -27,6 +28,11 @@ class Connection extends DatabaseConnection { const DATABASE_NOT_FOUND = 1049; /** + * Error code for "Access denied" error. + */ + const ACCESS_DENIED = 1045; + + /** * Error code for "Can't initialize character set" error. */ const UNSUPPORTED_CHARSET = 2019; @@ -139,7 +145,18 @@ public static function open(array &$connection_options = array()) { $connection_options['pdo'] += [\PDO::MYSQL_ATTR_MULTI_STATEMENTS => FALSE]; } - $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']); + try { + $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']); + } + catch (\PDOException $e) { + if ($e->getCode() == static::DATABASE_NOT_FOUND) { + throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e); + } + if ($e->getCode() == static::ACCESS_DENIED) { + throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e); + } + throw $e; + } // Force MySQL to use the UTF-8 character set. Also set the collation, if a // certain one has been set; otherwise, MySQL defaults to diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php index 4688cde..042c221 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php @@ -4,6 +4,7 @@ use Drupal\Core\Database\Database; use Drupal\Core\Database\Connection as DatabaseConnection; +use Drupal\Core\Database\DatabaseAccessDeniedException; use Drupal\Core\Database\DatabaseNotFoundException; /** @@ -27,6 +28,13 @@ class Connection extends DatabaseConnection { const DATABASE_NOT_FOUND = 7; /** + * Error code for "Access denied" error. + * + * Technically the error message is "insufficient_privilege". + */ + const ACCESS_DENIED = '42501'; + + /** * The list of PostgreSQL reserved key words. * * @see http://www.postgresql.org/docs/9.4/static/sql-keywords-appendix.html @@ -113,7 +121,19 @@ public static function open(array &$connection_options = array()) { // Convert numeric values to strings when fetching. \PDO::ATTR_STRINGIFY_FETCHES => TRUE, ); - $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']); + + try { + $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']); + } + catch (\PDOException $e) { + if ($e->getCode() == static::DATABASE_NOT_FOUND) { + throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e); + } + if ($e->getCode() == static::ACCESS_DENIED) { + throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e); + } + throw $e; + } return $pdo; } diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php index 2f5374b..10c63e8 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php @@ -99,7 +99,19 @@ public static function open(array &$connection_options = array()) { // Convert numeric values to strings when fetching. \PDO::ATTR_STRINGIFY_FETCHES => TRUE, ); - $pdo = new \PDO('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']); + + try { + $pdo = new \PDO('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']); + } + catch (\PDOException $e) { + if ($e->getCode() == static::DATABASE_NOT_FOUND) { + throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e); + } + // SQLite doesn't have a distinct error code for access denied, so don't + // deal with that case. + throw $e; + } + // Create functions needed by SQLite. $pdo->sqliteCreateFunction('if', array(__CLASS__, 'sqlFunctionIf')); diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 1a3b943..db2df4f5 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -16,6 +16,7 @@ use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\File\MimeType\MimeTypeGuesser; use Drupal\Core\Http\TrustedHostsRequestFactory; +use Drupal\Core\Installer\CheckInstalledTrait; use Drupal\Core\Language\Language; use Drupal\Core\Site\Settings; use Symfony\Cmf\Component\Routing\RouteObjectInterface; @@ -44,6 +45,7 @@ * container, or modify existing services. */ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { + use CheckInstalledTrait; /** * Holds the class used for dumping the container to a PHP array. @@ -621,7 +623,7 @@ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = // installed yet (i.e., if no $databases array has been defined in the // settings.php file) and we are not already installing. if (!Database::getConnectionInfo() && !drupal_installation_attempted() && PHP_SAPI !== 'cli') { - $response = new RedirectResponse($request->getBasePath() . '/core/install.php'); + $response = new RedirectResponse($request->getBasePath() . '/core/install.php', 302, ['Cache-Control' => 'no-cache']); } else { $this->boot(); @@ -660,14 +662,17 @@ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = * If the passed in exception cannot be turned into a response. */ protected function handleException(\Exception $e, $request, $type) { + if ($this->shouldRedirectToInstaller($e, $this->container ? $this->container->get('database') : NULL)) { + return new RedirectResponse($request->getBasePath() . '/core/install.php', 302, ['Cache-Control' => 'no-cache']); + } + if ($e instanceof HttpExceptionInterface) { $response = new Response($e->getMessage(), $e->getStatusCode()); $response->headers->add($e->getHeaders()); return $response; } - else { - throw $e; - } + + throw $e; } /** diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionDetectNeedsInstallSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionDetectNeedsInstallSubscriber.php new file mode 100644 index 0000000..b94a0dc --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionDetectNeedsInstallSubscriber.php @@ -0,0 +1,65 @@ +connection = $connection; + } + + /** + * Handles errors for this subscriber. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function onException(GetResponseForExceptionEvent $event) { + $exception = $event->getException(); + if ($this->shouldRedirectToInstaller($exception, $this->connection)) { + // Only redirect if this is an HTML response (i.e., a user trying to view + // the site in a web browser before installing it). + $request = $event->getRequest(); + $format = $request->query->get(MainContentViewSubscriber::WRAPPER_FORMAT, $request->getRequestFormat()); + if ($format == 'html') { + $event->setResponse(new RedirectResponse($request->getBasePath() . '/core/install.php', 302, ['Cache-Control' => 'no-cache'])); + } + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + public static function getSubscribedEvents() { + $events[KernelEvents::EXCEPTION][] = ['onException', 100]; + return $events; + } + +} diff --git a/core/lib/Drupal/Core/Installer/CheckInstalledTrait.php b/core/lib/Drupal/Core/Installer/CheckInstalledTrait.php new file mode 100644 index 0000000..4fe8af8 --- /dev/null +++ b/core/lib/Drupal/Core/Installer/CheckInstalledTrait.php @@ -0,0 +1,80 @@ +schema()->tableExists('sessions'); + } + catch (\Exception $e) { + // If we still have an exception at this point, we need to be careful + // since we should not redirect if the exception represents an error on + // an already-installed site (for example, if the database server went + // down). Assume we shouldn't redirect, just in case. + return FALSE; + } + } + + // When in doubt, don't redirect. + return FALSE; + } +} diff --git a/core/modules/system/src/Tests/System/UncaughtExceptionTest.php b/core/modules/system/src/Tests/System/UncaughtExceptionTest.php index 0cc6862..ed25264 100644 --- a/core/modules/system/src/Tests/System/UncaughtExceptionTest.php +++ b/core/modules/system/src/Tests/System/UncaughtExceptionTest.php @@ -223,7 +223,7 @@ public function testLostDatabaseConnection() { 'value' => $incorrect_username, 'required' => TRUE, ); - $settings['databases']['default']['default']['passowrd'] = (object) array( + $settings['databases']['default']['default']['password'] = (object) array( 'value' => $this->randomMachineName(16), 'required' => TRUE, ); @@ -232,7 +232,7 @@ public function testLostDatabaseConnection() { $this->drupalGet(''); $this->assertResponse(500); - $this->assertRaw('PDOException'); + $this->assertRaw('DatabaseAccessDeniedException'); $this->assertErrorLogged($this->expectedExceptionMessage); }