Pull merge.
[yaffs-website] / web / core / lib / Drupal / Core / Command / InstallCommand.php
1 <?php
2
3 namespace Drupal\Core\Command;
4
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Core\Database\ConnectionNotDefinedException;
7 use Drupal\Core\Database\Database;
8 use Drupal\Core\DrupalKernel;
9 use Drupal\Core\Extension\ExtensionDiscovery;
10 use Drupal\Core\Extension\InfoParserDynamic;
11 use Drupal\Core\Site\Settings;
12 use Symfony\Component\Console\Command\Command;
13 use Symfony\Component\Console\Input\InputArgument;
14 use Symfony\Component\Console\Input\InputInterface;
15 use Symfony\Component\Console\Input\InputOption;
16 use Symfony\Component\Console\Output\OutputInterface;
17 use 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  */
25 class 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(dirname(dirname(dirname(dirname(__DIR__))))));
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   protected function install($class_loader, SymfonyStyle $io, $profile, $langcode, $site_path, $site_name) {
131     $password = Crypt::randomBytesBase64(12);
132     $parameters = [
133       'interactive' => FALSE,
134       'site_path' => $site_path,
135       'parameters' => [
136         'profile' => $profile,
137         'langcode' => $langcode,
138       ],
139       'forms' => [
140         'install_settings_form' => [
141           'driver' => 'sqlite',
142           'sqlite' => [
143             'database' => $site_path . '/files/.sqlite',
144           ],
145         ],
146         'install_configure_form' => [
147           'site_name' => $site_name,
148           'site_mail' => 'drupal@localhost',
149           'account' => [
150             'name' => 'admin',
151             'mail' => 'admin@localhost',
152             'pass' => [
153               'pass1' => $password,
154               'pass2' => $password,
155             ],
156           ],
157           'enable_update_status_module' => TRUE,
158           // form_type_checkboxes_value() requires NULL instead of FALSE values
159           // for programmatic form submissions to disable a checkbox.
160           'enable_update_status_emails' => NULL,
161         ],
162       ],
163     ];
164
165     // Create the directory and settings.php if not there so that the installer
166     // works.
167     if (!is_dir($site_path)) {
168       if ($io->isVerbose()) {
169         $io->writeln("Creating directory: $site_path");
170       }
171       if (!mkdir($site_path, 0775)) {
172         throw new \RuntimeException("Failed to create directory $site_path");
173       }
174     }
175     if (!file_exists("{$site_path}/settings.php")) {
176       if ($io->isVerbose()) {
177         $io->writeln("Creating file: {$site_path}/settings.php");
178       }
179       if (!copy('sites/default/default.settings.php', "{$site_path}/settings.php")) {
180         throw new \RuntimeException("Copying sites/default/default.settings.php to {$site_path}/settings.php failed.");
181       }
182     }
183
184     require_once 'core/includes/install.core.inc';
185
186     $progress_bar = $io->createProgressBar();
187     install_drupal($class_loader, $parameters, function ($install_state) use ($progress_bar) {
188       static $started = FALSE;
189       if (!$started) {
190         $started = TRUE;
191         // We've already done 1.
192         $progress_bar->setFormat("%current%/%max% [%bar%]\n%message%\n");
193         $progress_bar->setMessage(t('Installing @drupal', ['@drupal' => drupal_install_profile_distribution_name()]));
194         $tasks = install_tasks($install_state);
195         $progress_bar->start(count($tasks) + 1);
196       }
197       $tasks_to_perform = install_tasks_to_perform($install_state);
198       $task = current($tasks_to_perform);
199       if (isset($task['display_name'])) {
200         $progress_bar->setMessage($task['display_name']);
201       }
202       $progress_bar->advance();
203     });
204     $success_message = t('Congratulations, you installed @drupal!', [
205       '@drupal' => drupal_install_profile_distribution_name(),
206       '@name' => 'admin',
207       '@pass' => $password,
208     ], ['langcode' => $langcode]);
209     $progress_bar->setMessage('<info>' . $success_message . '</info>');
210     $progress_bar->display();
211     $progress_bar->finish();
212     $io->writeln('<info>Username:</info> admin');
213     $io->writeln("<info>Password:</info> $password");
214   }
215
216   /**
217    * Gets the site path.
218    *
219    * Defaults to 'sites/default'. For testing purposes this can be overridden
220    * using the DRUPAL_DEV_SITE_PATH environment variable.
221    *
222    * @return string
223    *   The site path to use.
224    */
225   protected function getSitePath() {
226     return getenv('DRUPAL_DEV_SITE_PATH') ?: 'sites/default';
227   }
228
229   /**
230    * Selects the install profile to use.
231    *
232    * @param \Symfony\Component\Console\Style\SymfonyStyle $io
233    *   Symfony style output decorator.
234    *
235    * @return string
236    *   The selected install profile.
237    *
238    * @see _install_select_profile()
239    * @see \Drupal\Core\Installer\Form\SelectProfileForm
240    */
241   protected function selectProfile(SymfonyStyle $io) {
242     $profiles = $this->getProfiles();
243
244     // If there is a distribution there will be only one profile.
245     if (count($profiles) == 1) {
246       return key($profiles);
247     }
248     // Display alphabetically by human-readable name, but always put the core
249     // profiles first (if they are present in the filesystem).
250     natcasesort($profiles);
251     if (isset($profiles['minimal'])) {
252       // If the expert ("Minimal") core profile is present, put it in front of
253       // any non-core profiles rather than including it with them
254       // alphabetically, since the other profiles might be intended to group
255       // together in a particular way.
256       $profiles = ['minimal' => $profiles['minimal']] + $profiles;
257     }
258     if (isset($profiles['standard'])) {
259       // If the default ("Standard") core profile is present, put it at the very
260       // top of the list. This profile will have its radio button pre-selected,
261       // so we want it to always appear at the top.
262       $profiles = ['standard' => $profiles['standard']] + $profiles;
263     }
264     reset($profiles);
265     return $io->choice('Select an installation profile', $profiles, current($profiles));
266   }
267
268   /**
269    * Validates a user provided install profile.
270    *
271    * @param string $install_profile
272    *   Install profile to validate.
273    * @param \Symfony\Component\Console\Style\SymfonyStyle $io
274    *   Symfony style output decorator.
275    *
276    * @return bool
277    *   TRUE if the profile is valid, FALSE if not.
278    */
279   protected function validateProfile($install_profile, SymfonyStyle $io) {
280     // Allow people to install hidden and non-distribution profiles if they
281     // supply the argument.
282     $profiles = $this->getProfiles(TRUE, FALSE);
283     if (!isset($profiles[$install_profile])) {
284       $error_msg = sprintf("'%s' is not a valid install profile.", $install_profile);
285       $alternatives = [];
286       foreach (array_keys($profiles) as $profile_name) {
287         $lev = levenshtein($install_profile, $profile_name);
288         if ($lev <= strlen($profile_name) / 4 || FALSE !== strpos($profile_name, $install_profile)) {
289           $alternatives[] = $profile_name;
290         }
291       }
292       if (!empty($alternatives)) {
293         $error_msg .= sprintf(" Did you mean '%s'?", implode("' or '", $alternatives));
294       }
295       $io->getErrorStyle()->error($error_msg);
296       return FALSE;
297     }
298     return TRUE;
299   }
300
301   /**
302    * Gets a list of profiles.
303    *
304    * @param bool $include_hidden
305    *   (optional) Whether to include hidden profiles. Defaults to FALSE.
306    * @param bool $auto_select_distributions
307    *   (optional) Whether to only return the first distribution found.
308    *
309    * @return string[]
310    *   An array of profile descriptions keyed by the profile machine name.
311    */
312   protected function getProfiles($include_hidden = FALSE, $auto_select_distributions = TRUE) {
313     // Build a list of all available profiles.
314     $listing = new ExtensionDiscovery(getcwd(), FALSE);
315     $listing->setProfileDirectories([]);
316     $profiles = [];
317     $info_parser = new InfoParserDynamic();
318     foreach ($listing->scan('profile') as $profile) {
319       $details = $info_parser->parse($profile->getPathname());
320       // Don't show hidden profiles.
321       if (!$include_hidden && !empty($details['hidden'])) {
322         continue;
323       }
324       // Determine the name of the profile; default to the internal name if none
325       // is specified.
326       $name = isset($details['name']) ? $details['name'] : $profile->getName();
327       $description = isset($details['description']) ? $details['description'] : $name;
328       $profiles[$profile->getName()] = $description;
329
330       if ($auto_select_distributions && !empty($details['distribution'])) {
331         return [$profile->getName() => $description];
332       }
333     }
334     return $profiles;
335   }
336
337 }