annotate core/lib/Drupal/Core/Command/InstallCommand.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@17 1 <?php
Chris@17 2
Chris@17 3 namespace Drupal\Core\Command;
Chris@17 4
Chris@17 5 use Drupal\Component\Utility\Crypt;
Chris@17 6 use Drupal\Core\Database\ConnectionNotDefinedException;
Chris@17 7 use Drupal\Core\Database\Database;
Chris@17 8 use Drupal\Core\DrupalKernel;
Chris@17 9 use Drupal\Core\Extension\ExtensionDiscovery;
Chris@17 10 use Drupal\Core\Extension\InfoParserDynamic;
Chris@17 11 use Drupal\Core\Site\Settings;
Chris@17 12 use Symfony\Component\Console\Command\Command;
Chris@17 13 use Symfony\Component\Console\Input\InputArgument;
Chris@17 14 use Symfony\Component\Console\Input\InputInterface;
Chris@17 15 use Symfony\Component\Console\Input\InputOption;
Chris@17 16 use Symfony\Component\Console\Output\OutputInterface;
Chris@17 17 use Symfony\Component\Console\Style\SymfonyStyle;
Chris@17 18
Chris@17 19 /**
Chris@17 20 * Installs a Drupal site for local testing/development.
Chris@17 21 *
Chris@17 22 * @internal
Chris@17 23 * This command makes no guarantee of an API for Drupal extensions.
Chris@17 24 */
Chris@17 25 class InstallCommand extends Command {
Chris@17 26
Chris@17 27 /**
Chris@17 28 * The class loader.
Chris@17 29 *
Chris@17 30 * @var object
Chris@17 31 */
Chris@17 32 protected $classLoader;
Chris@17 33
Chris@17 34 /**
Chris@17 35 * Constructs a new InstallCommand command.
Chris@17 36 *
Chris@17 37 * @param object $class_loader
Chris@17 38 * The class loader.
Chris@17 39 */
Chris@17 40 public function __construct($class_loader) {
Chris@17 41 parent::__construct('install');
Chris@17 42 $this->classLoader = $class_loader;
Chris@17 43 }
Chris@17 44
Chris@17 45 /**
Chris@17 46 * {@inheritdoc}
Chris@17 47 */
Chris@17 48 protected function configure() {
Chris@17 49 $this->setName('install')
Chris@17 50 ->setDescription('Installs a Drupal demo site. This is not meant for production and might be too simple for custom development. It is a quick and easy way to get Drupal running.')
Chris@17 51 ->addArgument('install-profile', InputArgument::OPTIONAL, 'Install profile to install the site in.')
Chris@17 52 ->addOption('langcode', NULL, InputOption::VALUE_OPTIONAL, 'The language to install the site in.', 'en')
Chris@17 53 ->addOption('site-name', NULL, InputOption::VALUE_OPTIONAL, 'Set the site name.', 'Drupal')
Chris@17 54 ->addUsage('demo_umami --langcode fr')
Chris@17 55 ->addUsage('standard --site-name QuickInstall');
Chris@17 56
Chris@17 57 parent::configure();
Chris@17 58 }
Chris@17 59
Chris@17 60 /**
Chris@17 61 * {@inheritdoc}
Chris@17 62 */
Chris@17 63 protected function execute(InputInterface $input, OutputInterface $output) {
Chris@17 64 $io = new SymfonyStyle($input, $output);
Chris@17 65 if (!extension_loaded('pdo_sqlite')) {
Chris@17 66 $io->getErrorStyle()->error('You must have the pdo_sqlite PHP extension installed. See core/INSTALL.sqlite.txt for instructions.');
Chris@17 67 return 1;
Chris@17 68 }
Chris@17 69
Chris@17 70 // Change the directory to the Drupal root.
Chris@17 71 chdir(dirname(dirname(dirname(dirname(dirname(__DIR__))))));
Chris@17 72
Chris@17 73 // Check whether there is already an installation.
Chris@17 74 if ($this->isDrupalInstalled()) {
Chris@17 75 // Do not fail if the site is already installed so this command can be
Chris@17 76 // chained with ServerCommand.
Chris@17 77 $output->writeln('<info>Drupal is already installed.</info> If you want to reinstall, remove sites/default/files and sites/default/settings.php.');
Chris@17 78 return 0;
Chris@17 79 }
Chris@17 80
Chris@17 81 $install_profile = $input->getArgument('install-profile');
Chris@17 82 if ($install_profile && !$this->validateProfile($install_profile, $io)) {
Chris@17 83 return 1;
Chris@17 84 }
Chris@17 85 if (!$install_profile) {
Chris@17 86 $install_profile = $this->selectProfile($io);
Chris@17 87 }
Chris@17 88
Chris@17 89 return $this->install($this->classLoader, $io, $install_profile, $input->getOption('langcode'), $this->getSitePath(), $input->getOption('site-name'));
Chris@17 90 }
Chris@17 91
Chris@17 92 /**
Chris@17 93 * Returns whether there is already an existing Drupal installation.
Chris@17 94 *
Chris@17 95 * @return bool
Chris@17 96 */
Chris@17 97 protected function isDrupalInstalled() {
Chris@17 98 try {
Chris@17 99 $kernel = new DrupalKernel('prod', $this->classLoader, FALSE);
Chris@17 100 $kernel::bootEnvironment();
Chris@17 101 $kernel->setSitePath($this->getSitePath());
Chris@17 102 Settings::initialize($kernel->getAppRoot(), $kernel->getSitePath(), $this->classLoader);
Chris@17 103 $kernel->boot();
Chris@17 104 }
Chris@17 105 catch (ConnectionNotDefinedException $e) {
Chris@17 106 return FALSE;
Chris@17 107 }
Chris@17 108 return !empty(Database::getConnectionInfo());
Chris@17 109 }
Chris@17 110
Chris@17 111 /**
Chris@17 112 * Installs Drupal with specified installation profile.
Chris@17 113 *
Chris@17 114 * @param object $class_loader
Chris@17 115 * The class loader.
Chris@17 116 * @param \Symfony\Component\Console\Style\SymfonyStyle $io
Chris@17 117 * The Symfony output decorator.
Chris@17 118 * @param string $profile
Chris@17 119 * The installation profile to use.
Chris@17 120 * @param string $langcode
Chris@17 121 * The language to install the site in.
Chris@17 122 * @param string $site_path
Chris@17 123 * The path to install the site to, like 'sites/default'.
Chris@17 124 * @param string $site_name
Chris@17 125 * The site name.
Chris@17 126 *
Chris@17 127 * @throws \Exception
Chris@17 128 * Thrown when failing to create the $site_path directory or settings.php.
Chris@17 129 */
Chris@17 130 protected function install($class_loader, SymfonyStyle $io, $profile, $langcode, $site_path, $site_name) {
Chris@17 131 $password = Crypt::randomBytesBase64(12);
Chris@17 132 $parameters = [
Chris@17 133 'interactive' => FALSE,
Chris@17 134 'site_path' => $site_path,
Chris@17 135 'parameters' => [
Chris@17 136 'profile' => $profile,
Chris@17 137 'langcode' => $langcode,
Chris@17 138 ],
Chris@17 139 'forms' => [
Chris@17 140 'install_settings_form' => [
Chris@17 141 'driver' => 'sqlite',
Chris@17 142 'sqlite' => [
Chris@17 143 'database' => $site_path . '/files/.sqlite',
Chris@17 144 ],
Chris@17 145 ],
Chris@17 146 'install_configure_form' => [
Chris@17 147 'site_name' => $site_name,
Chris@17 148 'site_mail' => 'drupal@localhost',
Chris@17 149 'account' => [
Chris@17 150 'name' => 'admin',
Chris@17 151 'mail' => 'admin@localhost',
Chris@17 152 'pass' => [
Chris@17 153 'pass1' => $password,
Chris@17 154 'pass2' => $password,
Chris@17 155 ],
Chris@17 156 ],
Chris@17 157 'enable_update_status_module' => TRUE,
Chris@17 158 // form_type_checkboxes_value() requires NULL instead of FALSE values
Chris@17 159 // for programmatic form submissions to disable a checkbox.
Chris@17 160 'enable_update_status_emails' => NULL,
Chris@17 161 ],
Chris@17 162 ],
Chris@17 163 ];
Chris@17 164
Chris@17 165 // Create the directory and settings.php if not there so that the installer
Chris@17 166 // works.
Chris@17 167 if (!is_dir($site_path)) {
Chris@17 168 if ($io->isVerbose()) {
Chris@17 169 $io->writeln("Creating directory: $site_path");
Chris@17 170 }
Chris@17 171 if (!mkdir($site_path, 0775)) {
Chris@17 172 throw new \RuntimeException("Failed to create directory $site_path");
Chris@17 173 }
Chris@17 174 }
Chris@17 175 if (!file_exists("{$site_path}/settings.php")) {
Chris@17 176 if ($io->isVerbose()) {
Chris@17 177 $io->writeln("Creating file: {$site_path}/settings.php");
Chris@17 178 }
Chris@17 179 if (!copy('sites/default/default.settings.php', "{$site_path}/settings.php")) {
Chris@17 180 throw new \RuntimeException("Copying sites/default/default.settings.php to {$site_path}/settings.php failed.");
Chris@17 181 }
Chris@17 182 }
Chris@17 183
Chris@17 184 require_once 'core/includes/install.core.inc';
Chris@17 185
Chris@17 186 $progress_bar = $io->createProgressBar();
Chris@17 187 install_drupal($class_loader, $parameters, function ($install_state) use ($progress_bar) {
Chris@17 188 static $started = FALSE;
Chris@17 189 if (!$started) {
Chris@17 190 $started = TRUE;
Chris@17 191 // We've already done 1.
Chris@17 192 $progress_bar->setFormat("%current%/%max% [%bar%]\n%message%\n");
Chris@17 193 $progress_bar->setMessage(t('Installing @drupal', ['@drupal' => drupal_install_profile_distribution_name()]));
Chris@17 194 $tasks = install_tasks($install_state);
Chris@17 195 $progress_bar->start(count($tasks) + 1);
Chris@17 196 }
Chris@17 197 $tasks_to_perform = install_tasks_to_perform($install_state);
Chris@17 198 $task = current($tasks_to_perform);
Chris@17 199 if (isset($task['display_name'])) {
Chris@17 200 $progress_bar->setMessage($task['display_name']);
Chris@17 201 }
Chris@17 202 $progress_bar->advance();
Chris@17 203 });
Chris@17 204 $success_message = t('Congratulations, you installed @drupal!', [
Chris@17 205 '@drupal' => drupal_install_profile_distribution_name(),
Chris@17 206 '@name' => 'admin',
Chris@17 207 '@pass' => $password,
Chris@17 208 ], ['langcode' => $langcode]);
Chris@17 209 $progress_bar->setMessage('<info>' . $success_message . '</info>');
Chris@17 210 $progress_bar->display();
Chris@17 211 $progress_bar->finish();
Chris@17 212 $io->writeln('<info>Username:</info> admin');
Chris@17 213 $io->writeln("<info>Password:</info> $password");
Chris@17 214 }
Chris@17 215
Chris@17 216 /**
Chris@17 217 * Gets the site path.
Chris@17 218 *
Chris@17 219 * Defaults to 'sites/default'. For testing purposes this can be overridden
Chris@17 220 * using the DRUPAL_DEV_SITE_PATH environment variable.
Chris@17 221 *
Chris@17 222 * @return string
Chris@17 223 * The site path to use.
Chris@17 224 */
Chris@17 225 protected function getSitePath() {
Chris@17 226 return getenv('DRUPAL_DEV_SITE_PATH') ?: 'sites/default';
Chris@17 227 }
Chris@17 228
Chris@17 229 /**
Chris@17 230 * Selects the install profile to use.
Chris@17 231 *
Chris@17 232 * @param \Symfony\Component\Console\Style\SymfonyStyle $io
Chris@17 233 * Symfony style output decorator.
Chris@17 234 *
Chris@17 235 * @return string
Chris@17 236 * The selected install profile.
Chris@17 237 *
Chris@17 238 * @see _install_select_profile()
Chris@17 239 * @see \Drupal\Core\Installer\Form\SelectProfileForm
Chris@17 240 */
Chris@17 241 protected function selectProfile(SymfonyStyle $io) {
Chris@17 242 $profiles = $this->getProfiles();
Chris@17 243
Chris@17 244 // If there is a distribution there will be only one profile.
Chris@17 245 if (count($profiles) == 1) {
Chris@17 246 return key($profiles);
Chris@17 247 }
Chris@17 248 // Display alphabetically by human-readable name, but always put the core
Chris@17 249 // profiles first (if they are present in the filesystem).
Chris@17 250 natcasesort($profiles);
Chris@17 251 if (isset($profiles['minimal'])) {
Chris@17 252 // If the expert ("Minimal") core profile is present, put it in front of
Chris@17 253 // any non-core profiles rather than including it with them
Chris@17 254 // alphabetically, since the other profiles might be intended to group
Chris@17 255 // together in a particular way.
Chris@17 256 $profiles = ['minimal' => $profiles['minimal']] + $profiles;
Chris@17 257 }
Chris@17 258 if (isset($profiles['standard'])) {
Chris@17 259 // If the default ("Standard") core profile is present, put it at the very
Chris@17 260 // top of the list. This profile will have its radio button pre-selected,
Chris@17 261 // so we want it to always appear at the top.
Chris@17 262 $profiles = ['standard' => $profiles['standard']] + $profiles;
Chris@17 263 }
Chris@17 264 reset($profiles);
Chris@17 265 return $io->choice('Select an installation profile', $profiles, current($profiles));
Chris@17 266 }
Chris@17 267
Chris@17 268 /**
Chris@17 269 * Validates a user provided install profile.
Chris@17 270 *
Chris@17 271 * @param string $install_profile
Chris@17 272 * Install profile to validate.
Chris@17 273 * @param \Symfony\Component\Console\Style\SymfonyStyle $io
Chris@17 274 * Symfony style output decorator.
Chris@17 275 *
Chris@17 276 * @return bool
Chris@17 277 * TRUE if the profile is valid, FALSE if not.
Chris@17 278 */
Chris@17 279 protected function validateProfile($install_profile, SymfonyStyle $io) {
Chris@17 280 // Allow people to install hidden and non-distribution profiles if they
Chris@17 281 // supply the argument.
Chris@17 282 $profiles = $this->getProfiles(TRUE, FALSE);
Chris@17 283 if (!isset($profiles[$install_profile])) {
Chris@17 284 $error_msg = sprintf("'%s' is not a valid install profile.", $install_profile);
Chris@17 285 $alternatives = [];
Chris@17 286 foreach (array_keys($profiles) as $profile_name) {
Chris@17 287 $lev = levenshtein($install_profile, $profile_name);
Chris@17 288 if ($lev <= strlen($profile_name) / 4 || FALSE !== strpos($profile_name, $install_profile)) {
Chris@17 289 $alternatives[] = $profile_name;
Chris@17 290 }
Chris@17 291 }
Chris@17 292 if (!empty($alternatives)) {
Chris@17 293 $error_msg .= sprintf(" Did you mean '%s'?", implode("' or '", $alternatives));
Chris@17 294 }
Chris@17 295 $io->getErrorStyle()->error($error_msg);
Chris@17 296 return FALSE;
Chris@17 297 }
Chris@17 298 return TRUE;
Chris@17 299 }
Chris@17 300
Chris@17 301 /**
Chris@17 302 * Gets a list of profiles.
Chris@17 303 *
Chris@17 304 * @param bool $include_hidden
Chris@17 305 * (optional) Whether to include hidden profiles. Defaults to FALSE.
Chris@17 306 * @param bool $auto_select_distributions
Chris@17 307 * (optional) Whether to only return the first distribution found.
Chris@17 308 *
Chris@17 309 * @return string[]
Chris@17 310 * An array of profile descriptions keyed by the profile machine name.
Chris@17 311 */
Chris@17 312 protected function getProfiles($include_hidden = FALSE, $auto_select_distributions = TRUE) {
Chris@17 313 // Build a list of all available profiles.
Chris@17 314 $listing = new ExtensionDiscovery(getcwd(), FALSE);
Chris@17 315 $listing->setProfileDirectories([]);
Chris@17 316 $profiles = [];
Chris@17 317 $info_parser = new InfoParserDynamic();
Chris@17 318 foreach ($listing->scan('profile') as $profile) {
Chris@17 319 $details = $info_parser->parse($profile->getPathname());
Chris@17 320 // Don't show hidden profiles.
Chris@17 321 if (!$include_hidden && !empty($details['hidden'])) {
Chris@17 322 continue;
Chris@17 323 }
Chris@17 324 // Determine the name of the profile; default to the internal name if none
Chris@17 325 // is specified.
Chris@17 326 $name = isset($details['name']) ? $details['name'] : $profile->getName();
Chris@17 327 $description = isset($details['description']) ? $details['description'] : $name;
Chris@17 328 $profiles[$profile->getName()] = $description;
Chris@17 329
Chris@17 330 if ($auto_select_distributions && !empty($details['distribution'])) {
Chris@17 331 return [$profile->getName() => $description];
Chris@17 332 }
Chris@17 333 }
Chris@17 334 return $profiles;
Chris@17 335 }
Chris@17 336
Chris@17 337 }