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 }
|