Mercurial > hg > isophonics-drupal-site
comparison core/lib/Drupal/Core/Command/InstallCommand.php @ 17:129ea1e6d783
Update, including to Drupal core 8.6.10
author | Chris Cannam |
---|---|
date | Thu, 28 Feb 2019 13:21:36 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
16:c2387f117808 | 17:129ea1e6d783 |
---|---|
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 } |