Further Drupal 8.6.4 changes. Some core files were not committed before a commit...
[yaffs-website] / vendor / drush / drush / src / Commands / core / SiteInstallCommands.php
1 <?php
2 namespace Drush\Commands\core;
3
4 use Composer\Semver\Comparator;
5 use Consolidation\AnnotatedCommand\CommandData;
6 use Drupal\Component\FileCache\FileCacheFactory;
7 use Drupal\Core\Database\ConnectionNotDefinedException;
8 use Drush\Commands\DrushCommands;
9 use Drush\Drush;
10 use Drush\Exceptions\UserAbortException;
11 use Drupal\Core\Config\FileStorage;
12 use Consolidation\SiteAlias\SiteAliasManagerAwareInterface;
13 use Consolidation\SiteAlias\SiteAliasManagerAwareTrait;
14 use Drush\Sql\SqlBase;
15 use Drush\Utils\StringUtils;
16 use Webmozart\PathUtil\Path;
17
18 class SiteInstallCommands extends DrushCommands implements SiteAliasManagerAwareInterface
19 {
20     use SiteAliasManagerAwareTrait;
21
22     /**
23      * Install Drupal along with modules/themes/configuration/profile.
24      *
25      * @command site:install
26      * @param $profile An install profile name. Defaults to 'standard' unless an install profile is marked as a distribution. Additional info for the install profile may also be provided with additional arguments. The key is in the form [form name].[parameter name]
27      * @option db-url A Drupal 6 style database URL. Required for initial install, not re-install. If omitted and required, Drush prompts for this item.
28      * @option db-prefix An optional table prefix to use for initial install.
29      * @option db-su Account to use when creating a new database. Must have Grant permission (mysql only). Optional.
30      * @option db-su-pw Password for the "db-su" account. Optional.
31      * @option account-name uid1 name. Defaults to admin
32      * @option account-pass uid1 pass. Defaults to a randomly generated password. If desired, set a fixed password in config.yml.
33      * @option account-mail uid1 email. Defaults to admin@example.com
34      * @option locale A short language code. Sets the default site language. Language files must already be present.
35      * @option site-name Defaults to Site-Install
36      * @option site-mail From: for system mailings. Defaults to admin@example.com
37      * @option sites-subdir Name of directory under 'sites' which should be created.
38      * @option config-dir Deprecated - only use with Drupal 8.5-. A path pointing to a full set of configuration which should be installed during installation.
39      * @option existing-config Configuration from "sync" directory should be imported during installation. Use with Drupal 8.6+.
40      * @usage drush si expert --locale=uk
41      *   (Re)install using the expert install profile. Set default language to Ukrainian.
42      * @usage drush si --db-url=mysql://root:pass@localhost:port/dbname
43      *   Install using the specified DB params.
44      * @usage drush si --db-url=sqlite://sites/example.com/files/.ht.sqlite
45      *   Install using SQLite
46      * @usage drush si --account-pass=mom
47      *   Re-install with specified uid1 password.
48      * @usage drush si --existing-config
49      *   Install based on the yml files stored in the config export/import directory.
50      * @usage drush si standard install_configure_form.enable_update_status_emails=NULL
51      *   Disable email notification during install and later. If your server has no mail transfer agent, this gets rid of an error during install.
52      * @bootstrap root
53      * @kernel installer
54      * @aliases si,sin,site-install
55      *
56      */
57     public function install(array $profile, $options = ['db-url' => self::REQ, 'db-prefix' => self::REQ, 'db-su' => self::REQ, 'db-su-pw' => self::REQ, 'account-name' => 'admin', 'account-mail' => 'admin@example.com', 'site-mail' => 'admin@example.com', 'account-pass' => self::REQ, 'locale' => 'en', 'site-name' => 'Drush Site-Install', 'site-pass' => self::REQ, 'sites-subdir' => self::REQ, 'config-dir' => self::REQ, 'existing-config' => false])
58     {
59         $additional = $profile;
60         $profile = array_shift($additional) ?: '';
61         $form_options = [];
62         foreach ((array)$additional as $arg) {
63             list($key, $value) = explode('=', $arg, 2);
64
65             // Allow for numeric and NULL values to be passed in.
66             if (is_numeric($value)) {
67                 $value = intval($value);
68             } elseif ($value == 'NULL') {
69                 $value = null;
70             }
71
72             $form_options[$key] = $value;
73         }
74
75         $this->serverGlobals(Drush::bootstrapManager()->getUri());
76         $class_loader = Drush::service('loader');
77         $profile = $this->determineProfile($profile, $options, $class_loader);
78
79         $sql = SqlBase::create($options);
80         $db_spec = $sql->getDbSpec();
81
82         $account_pass = $options['account-pass'] ?: StringUtils::generatePassword();
83
84         // Was giving error during validate() so its here for now.
85         if ($options['existing-config']) {
86             $existing_config_dir = config_get_config_directory(CONFIG_SYNC_DIRECTORY);
87             if (!is_dir($existing_config_dir)) {
88                 throw new \Exception(dt('Existing config directory @dir not found', ['@dir' => $existing_config_dir]));
89             }
90             $this->logger()->info(dt('Installing from existing config at @dir', ['@dir' => $existing_config_dir]));
91         }
92
93         $settings = [
94             'parameters' => [
95                 'profile' => $profile,
96                 'langcode' => $options['locale'],
97                 'existing_config' => $options['existing-config'],
98             ],
99             'forms' => [
100                 'install_settings_form' => [
101                     'driver' => $db_spec['driver'],
102                     $db_spec['driver'] => $db_spec,
103                     'op' => dt('Save and continue'),
104                 ],
105                 'install_configure_form' => [
106                     'site_name' => $options['site-name'],
107                     'site_mail' => $options['site-mail'],
108                     'account' => [
109                       'name' => $options['account-name'],
110                       'mail' => $options['account-mail'],
111                       'pass' => [
112                         'pass1' => $account_pass,
113                         'pass2' => $account_pass,
114                       ],
115                     ],
116                     'enable_update_status_module' => true,
117                     'enable_update_status_emails' => true,
118                     'clean_url' => true,
119                     'op' => dt('Save and continue'),
120                 ],
121             ],
122             'config_install_path' => $options['config-dir'],
123         ];
124
125         // Merge in the additional options.
126         foreach ($form_options as $key => $value) {
127             $current = &$settings['forms'];
128             foreach (explode('.', $key) as $param) {
129                 $current = &$current[$param];
130             }
131             $current = $value;
132         }
133
134         $msg = 'Starting Drupal installation. This takes a while.';
135         $this->logger()->notice(dt($msg));
136
137         // Define some functions which alter away the install_finished task.
138         require_once Path::join(DRUSH_BASE_PATH, 'includes/site_install.inc');
139
140         require_once DRUSH_DRUPAL_CORE . '/includes/install.core.inc';
141         drush_op('install_drupal', $class_loader, $settings);
142         if (empty($options['account-pass'])) {
143             $this->logger()->success(dt('Installation complete.  User name: @name  User password: @pass', ['@name' => $options['account-name'], '@pass' => $account_pass]));
144         } else {
145             $this->logger()->success(dt('Installation complete.'));
146         }
147     }
148
149     protected function determineProfile($profile, $options, $class_loader)
150     {
151         // --config-dir fails with Standard profile and any other one that carries content entities.
152         // Force to minimal install profile only for drupal < 8.6.
153         if ($options['config-dir'] && Comparator::lessThan(self::getVersion(), '8.6')) {
154             $this->logger()->info(dt("Using 'minimal' install profile since --config-dir option was provided."));
155             $profile = 'minimal';
156         }
157
158         // Try to get profile from existing config if not provided as an argument.
159         // @todo Arguably Drupal core [$boot->getKernel()->getInstallProfile()] could do this - https://github.com/drupal/drupal/blob/8.6.x/core/lib/Drupal/Core/DrupalKernel.php#L1606 reads from DB storage but not file storage.
160         if (empty($profile) && $options['existing-config']) {
161             FileCacheFactory::setConfiguration([FileCacheFactory::DISABLE_CACHE => true]);
162             $source_storage = new FileStorage(config_get_config_directory(CONFIG_SYNC_DIRECTORY));
163             if (!$source_storage->exists('core.extension')) {
164                 throw new \Exception('Existing configuration directory not found or does not contain a core.extension.yml file.".');
165             }
166             $config = $source_storage->read('core.extension');
167             $profile = $config['profile'];
168         }
169
170         if (empty($profile)) {
171             $boot = Drush::bootstrap();
172             $profile = $boot->getKernel()->getInstallProfile();
173         }
174
175         if (empty($profile)) {
176             // If there is an installation profile that acts as a distribution, use it.
177             // You can turn your installation profile into a distribution by providing a
178             // @code
179             //   distribution:
180             //     name: 'Distribution name'
181             // @endcode
182             // block in the profile's info YAML file.
183             // See https://www.drupal.org/node/2210443 for more information.
184             require_once DRUSH_DRUPAL_CORE . '/includes/install.core.inc';
185             $install_state = ['interactive' => false] + install_state_defaults();
186             try {
187                 install_begin_request($class_loader, $install_state);
188                 $profile = _install_select_profile($install_state);
189             } catch (\Exception $e) {
190                 // This is only a best effort to provide a better default, no harm done
191                 // if it fails.
192             }
193         }
194
195         // Drupal currently requires that non-interactive installs provide a profile.
196         if (empty($profile)) {
197             $profile = 'standard';
198         }
199         return $profile;
200     }
201
202     /**
203      * Post installation, run the configuration import.
204      *
205      * @hook post-command site-install
206      */
207     public function post($result, CommandData $commandData)
208     {
209         if ($config = $commandData->input()->getOption('config-dir') && Comparator::lessThan(self::getVersion(), '8.6')) {
210             // Set the destination site UUID to match the source UUID, to bypass a core fail-safe.
211             $source_storage = new FileStorage($config);
212             $options = ['yes' => true];
213             drush_invoke_process('@self', 'config-set', ['system.site', 'uuid', $source_storage->read('system.site')['uuid']], $options);
214             // Run a full configuration import.
215             drush_invoke_process('@self', 'config-import', [], ['source' => $config] + $options);
216         }
217     }
218
219     /**
220      * Check to see if there are any .yml files in the provided config directory.
221      */
222     protected function hasConfigFiles($config)
223     {
224         $files = glob("$config/*.yml");
225         return !empty($files);
226     }
227
228     /**
229      * @hook validate site-install
230      */
231     public function validate(CommandData $commandData)
232     {
233         if ($sites_subdir = $commandData->input()->getOption('sites-subdir')) {
234             $lower = strtolower($sites_subdir);
235             if ($sites_subdir != $lower) {
236                 $this->logger()->warning(dt('Only lowercase sites-subdir are valid. Switching to !lower.', ['!lower' => $lower]));
237                 $commandData->input()->setOption('sites-subdir', $lower);
238             }
239             // Make sure that we will bootstrap to the 'sites-subdir' site.
240             $bootstrapManager = Drush::bootstrapManager();
241             $bootstrapManager->setUri('http://' . $sites_subdir);
242         }
243
244         if ($config = $commandData->input()->getOption('config-dir')) {
245             $this->validateConfigDir($commandData, $config);
246         }
247
248         try {
249             // Get AnnotationData. @todo Find a better way.
250             $annotationData = Drush::getApplication()->find('site:install')->getAnnotationData();
251             Drush::bootstrapManager()->bootstrapMax(DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION, $annotationData);
252             $sql = SqlBase::create($commandData->input()->getOptions());
253         } catch (\Exception $e) {
254             // Ask questions to get our data.
255             // TODO: we should only 'ask' in hook interact, never in hook validate
256             if ($commandData->input()->getOption('db-url') == '') {
257                 // Prompt for the db-url data if it was not provided via --db-url.
258                 $database = $this->io()->ask('Database name', 'drupal');
259                 $driver = $this->io()->ask('Database driver', 'mysql');
260                 $username = $this->io()->ask('Database username', 'drupal');
261                 $password = $this->io()->ask('Database password', 'drupal');
262                 $host = $this->io()->ask('Database host', '127.0.0.1');
263                 $port = $this->io()->ask('Database port', '3306');
264                 $db_url = "$driver://$username:$password@$host:$port/$database";
265                 $commandData->input()->setOption('db-url', $db_url);
266
267                 try {
268                     $sql = SqlBase::create($commandData->input()->getOptions());
269                 } catch (\Exception $e) {
270                     throw new \Exception(dt('Could not determine database connection parameters. Pass --db-url option.'));
271                 }
272             }
273         }
274     }
275
276     /**
277      * Perform setup tasks before installation.
278      *
279      * @hook pre-command site-install
280      *
281      */
282     public function pre(CommandData $commandData)
283     {
284         $sql = SqlBase::create($commandData->input()->getOptions());
285         $db_spec = $sql->getDbSpec();
286
287         $aliasRecord = $this->siteAliasManager()->getSelf();
288         $root = $aliasRecord->root();
289
290         $dir = $commandData->input()->getOption('sites-subdir');
291         if (!$dir) {
292             // We will allow the 'uri' from the site alias to provide
293             // a fallback name when '--sites-subdir' is not specified, but
294             // only if the uri and the folder name match, and only if
295             // the sites directory has already been created.
296             $dir = $this->getSitesSubdirFromUri($root, $aliasRecord->get('uri'));
297         }
298
299         if (!$dir) {
300             throw new \Exception(dt('Could not determine target sites directory for site to install. Use --sites-subdir to specify.'));
301         }
302
303         $sites_subdir = Path::join('sites', $dir);
304         $confPath = $sites_subdir;
305         $settingsfile = Path::join($confPath, 'settings.php');
306         $sitesfile = "sites/sites.php";
307         $default = realpath(Path::join($root, 'sites/default'));
308         $sitesfile_write = realpath($confPath) != $default && !file_exists($sitesfile);
309
310         if (!file_exists($settingsfile)) {
311             $msg[] = dt('create a @settingsfile file', ['@settingsfile' => $settingsfile]);
312         }
313         if ($sitesfile_write) {
314             $msg[] = dt('create a @sitesfile file', ['@sitesfile' => $sitesfile]);
315         }
316         if ($sql->dbExists()) {
317             $msg[] = dt("DROP all tables in your '@db' database.", ['@db' => $db_spec['database']]);
318         } else {
319             $msg[] = dt("CREATE the '@db' database.", ['@db' => $db_spec['database']]);
320         }
321
322         if (!$this->io()->confirm(dt('You are about to ') . implode(dt(' and '), $msg) . ' Do you want to continue?')) {
323             throw new UserAbortException();
324         }
325
326         // Can't install without sites subdirectory and settings.php.
327         if (!file_exists($confPath)) {
328             if (!drush_mkdir($confPath) && !Drush::simulate()) {
329                 throw new \Exception(dt('Failed to create directory @confPath', ['@confPath' => $confPath]));
330             }
331         } else {
332             $this->logger()->info(dt('Sites directory @subdir already exists - proceeding.', ['@subdir' => $confPath]));
333         }
334
335         if (!drush_file_not_empty($settingsfile)) {
336             if (!drush_op('copy', 'sites/default/default.settings.php', $settingsfile) && !Drush::simulate()) {
337                 throw new \Exception(dt('Failed to copy sites/default/default.settings.php to @settingsfile', ['@settingsfile' => $settingsfile]));
338             }
339         }
340
341         // Write an empty sites.php if we using multi-site.
342         if ($sitesfile_write) {
343             if (!drush_op('copy', 'sites/example.sites.php', $sitesfile) && !Drush::simulate()) {
344                 throw new \Exception(dt('Failed to copy sites/example.sites.php to @sitesfile', ['@sitesfile' => $sitesfile]));
345             }
346         }
347
348         // We need to be at least at DRUSH_BOOTSTRAP_DRUPAL_SITE to select the site uri to install to
349         define('MAINTENANCE_MODE', 'install');
350         $bootstrapManager = Drush::bootstrapManager();
351         $bootstrapManager->doBootstrap(DRUSH_BOOTSTRAP_DRUPAL_SITE);
352
353         if (!$sql->dropOrCreate()) {
354             throw new \Exception(dt('Failed to create database: @error', ['@error' => implode(drush_shell_exec_output())]));
355         }
356     }
357
358     /**
359      * Determine an appropriate site subdir name to use for the
360      * provided uri.
361      */
362     protected function getSitesSubdirFromUri($root, $uri)
363     {
364         $dir = strtolower($uri);
365         // Always accept simple uris (e.g. 'dev', 'stage', etc.)
366         if (preg_match('#^[a-z0-9_-]*$#', $dir)) {
367             return $dir;
368         }
369         // Strip off the protocol from the provided uri -- however,
370         // now we will require that the sites subdir already exist.
371         $dir = preg_replace('#[^/]*/*#', '', $dir);
372         if ($dir && file_exists(Path::join($root, $dir))) {
373             return $dir;
374         }
375         // Find the dir from sites.php file
376         $sites_file = $root . '/sites/sites.php';
377         if (file_exists($sites_file)) {
378             include $sites_file;
379             /** @var array $sites */
380             if (array_key_exists($uri, $sites)) {
381                 return $sites[$uri];
382             }
383         }
384         // Fall back to default directory if it exists.
385         if (file_exists(Path::join($root, 'sites', 'default'))) {
386             return 'default';
387         }
388         return false;
389     }
390
391     public static function getVersion()
392     {
393         $drupal_root = Drush::bootstrapManager()->getRoot();
394         return Drush::bootstrap()->getVersion($drupal_root);
395     }
396
397     /**
398      * Fake the necessary HTTP headers that the Drupal installer still needs:
399      * @see https://github.com/drupal/drupal/blob/d260101f1ea8a6970df88d2f1899248985c499fc/core/includes/install.core.inc#L287
400      */
401     public function serverGlobals($drupal_base_url)
402     {
403         $drupal_base_url = parse_url($drupal_base_url);
404
405         // Fill in defaults.
406         $drupal_base_url += [
407             'scheme' => null,
408             'path' => '',
409             'host' => null,
410             'port' => null,
411         ];
412         $_SERVER['HTTP_HOST'] = $drupal_base_url['host'];
413
414         if ($drupal_base_url['scheme'] == 'https') {
415               $_SERVER['HTTPS'] = 'on';
416         }
417
418         if ($drupal_base_url['port']) {
419               $_SERVER['HTTP_HOST'] .= ':' . $drupal_base_url['port'];
420         }
421         $_SERVER['SERVER_PORT'] = $drupal_base_url['port'];
422
423         $_SERVER['REQUEST_URI'] = $drupal_base_url['path'] . '/';
424
425         $_SERVER['PHP_SELF'] = $_SERVER['REQUEST_URI'] . 'index.php';
426         $_SERVER['SCRIPT_NAME'] = $_SERVER['PHP_SELF'];
427         $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
428         $_SERVER['REQUEST_METHOD']  = 'GET';
429
430         $_SERVER['SERVER_SOFTWARE'] = null;
431         $_SERVER['HTTP_USER_AGENT'] = null;
432         $_SERVER['SCRIPT_FILENAME'] = DRUPAL_ROOT . '/index.php';
433     }
434
435     /**
436      * Assure that a config directory exists and is populated.
437      *
438      * @param CommandData $commandData
439      * @param $directory
440      * @throws \Exception
441      */
442     protected function validateConfigDir(CommandData $commandData, $directory)
443     {
444         if (!file_exists($directory)) {
445             throw new \Exception(dt('The config source directory @config does not exist.', ['@config' => $directory]));
446         }
447         if (!is_dir($directory)) {
448             throw new \Exception(dt('The config source @config is not a directory.', ['@config' => $directory]));
449         }
450         // Skip config import with a warning if specified config dir is empty.
451         if (!$this->hasConfigFiles($directory)) {
452             $this->logger()->warning(dt('Configuration import directory @config does not contain any configuration; will skip import.', ['@config' => $directory]));
453             $commandData->input()->setOption('config-dir', '');
454         }
455     }
456 }