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