diff --git a/core/INSTALL.txt b/core/INSTALL.txt
index 5d5525b..c9803a4 100644
--- a/core/INSTALL.txt
+++ b/core/INSTALL.txt
@@ -2,6 +2,7 @@
CONTENTS OF THIS FILE
---------------------
+ * Quickstart
* Requirements and notes
* Optional server requirements
* Installation
@@ -10,6 +11,33 @@ CONTENTS OF THIS FILE
* Multisite configuration
* Multilingual configuration
+QUICKSTART
+----------------------
+
+Prerequisites:
+- PHP 5.5.9 (or greater) (https://php.net).
+
+In the instructions below, replace the version 8.x.x with the specific version
+you wish to download. Example: 8.6.0.zip. You can find the latest stable version
+at https://www.drupal.org/project/drupal.
+
+Download and extract the Drupal package
+- curl -sS https://ftp.drupal.org/files/projects/drupal-8.x.x.zip --output drupal-8.x.x.zip
+- unzip drupal-8.x.x.zip
+- cd /path/to/drupal-8.x.x
+// Installs Drupal and open it up
+- php core/scripts/drupal quick-start
+
+Wait… installation can take a minute or two. A successful installation will
+result in logging in to your new site in your web browser.
+
+NOTE: This quick start solution uses PHP's built-in web server and is not
+intended for production use. Read more about how to run Drupal in a production
+environment below.
+
+You may need to configure server settings such as the host address or port. Run
+php core/scripts/drupal quick-start --help for a list of available options.
+
REQUIREMENTS AND NOTES
----------------------
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index c27f75e..fb0400a 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -89,10 +89,13 @@
* page request (optimized for the command line) and not send any output
* intended for the web browser. See install_state_defaults() for a list of
* elements that are allowed to appear in this array.
+ * @param callable $callback
+ * (optional) A callback to allow command line processes to update a progress
+ * bar. The callback is passed the $install_state variable.
*
* @see install_state_defaults()
*/
-function install_drupal($class_loader, $settings = []) {
+function install_drupal($class_loader, $settings = [], callable $callback = NULL) {
// Support the old way of calling this function with just a settings array.
// @todo Remove this when Drush is updated in the Drupal testing
// infrastructure in https://www.drupal.org/node/2389243
@@ -114,7 +117,7 @@ function install_drupal($class_loader, $settings = []) {
install_begin_request($class_loader, $install_state);
// Based on the installation state, run the remaining tasks for this page
// request, and collect any output.
- $output = install_run_tasks($install_state);
+ $output = install_run_tasks($install_state, $callback);
}
catch (InstallerException $e) {
// In the non-interactive installer, exceptions are always thrown directly.
@@ -285,6 +288,8 @@ function install_state_defaults() {
* @param $install_state
* An array of information about the current installation state. This is
* modified with information gleaned from the beginning of the page request.
+ *
+ * @see install_drupal()
*/
function install_begin_request($class_loader, &$install_state) {
$request = Request::createFromGlobals();
@@ -324,7 +329,7 @@ function install_begin_request($class_loader, &$install_state) {
date_default_timezone_set('Australia/Sydney');
}
- $site_path = DrupalKernel::findSitePath($request, FALSE);
+ $site_path = empty($install_state['site_path']) ? DrupalKernel::findSitePath($request, FALSE) : $install_state['site_path'];
Settings::initialize(dirname(dirname(__DIR__)), $site_path, $class_loader);
// Ensure that procedural dependencies are loaded as early as possible,
@@ -535,11 +540,14 @@ function install_begin_request($class_loader, &$install_state) {
* @param $install_state
* An array of information about the current installation state. This is
* passed along to each task, so it can be modified if necessary.
+ * @param callable $callback
+ * (optional) A callback to allow command line processes to update a progress
+ * bar. The callback is passed the $install_state variable.
*
* @return
* HTML output from the last completed task.
*/
-function install_run_tasks(&$install_state) {
+function install_run_tasks(&$install_state, callable $callback = NULL) {
do {
// Obtain a list of tasks to perform. The list of tasks itself can be
// dynamic (e.g., some might be defined by the installation profile,
@@ -571,6 +579,9 @@ function install_run_tasks(&$install_state) {
\Drupal::state()->set('install_task', $install_state['installation_finished'] ? 'done' : $task_name);
}
}
+ if ($callback) {
+ $callback($install_state);
+ }
// Stop when there are no tasks left. In the case of an interactive
// installation, also stop if we have some output to send to the browser,
// the URL parameters have changed, or an end to the page request was
diff --git a/core/lib/Drupal/Core/Command/InstallCommand.php b/core/lib/Drupal/Core/Command/InstallCommand.php
new file mode 100644
index 0000000..ffa342d
--- /dev/null
+++ b/core/lib/Drupal/Core/Command/InstallCommand.php
@@ -0,0 +1,336 @@
+classLoader = $class_loader;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure() {
+ $this->setName('install')
+ ->setDescription('Installs a Drupal development site. This is not meant for production or any custom development. It is a quick and easy way to get Drupal running.')
+ ->addArgument('install-profile', InputArgument::OPTIONAL, 'Install profile to install the site in.')
+ ->addOption('langcode', NULL, InputOption::VALUE_OPTIONAL, 'The language to install the site in.', 'en')
+ ->addOption('site-name', NULL, InputOption::VALUE_OPTIONAL, 'Set the site name.', 'Drupal')
+ ->addUsage('demo_umami --langcode fr')
+ ->addUsage('standard --site-name QuickInstall');
+
+ parent::configure();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $io = new SymfonyStyle($input, $output);
+ if (!extension_loaded('pdo_sqlite')) {
+ $io->getErrorStyle()->error('You must have the SQLite PHP extension installed. See https://secure.php.net/manual/en/sqlite.installation.php for instructions.');
+ return 1;
+ }
+
+ // Change the directory to the Drupal root.
+ chdir(dirname(dirname(dirname(dirname(dirname(__DIR__))))));
+
+ // Check whether there is already an installation.
+ if ($this->isDrupalInstalled()) {
+ // Do not fail if the site is already installed so this command can be
+ // chained with ServerCommand.
+ $output->writeln('Drupal is already installed.');
+ return 0;
+ }
+
+ $install_profile = $input->getArgument('install-profile');
+ if ($install_profile && !$this->validateProfile($install_profile, $io)) {
+ return 1;
+ }
+ if (!$install_profile) {
+ $install_profile = $this->selectProfile($io);
+ }
+
+ return $this->install($this->classLoader, $io, $install_profile, $input->getOption('langcode'), $this->getSitePath(), $input->getOption('site-name'));
+ }
+
+ /**
+ * Returns whether there is already an existing Drupal installation.
+ *
+ * @return bool
+ */
+ protected function isDrupalInstalled() {
+ try {
+ $kernel = new DrupalKernel('prod', $this->classLoader, FALSE);
+ $kernel::bootEnvironment();
+ $kernel->setSitePath($this->getSitePath());
+ Settings::initialize($kernel->getAppRoot(), $kernel->getSitePath(), $this->classLoader);
+ $kernel->boot();
+ }
+ catch (ConnectionNotDefinedException $e) {
+ return FALSE;
+ }
+ return !empty(Database::getConnectionInfo());
+ }
+
+ /**
+ * Installs Drupal with specified installation profile.
+ *
+ * @param object $class_loader
+ * The class loader.
+ * @param \Symfony\Component\Console\Style\SymfonyStyle $io
+ * The Symfony output decorator.
+ * @param string $profile
+ * The installation profile to use.
+ * @param string $langcode
+ * The language to install the site in.
+ * @param string $site_path
+ * The path to install the site to, like 'sites/default'.
+ * @param string $site_name
+ * The site name.
+ *
+ * @throws \Exception
+ * Thrown when failing to create the $site_path directory or settings.php.
+ */
+ protected function install($class_loader, SymfonyStyle $io, $profile, $langcode, $site_path, $site_name) {
+ $password = Crypt::randomBytesBase64(12);
+ $parameters = [
+ 'interactive' => FALSE,
+ 'site_path' => $site_path,
+ 'parameters' => [
+ 'profile' => $profile,
+ 'langcode' => $langcode,
+ ],
+ 'forms' => [
+ 'install_settings_form' => [
+ 'driver' => 'sqlite',
+ 'sqlite' => [
+ 'database' => $site_path . '/files/.sqlite',
+ ],
+ ],
+ 'install_configure_form' => [
+ 'site_name' => $site_name,
+ 'site_mail' => 'drupal@localhost',
+ 'account' => [
+ 'name' => 'admin',
+ 'mail' => 'admin@localhost',
+ 'pass' => [
+ 'pass1' => $password,
+ 'pass2' => $password,
+ ],
+ ],
+ 'enable_update_status_module' => TRUE,
+ // form_type_checkboxes_value() requires NULL instead of FALSE values
+ // for programmatic form submissions to disable a checkbox.
+ 'enable_update_status_emails' => NULL,
+ ],
+ ],
+ ];
+
+ // Create the directory and settings.php if not there so that the installer
+ // works.
+ if (!is_dir($site_path)) {
+ if ($io->isVerbose()) {
+ $io->writeln("Creating directory: $site_path");
+ }
+ if (!mkdir($site_path, 0775)) {
+ throw new \RuntimeException("Failed to create directory $site_path");
+ }
+ }
+ if (!file_exists("{$site_path}/settings.php")) {
+ if ($io->isVerbose()) {
+ $io->writeln("Creating file: {$site_path}/settings.php");
+ }
+ if (!copy('sites/default/default.settings.php', "{$site_path}/settings.php")) {
+ throw new \RuntimeException("Copying sites/default/default.settings.php to {$site_path}/settings.php failed.");
+ }
+ }
+
+ $io->writeln('Drupal installation started. This could take a minute.');
+ require_once 'core/includes/install.core.inc';
+
+ $progress_bar = $io->createProgressBar();
+ install_drupal($class_loader, $parameters, function ($install_state) use ($progress_bar) {
+ static $started = FALSE;
+ if (!$started) {
+ $started = TRUE;
+ $tasks = install_tasks_to_perform($install_state);
+ // We've already done 1.
+ $progress_bar->setFormat("%current%/%max% [%bar%]\n%message%\n");
+ $progress_bar->start(count($tasks) + 1);
+ $progress_bar->setMessage(t('Installing @drupal', ['@drupal' => drupal_install_profile_distribution_name()]));
+ }
+ $task = current(install_tasks_to_perform($install_state));
+ if (isset($task['display_name'])) {
+ $progress_bar->setMessage($task['display_name']);
+ }
+ $progress_bar->advance();
+ });
+ $success_message = t('Congratulations, you installed @drupal!', [
+ '@drupal' => drupal_install_profile_distribution_name(),
+ '@name' => 'admin',
+ '@pass' => $password,
+ ], ['langcode' => $langcode]);
+ $progress_bar->setMessage('' . $success_message . '');
+ $progress_bar->finish();
+ $io->writeln('Username: admin');
+ $io->writeln("Password: $password");
+ }
+
+ /**
+ * Gets the site path.
+ *
+ * Defaults to 'sites/default'. For testing purposes this can be overridden
+ * using the DRUPAL_DEV_SITE_PATH environment variable.
+ *
+ * @return string
+ * The site path to use.
+ */
+ protected function getSitePath() {
+ return getenv('DRUPAL_DEV_SITE_PATH') ?: 'sites/default';
+ }
+
+ /**
+ * Selects the install profile to use.
+ *
+ * @param \Symfony\Component\Console\Style\SymfonyStyle $io
+ * Symfony style output decorator.
+ *
+ * @return string
+ * The selected install profile.
+ *
+ * @see _install_select_profile()
+ * @see \Drupal\Core\Installer\Form\SelectProfileForm
+ */
+ protected function selectProfile(SymfonyStyle $io) {
+ $profiles = $this->getProfiles();
+
+ // If there is a distribution there will be only one profile.
+ if (count($profiles) == 1) {
+ return key($profiles);
+ }
+ // Display alphabetically by human-readable name, but always put the core
+ // profiles first (if they are present in the filesystem).
+ natcasesort($profiles);
+ if (isset($profiles['minimal'])) {
+ // If the expert ("Minimal") core profile is present, put it in front of
+ // any non-core profiles rather than including it with them
+ // alphabetically, since the other profiles might be intended to group
+ // together in a particular way.
+ $profiles = ['minimal' => $profiles['minimal']] + $profiles;
+ }
+ if (isset($profiles['standard'])) {
+ // If the default ("Standard") core profile is present, put it at the very
+ // top of the list. This profile will have its radio button pre-selected,
+ // so we want it to always appear at the top.
+ $profiles = ['standard' => $profiles['standard']] + $profiles;
+ }
+ reset($profiles);
+ return $io->choice('Select an installation profile', $profiles, current($profiles));
+ }
+
+ /**
+ * Validates a user provided install profile.
+ *
+ * @param string $install_profile
+ * Install profile to validate.
+ * @param \Symfony\Component\Console\Style\SymfonyStyle $io
+ * Symfony style output decorator.
+ *
+ * @return bool
+ * TRUE if the profile is valid, FALSE if not.
+ */
+ protected function validateProfile($install_profile, SymfonyStyle $io) {
+ // Allow people to install hidden and non-distribution profiles if they
+ // supply the argument.
+ $profiles = $this->getProfiles(TRUE, FALSE);
+ if (!isset($profiles[$install_profile])) {
+ $error_msg = sprintf("'%s' is not a valid install profile.", $install_profile);
+ $alternatives = [];
+ foreach (array_keys($profiles) as $profile_name) {
+ $lev = levenshtein($install_profile, $profile_name);
+ if ($lev <= strlen($profile_name) / 4 || FALSE !== strpos($profile_name, $install_profile)) {
+ $alternatives[] = $profile_name;
+ }
+ }
+ if (!empty($alternatives)) {
+ $error_msg .= sprintf(" Did you mean '%s'?", implode("' or '", $alternatives));
+ }
+ $io->getErrorStyle()->error($error_msg);
+ return FALSE;
+ }
+ return TRUE;
+ }
+
+ /**
+ * Gets a list of profiles.
+ *
+ * @param bool $include_hidden
+ * (optional) Whether to include hidden profiles. Defaults to FALSE.
+ * @param bool $auto_select_distributions
+ * (optional) Whether to only return the first distribution found.
+ *
+ * @return string[]
+ * An array of profile descriptions keyed by the profile machine name.
+ */
+ protected function getProfiles($include_hidden = FALSE, $auto_select_distributions = TRUE) {
+ // Build a list of all available profiles.
+ $listing = new ExtensionDiscovery(getcwd(), FALSE);
+ $listing->setProfileDirectories([]);
+ $profiles = [];
+ $info_parser = new InfoParserDynamic();
+ foreach ($listing->scan('profile') as $profile) {
+ $details = $info_parser->parse($profile->getPathname());
+ // Don't show hidden profiles.
+ if (!$include_hidden && !empty($details['hidden'])) {
+ continue;
+ }
+ // Determine the name of the profile; default to the internal name if none
+ // is specified.
+ $name = isset($details['name']) ? $details['name'] : $profile->getName();
+ $description = isset($details['description']) ? $details['description'] : $name;
+ $profiles[$profile->getName()] = $description;
+
+ if ($auto_select_distributions && !empty($details['distribution'])) {
+ return [$profile->getName() => $description];
+ }
+ }
+ return $profiles;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Command/QuickStartCommand.php b/core/lib/Drupal/Core/Command/QuickStartCommand.php
new file mode 100644
index 0000000..15a2922
--- /dev/null
+++ b/core/lib/Drupal/Core/Command/QuickStartCommand.php
@@ -0,0 +1,75 @@
+setName('quick-start')
+ ->setDescription('Installs a Drupal site and runs a web server. This is not meant for production or any custom development. It is a quick and easy way to get Drupal running.')
+ ->addArgument('install-profile', InputArgument::OPTIONAL, 'Install profile to install the site in.')
+ ->addOption('langcode', NULL, InputOption::VALUE_OPTIONAL, 'The language to install the site in. Defaults to en.', 'en')
+ ->addOption('site-name', NULL, InputOption::VALUE_OPTIONAL, 'Set the site name. Defaults to Drupal.', 'Drupal')
+ ->addOption('host', NULL, InputOption::VALUE_OPTIONAL, 'Provide a host for the server to run on. Defaults to 127.0.0.1.', '127.0.0.1')
+ ->addOption('port', NULL, InputOption::VALUE_OPTIONAL, 'Provide a port for the server to run on. Will be determined automatically if none supplied.')
+ ->addOption('suppress-login', 's', InputOption::VALUE_NONE, 'Disable opening a login URL in a browser.')
+ ->addUsage('demo_umami --langcode fr')
+ ->addUsage('standard --site-name QuickInstall --host localhost --port 8080');
+
+ parent::configure();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $command = $this->getApplication()->find('install');
+
+ $arguments = [
+ 'command' => 'install',
+ 'install-profile' => $input->getArgument('install-profile'),
+ '--langcode' => $input->getOption('langcode'),
+ '--site-name' => $input->getOption('site-name'),
+ ];
+
+ $installInput = new ArrayInput($arguments);
+ $returnCode = $command->run($installInput, $output);
+
+ if ($returnCode === 0) {
+ $command = $this->getApplication()->find('server');
+ $arguments = [
+ 'command' => 'server',
+ '--host' => $input->getOption('host'),
+ '--port' => $input->getOption('port'),
+ ];
+ if ($input->getOption('suppress-login')) {
+ $arguments['--suppress-login'] = TRUE;
+ }
+ $serverInput = new ArrayInput($arguments);
+ $returnCode = $command->run($serverInput, $output);
+ }
+ return $returnCode;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Command/ServerCommand.php b/core/lib/Drupal/Core/Command/ServerCommand.php
new file mode 100644
index 0000000..f06daf3
--- /dev/null
+++ b/core/lib/Drupal/Core/Command/ServerCommand.php
@@ -0,0 +1,263 @@
+classLoader = $class_loader;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure() {
+ $this->setDescription('Starts up a webserver for a site.')
+ ->addOption('host', NULL, InputOption::VALUE_OPTIONAL, 'Provide a host for the server to run on.', '127.0.0.1')
+ ->addOption('port', NULL, InputOption::VALUE_OPTIONAL, 'Provide a port for the server to run on. Will be determined automatically if none supplied.')
+ ->addOption('suppress-login', 's', InputOption::VALUE_NONE, 'Disable opening a login URL in a browser.')
+ ->addUsage('--host localhost --port 8080');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $io = new SymfonyStyle($input, $output);
+
+ $host = $input->getOption('host');
+ $port = $input->getOption('port');
+ if (!$port) {
+ $port = $this->findAvailablePort($host);
+ }
+ if (!$port) {
+ $io->getErrorStyle()->error('Unable to automatically determine a port. Use the --port to hardcode an available port.');
+ }
+
+ try {
+ $kernel = $this->boot();
+ }
+ catch (ConnectionNotDefinedException $e) {
+ $io->getErrorStyle()->error('No installation found. Use the \'install\' command.');
+ return 1;
+ }
+ return $this->start($host, $port, $kernel, $input, $io);
+ }
+
+ /**
+ * Boots up a Drupal environment.
+ *
+ * @return \Drupal\Core\DrupalKernelInterface
+ * The Drupal kernel.
+ *
+ * @throws \Exception
+ * Exception thrown if kernel does not boot.
+ */
+ protected function boot() {
+ $kernel = new DrupalKernel('prod', $this->classLoader, FALSE);
+ $kernel::bootEnvironment();
+ $kernel->setSitePath($this->getSitePath());
+ Settings::initialize($kernel->getAppRoot(), $kernel->getSitePath(), $this->classLoader);
+ $kernel->boot();
+ // Some services require a request to work. For example, CommentManager.
+ // This is needed as generating the URL fires up entity load hooks.
+ $kernel->getContainer()
+ ->get('request_stack')
+ ->push(Request::createFromGlobals());
+
+ return $kernel;
+ }
+
+ /**
+ * Finds an available port.
+ *
+ * @param string $host
+ * The host to find a port on.
+ *
+ * @return int|false
+ * The available port or FALSE, if no available port found,
+ */
+ protected function findAvailablePort($host) {
+ $port = 8888;
+ while ($port >= 8888 && $port <= 9999) {
+ $connection = @fsockopen($host, $port);
+ if (is_resource($connection)) {
+ // Port is being used.
+ fclose($connection);
+ }
+ else {
+ // Port is available.
+ return $port;
+ }
+ $port++;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Opens a URL in your system default browser.
+ *
+ * @param string $url
+ * The URL to browser to.
+ * @param \Symfony\Component\Console\Style\SymfonyStyle $io
+ * The IO.
+ */
+ protected function openBrowser($url, SymfonyStyle $io) {
+ $is_windows = defined('PHP_WINDOWS_VERSION_BUILD');
+ if ($is_windows) {
+ // Handle escaping ourselves.
+ $cmd = 'start "web" "' . $url . '""';
+ }
+ else {
+ $url = escapeshellarg($url);
+ }
+
+ $is_linux = (new Process('which xdg-open'))->run();
+ $is_osx = (new Process('which open'))->run();
+ if ($is_linux === 0) {
+ $cmd = 'xdg-open ' . $url;
+ }
+ elseif ($is_osx === 0) {
+ $cmd = 'open ' . $url;
+ }
+
+ if (empty($cmd)) {
+ $io->getErrorStyle()
+ ->error('No suitable browser opening command found, open yourself: ' . $url);
+ return;
+ }
+
+ if ($io->isVerbose()) {
+ $io->writeln("Browser command: $cmd");
+ }
+
+ // Need to escape double quotes in the command so the PHP will work.
+ $cmd = str_replace('"', '\"', $cmd);
+ // Sleep for 2 seconds before opening the browser. This allows the command
+ // to start up the PHP built-in webserver in the meantime. We use a
+ // PhpProcess so that Windows powershell users also get a browser opened
+ // for them.
+ $php = "";
+ $process = new PhpProcess($php);
+ $process->start();
+ return;
+ }
+
+ /**
+ * Gets a one time login URL for user 1.
+ *
+ * @return string
+ * The one time login URL for user 1.
+ */
+ protected function getOneTimeLoginUrl() {
+ $user = User::load(1);
+ \Drupal::moduleHandler()->load('user');
+ return user_pass_reset_url($user);
+ }
+
+ /**
+ * Starts up a webserver with a running Drupal.
+ *
+ * @param string $host
+ * The hostname of the webserver.
+ * @param int $port
+ * The port to start the webserver on.
+ * @param \Drupal\Core\DrupalKernelInterface $kernel
+ * The Drupal kernel.
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * The input.
+ * @param \Symfony\Component\Console\Style\SymfonyStyle $io
+ * The IO.
+ *
+ * @return int
+ * The exit status of the PHP in-built webserver command.
+ */
+ protected function start($host, $port, DrupalKernelInterface $kernel, InputInterface $input, SymfonyStyle $io) {
+ $finder = new PhpExecutableFinder();
+ $binary = $finder->find();
+ if ($binary === FALSE) {
+ throw new \RuntimeException('Unable to find the PHP binary.');
+ }
+
+ $io->writeln("Drupal development server started: ");
+ $one_time_login = "http://$host:$port{$this->getOneTimeLoginUrl()}/login";
+ $io->writeln("One time login url: <$one_time_login>");
+ $io->writeln('Press Ctrl-C to quit.');
+
+ if (!$input->getOption('suppress-login')) {
+ if ($this->openBrowser("$one_time_login?destination=" . urlencode("/"), $io) === 1) {
+ $io->error('Error while opening up a one time login URL');
+ }
+ }
+
+ // Use the Process object to construct an escaped command line.
+ $process = new Process([
+ $binary,
+ '-S',
+ $host . ':' . $port,
+ '.ht.router.php',
+ ], $kernel->getAppRoot(), [], NULL, NULL);
+ if ($io->isVerbose()) {
+ $io->writeln("Server command: {$process->getCommandLine()}");
+ }
+
+ // Write a blank line so that server output and the useful information are
+ // visually separated.
+ $io->writeln('');
+ $cwd = getcwd();
+ chdir($kernel->getAppRoot());
+ // To support Windows we can't open the server in another process.
+ passthru($process->getCommandLine(), $status);
+ chdir($cwd);
+ return $status;
+ }
+
+ /**
+ * Gets the site path.
+ *
+ * Defaults to 'sites/default'. For testing purposes this can be overridden
+ * using the DRUPAL_DEV_SITE_PATH environment variable.
+ *
+ * @return string
+ * The site path to use.
+ */
+ protected function getSitePath() {
+ return getenv('DRUPAL_DEV_SITE_PATH') ?: 'sites/default';
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Composer/Composer.php b/core/lib/Drupal/Core/Composer/Composer.php
index c84797f..9833b7e 100644
--- a/core/lib/Drupal/Core/Composer/Composer.php
+++ b/core/lib/Drupal/Core/Composer/Composer.php
@@ -6,6 +6,7 @@
use Composer\Script\Event;
use Composer\Installer\PackageEvent;
use Composer\Semver\Constraint\Constraint;
+use Composer\Util\ProcessExecutor;
/**
* Provides static functions for composer script events.
@@ -270,6 +271,13 @@ protected static function findPackageKey($package_name) {
}
/**
+ * Removes Composer's timeout so that scripts can run indefinitely.
+ */
+ public static function removeTimeout() {
+ ProcessExecutor::setTimeout(0);
+ }
+
+ /**
* Helper method to remove directories and the files they contain.
*
* @param string $path
diff --git a/core/lib/Drupal/Core/Installer/InstallerKernel.php b/core/lib/Drupal/Core/Installer/InstallerKernel.php
index adeb5c5..eefeffe 100644
--- a/core/lib/Drupal/Core/Installer/InstallerKernel.php
+++ b/core/lib/Drupal/Core/Installer/InstallerKernel.php
@@ -3,6 +3,7 @@
namespace Drupal\Core\Installer;
use Drupal\Core\DrupalKernel;
+use Symfony\Component\HttpFoundation\Request;
/**
* Extend DrupalKernel to handle force some kernel behaviors.
@@ -66,4 +67,15 @@ public function getInstallProfile() {
return $profile;
}
+ /**
+ * {@inheritdoc}
+ */
+ public static function createFromRequest(Request $request, $class_loader, $environment, $allow_dumping = TRUE, $app_root = NULL) {
+ // This override exists because we don't need to initialize the settings
+ // again as they already are in install_begin_request().
+ $kernel = new static($environment, $class_loader, $allow_dumping, $app_root);
+ static::bootEnvironment($app_root);
+ return $kernel;
+ }
+
}
diff --git a/core/scripts/drupal b/core/scripts/drupal
new file mode 100644
index 0000000..7f71228
--- /dev/null
+++ b/core/scripts/drupal
@@ -0,0 +1,26 @@
+#!/usr/bin/env php
+add(new QuickStartCommand());
+$application->add(new InstallCommand($classloader));
+$application->add(new ServerCommand($classloader));
+
+$application->run();
diff --git a/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php b/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php
new file mode 100644
index 0000000..b8f346f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php
@@ -0,0 +1,256 @@
+php = $php_executable_finder->find();
+ $this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))));
+ chdir($this->root);
+ if (!is_writable("{$this->root}/sites/simpletest")) {
+ $this->markTestSkipped('This test requires a writable sites/simpletest directory');
+ }
+ // Get a lock and a valid site path.
+ $this->testDb = new TestDatabase();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function tearDown() {
+ if ($this->testDb) {
+ $test_site_directory = $this->root . DIRECTORY_SEPARATOR . $this->testDb->getTestSitePath();
+ if (file_exists($test_site_directory)) {
+ // @todo use the tear down command from
+ // https://www.drupal.org/project/drupal/issues/2926633
+ // Delete test site directory.
+ $this->fileUnmanagedDeleteRecursive($test_site_directory, [
+ BrowserTestBase::class,
+ 'filePreDeleteCallback'
+ ]);
+ }
+ }
+ parent::tearDown();
+ }
+
+ /**
+ * Tests the quick-start command.
+ */
+ public function testQuickStartCommand() {
+ // Install a site using the standard profile to ensure the one time login
+ // link generation works.
+ $install_command = "{$this->php} core/scripts/drupal quick-start standard --site-name='Test site {$this->testDb->getDatabasePrefix()}' --suppress-login";
+ $process = new Process($install_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]);
+ $process->inheritEnvironmentVariables();
+ $process->setTimeout(500);
+ $process->start();
+ $guzzle = new Client();
+ $port = FALSE;
+ while ($process->isRunning()) {
+ if (preg_match('/127.0.0.1:(\d+)/', $process->getOutput(), $match)) {
+ $port = $match[1];
+ break;
+ }
+ // Wait for more output.
+ sleep(1);
+ }
+ $this->assertContains('Drupal installation started. This could take a minute.', $process->getOutput());
+ $this->assertContains('Congratulations, you installed Drupal!', $process->getOutput());
+ $this->assertNotFalse($port, "Web server running on port $port");
+ $this->assertContains("127.0.0.1:$port/user/reset/1/", $process->getOutput());
+
+ // Give the server a couple of seconds to be ready.
+ sleep(2);
+
+ // Generate a cookie so we can make a request against the installed site.
+ include $this->root . '/core/includes/bootstrap.inc';
+ define('DRUPAL_TEST_IN_CHILD_SITE', FALSE);
+ chmod($this->testDb->getTestSitePath(), 0755);
+ $cookieJar = CookieJar::fromArray([
+ 'SIMPLETEST_USER_AGENT' => drupal_generate_test_ua($this->testDb->getDatabasePrefix())
+ ], '127.0.0.1');
+
+ $response = $guzzle->get('http://127.0.0.1:' . $port, ['cookies' => $cookieJar]);
+ $content = (string) $response->getBody();
+ $this->assertContains('Test site ' . $this->testDb->getDatabasePrefix(), $content);
+
+ // Stop the web server.
+ $process->stop();
+ }
+
+ /**
+ * Tests the quick-start commands.
+ */
+ public function testQuickStartInstallAndServerCommands() {
+ // Install a site.
+ $install_command = "{$this->php} core/scripts/drupal install testing --site-name='Test site {$this->testDb->getDatabasePrefix()}'";
+ $install_process = new Process($install_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]);
+ $install_process->inheritEnvironmentVariables();
+ $install_process->setTimeout(500);
+ $result = $install_process->run();
+ $this->assertContains('Drupal installation started. This could take a minute.', $install_process->getOutput());
+ $this->assertContains('Congratulations, you installed Drupal!', $install_process->getOutput());
+ $this->assertSame(0, $result);
+
+ // Run the PHP built-in webserver.
+ $server_command = "{$this->php} core/scripts/drupal server --suppress-login";
+ $server_process = new Process($server_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]);
+ $server_process->inheritEnvironmentVariables();
+ $server_process->start();
+ $guzzle = new Client();
+ $port = FALSE;
+ while ($server_process->isRunning()) {
+ if (preg_match('/127.0.0.1:(\d+)/', $server_process->getOutput(), $match)) {
+ $port = $match[1];
+ break;
+ }
+ // Wait for more output.
+ sleep(1);
+ }
+ $this->assertEquals('', $server_process->getErrorOutput());
+ $this->assertContains("127.0.0.1:$port/user/reset/1/", $server_process->getOutput());
+ $this->assertNotFalse($port, "Web server running on port $port");
+
+ // Give the server a couple of seconds to be ready.
+ sleep(2);
+
+ // Generate a cookie so we can make a request against the installed site.
+ include $this->root . '/core/includes/bootstrap.inc';
+ define('DRUPAL_TEST_IN_CHILD_SITE', FALSE);
+ chmod($this->testDb->getTestSitePath(), 0755);
+ $cookieJar = CookieJar::fromArray([
+ 'SIMPLETEST_USER_AGENT' => drupal_generate_test_ua($this->testDb->getDatabasePrefix())
+ ], '127.0.0.1');
+
+ $response = $guzzle->get('http://127.0.0.1:' . $port, ['cookies' => $cookieJar]);
+ $content = (string) $response->getBody();
+ $this->assertContains('Test site ' . $this->testDb->getDatabasePrefix(), $content);
+
+ // Try to re-install over the top of an existing site.
+ $install_command = "{$this->php} core/scripts/drupal install testing --site-name='Test another site {$this->testDb->getDatabasePrefix()}'";
+ $install_process = new Process($install_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]);
+ $install_process->inheritEnvironmentVariables();
+ $install_process->setTimeout(500);
+ $result = $install_process->run();
+ $this->assertContains('Drupal is already installed.', $install_process->getOutput());
+ $this->assertSame(0, $result);
+
+ // Ensure the site name has not changed.
+ $response = $guzzle->get('http://127.0.0.1:' . $port, ['cookies' => $cookieJar]);
+ $content = (string) $response->getBody();
+ $this->assertContains('Test site ' . $this->testDb->getDatabasePrefix(), $content);
+
+ // Stop the web server.
+ $server_process->stop();
+ }
+
+ /**
+ * Tests the install command with an invalid profile.
+ */
+ public function testQuickStartCommandProfileValidation() {
+ // Install a site using the standard profile to ensure the one time login
+ // link generation works.
+ $install_command = "{$this->php} core/scripts/drupal quick-start umami --site-name='Test site {$this->testDb->getDatabasePrefix()}' --suppress-login";
+ $process = new Process($install_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]);
+ $process->inheritEnvironmentVariables();
+ $process->run();
+ $this->assertContains('\'umami\' is not a valid install profile. Did you mean \'demo_umami\'?', $process->getErrorOutput());
+ }
+
+ /**
+ * Tests the server command when there is no installation.
+ */
+ public function testServerWithNoInstall() {
+ $server_command = "{$this->php} core/scripts/drupal server --suppress-login";
+ $server_process = new Process($server_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]);
+ $server_process->inheritEnvironmentVariables();
+ $server_process->run();
+ $this->assertContains('No installation found. Use the \'install\' command.', $server_process->getErrorOutput());
+ }
+
+ /**
+ * Deletes all files and directories in the specified path recursively.
+ *
+ * Note this method has no dependencies on Drupal core to ensure that the
+ * test site can be torn down even if something in the test site is broken.
+ *
+ * @param string $path
+ * A string containing either an URI or a file or directory path.
+ * @param callable $callback
+ * (optional) Callback function to run on each file prior to deleting it and
+ * on each directory prior to traversing it. For example, can be used to
+ * modify permissions.
+ *
+ * @return bool
+ * TRUE for success or if path does not exist, FALSE in the event of an
+ * error.
+ *
+ * @see file_unmanaged_delete_recursive()
+ */
+ protected function fileUnmanagedDeleteRecursive($path, $callback = NULL) {
+ if (isset($callback)) {
+ call_user_func($callback, $path);
+ }
+ if (is_dir($path)) {
+ $dir = dir($path);
+ while (($entry = $dir->read()) !== FALSE) {
+ if ($entry == '.' || $entry == '..') {
+ continue;
+ }
+ $entry_path = $path . '/' . $entry;
+ $this->fileUnmanagedDeleteRecursive($entry_path, $callback);
+ }
+ $dir->close();
+
+ return rmdir($path);
+ }
+ return unlink($path);
+ }
+
+}