6a6075f7d5c8c8c903f337459b75be2130158104
[yaffs-website] / web / core / includes / install.core.inc
1 <?php
2
3 /**
4  * @file
5  * API functions for installing Drupal.
6  */
7
8 use Drupal\Component\Utility\UrlHelper;
9 use Drupal\Core\DrupalKernel;
10 use Drupal\Core\Database\Database;
11 use Drupal\Core\Database\DatabaseExceptionWrapper;
12 use Drupal\Core\Form\FormState;
13 use Drupal\Core\Installer\Exception\AlreadyInstalledException;
14 use Drupal\Core\Installer\Exception\InstallerException;
15 use Drupal\Core\Installer\Exception\InstallProfileMismatchException;
16 use Drupal\Core\Installer\Exception\NoProfilesException;
17 use Drupal\Core\Installer\InstallerKernel;
18 use Drupal\Core\Language\Language;
19 use Drupal\Core\Language\LanguageManager;
20 use Drupal\Core\Logger\LoggerChannelFactory;
21 use Drupal\Core\Site\Settings;
22 use Drupal\Core\StringTranslation\Translator\FileTranslation;
23 use Drupal\Core\StackMiddleware\ReverseProxyMiddleware;
24 use Drupal\Core\StreamWrapper\PublicStream;
25 use Drupal\Core\Extension\ExtensionDiscovery;
26 use Drupal\Core\DependencyInjection\ContainerBuilder;
27 use Drupal\Core\Url;
28 use Drupal\language\Entity\ConfigurableLanguage;
29 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
30 use Symfony\Component\DependencyInjection\Reference;
31 use Symfony\Component\HttpFoundation\Request;
32 use Symfony\Component\HttpFoundation\Response;
33 use Symfony\Component\Routing\Route;
34 use Drupal\user\Entity\User;
35 use GuzzleHttp\Exception\RequestException;
36
37 /**
38  * Do not run the task during the current installation request.
39  *
40  * This can be used to skip running an installation task when certain
41  * conditions are met, even though the task may still show on the list of
42  * installation tasks presented to the user. For example, the Drupal installer
43  * uses this flag to skip over the database configuration form when valid
44  * database connection information is already available from settings.php. It
45  * also uses this flag to skip language import tasks when the installation is
46  * being performed in English.
47  */
48 const INSTALL_TASK_SKIP = 1;
49
50 /**
51  * Run the task on each installation request that reaches it.
52  *
53  * This is primarily used by the Drupal installer for bootstrap-related tasks.
54  */
55 const INSTALL_TASK_RUN_IF_REACHED = 2;
56
57 /**
58  * Run the task on each installation request until the database is set up.
59  *
60  * This is the default method for running tasks and should be used for most
61  * tasks that occur after the database is set up; these tasks will then run
62  * once and be marked complete once they are successfully finished. For
63  * example, the Drupal installer uses this flag for the batch installation of
64  * modules on the new site, and also for the configuration form that collects
65  * basic site information and sets up the site maintenance account.
66  */
67 const INSTALL_TASK_RUN_IF_NOT_COMPLETED = 3;
68
69 /**
70  * Installs Drupal either interactively or via an array of passed-in settings.
71  *
72  * The Drupal installation happens in a series of steps, which may be spread
73  * out over multiple page requests. Each request begins by trying to determine
74  * the last completed installation step (also known as a "task"), if one is
75  * available from a previous request. Control is then passed to the task
76  * handler, which processes the remaining tasks that need to be run until (a)
77  * an error is thrown, (b) a new page needs to be displayed, or (c) the
78  * installation finishes (whichever happens first).
79  *
80  * @param $class_loader
81  *   The class loader. Normally Composer's ClassLoader, as included by the
82  *   front controller, but may also be decorated; e.g.,
83  *   \Symfony\Component\ClassLoader\ApcClassLoader.
84  * @param $settings
85  *   An optional array of installation settings. Leave this empty for a normal,
86  *   interactive, browser-based installation intended to occur over multiple
87  *   page requests. Alternatively, if an array of settings is passed in, the
88  *   installer will attempt to use it to perform the installation in a single
89  *   page request (optimized for the command line) and not send any output
90  *   intended for the web browser. See install_state_defaults() for a list of
91  *   elements that are allowed to appear in this array.
92  *
93  * @see install_state_defaults()
94  */
95 function install_drupal($class_loader, $settings = []) {
96   // Support the old way of calling this function with just a settings array.
97   // @todo Remove this when Drush is updated in the Drupal testing
98   //   infrastructure in https://www.drupal.org/node/2389243
99   if (is_array($class_loader) && $settings === []) {
100     $settings = $class_loader;
101     $class_loader = require __DIR__ . '/../../autoload.php';
102   }
103
104   global $install_state;
105   // Initialize the installation state with the settings that were passed in,
106   // as well as a boolean indicating whether or not this is an interactive
107   // installation.
108   $interactive = empty($settings);
109   $install_state = $settings + ['interactive' => $interactive] + install_state_defaults();
110
111   try {
112     // Begin the page request. This adds information about the current state of
113     // the Drupal installation to the passed-in array.
114     install_begin_request($class_loader, $install_state);
115     // Based on the installation state, run the remaining tasks for this page
116     // request, and collect any output.
117     $output = install_run_tasks($install_state);
118   }
119   catch (InstallerException $e) {
120     // In the non-interactive installer, exceptions are always thrown directly.
121     if (!$install_state['interactive']) {
122       throw $e;
123     }
124     $output = [
125       '#title' => $e->getTitle(),
126       '#markup' => $e->getMessage(),
127     ];
128   }
129
130   // After execution, all tasks might be complete, in which case
131   // $install_state['installation_finished'] is TRUE. In case the last task
132   // has been processed, remove the global $install_state, so other code can
133   // reliably check whether it is running during the installer.
134   // @see drupal_installation_attempted()
135   $state = $install_state;
136   if (!empty($install_state['installation_finished'])) {
137     unset($GLOBALS['install_state']);
138   }
139
140   // All available tasks for this page request are now complete. Interactive
141   // installations can send output to the browser or redirect the user to the
142   // next page.
143   if ($state['interactive']) {
144     // If a session has been initiated in this request, make sure to save it.
145     if ($session = \Drupal::request()->getSession()) {
146       $session->save();
147     }
148     if ($state['parameters_changed']) {
149       // Redirect to the correct page if the URL parameters have changed.
150       install_goto(install_redirect_url($state));
151     }
152     elseif (isset($output)) {
153       // Display a page only if some output is available. Otherwise it is
154       // possible that we are printing a JSON page and theme output should
155       // not be shown.
156       install_display_output($output, $state);
157     }
158     elseif ($state['installation_finished']) {
159       // Redirect to the newly installed site.
160       install_goto('');
161     }
162   }
163 }
164
165 /**
166  * Returns an array of default settings for the global installation state.
167  *
168  * The installation state is initialized with these settings at the beginning
169  * of each page request. They may evolve during the page request, but they are
170  * initialized again once the next request begins.
171  *
172  * Non-interactive Drupal installations can override some of these default
173  * settings by passing in an array to the installation script, most notably
174  * 'parameters' (which contains one-time parameters such as 'profile' and
175  * 'langcode' that are normally passed in via the URL) and 'forms' (which can
176  * be used to programmatically submit forms during the installation; the keys
177  * of each element indicate the name of the installation task that the form
178  * submission is for, and the values are used as the $form_state->getValues()
179  * array that is passed on to the form submission via
180  * \Drupal::formBuilder()->submitForm()).
181  *
182  * @see \Drupal\Core\Form\FormBuilderInterface::submitForm()
183  */
184 function install_state_defaults() {
185   $defaults = [
186     // The current task being processed.
187     'active_task' => NULL,
188     // The last task that was completed during the previous installation
189     // request.
190     'completed_task' => NULL,
191     // TRUE when there are valid config directories.
192     'config_verified' => FALSE,
193     // TRUE when there is a valid database connection.
194     'database_verified' => FALSE,
195     // TRUE if database is empty & ready to install.
196     'database_ready' => FALSE,
197     // TRUE when a valid settings.php exists (containing both database
198     // connection information and config directory names).
199     'settings_verified' => FALSE,
200     // TRUE when the base system has been installed and is ready to operate.
201     'base_system_verified' => FALSE,
202     // Whether a translation file for the selected language will be downloaded
203     // from the translation server.
204     'download_translation' => FALSE,
205     // An array of forms to be programmatically submitted during the
206     // installation. The keys of each element indicate the name of the
207     // installation task that the form submission is for, and the values are
208     // used as the $form_state->getValues() array that is passed on to the form
209     // submission via \Drupal::formBuilder()->submitForm().
210     'forms' => [],
211     // This becomes TRUE only at the end of the installation process, after
212     // all available tasks have been completed and Drupal is fully installed.
213     // It is used by the installer to store correct information in the database
214     // about the completed installation, as well as to inform theme functions
215     // that all tasks are finished (so that the task list can be displayed
216     // correctly).
217     'installation_finished' => FALSE,
218     // Whether or not this installation is interactive. By default this will
219     // be set to FALSE if settings are passed in to install_drupal().
220     'interactive' => TRUE,
221     // An array of parameters for the installation, pre-populated by the URL
222     // or by the settings passed in to install_drupal(). This is primarily
223     // used to store 'profile' (the name of the chosen installation profile)
224     // and 'langcode' (the code of the chosen installation language), since
225     // these settings need to persist from page request to page request before
226     // the database is available for storage.
227     'parameters' => [],
228     // Whether or not the parameters have changed during the current page
229     // request. For interactive installations, this will trigger a page
230     // redirect.
231     'parameters_changed' => FALSE,
232     // An array of information about the chosen installation profile. This will
233     // be filled in based on the profile's .info.yml file.
234     'profile_info' => [],
235     // An array of available installation profiles.
236     'profiles' => [],
237     // The name of the theme to use during installation.
238     'theme' => 'seven',
239     // The server URL where the interface translation files can be downloaded.
240     // Tokens in the pattern will be replaced by appropriate values for the
241     // required translation file.
242     'server_pattern' => 'http://ftp.drupal.org/files/translations/%core/%project/%project-%version.%language.po',
243     // Installation tasks can set this to TRUE to force the page request to
244     // end (even if there is no themable output), in the case of an interactive
245     // installation. This is needed only rarely; for example, it would be used
246     // by an installation task that prints JSON output rather than returning a
247     // themed page. The most common example of this is during batch processing,
248     // but the Drupal installer automatically takes care of setting this
249     // parameter properly in that case, so that individual installation tasks
250     // which implement the batch API do not need to set it themselves.
251     'stop_page_request' => FALSE,
252     // Installation tasks can set this to TRUE to indicate that the task should
253     // be run again, even if it normally wouldn't be. This can be used, for
254     // example, if a single task needs to be spread out over multiple page
255     // requests, or if it needs to perform some validation before allowing
256     // itself to be marked complete. The most common examples of this are batch
257     // processing and form submissions, but the Drupal installer automatically
258     // takes care of setting this parameter properly in those cases, so that
259     // individual installation tasks which implement the batch API or form API
260     // do not need to set it themselves.
261     'task_not_complete' => FALSE,
262     // A list of installation tasks which have already been performed during
263     // the current page request.
264     'tasks_performed' => [],
265     // An array of translation files URIs available for the installation. Keyed
266     // by the translation language code.
267     'translations' => [],
268   ];
269   return $defaults;
270 }
271
272 /**
273  * Begins an installation request, modifying the installation state as needed.
274  *
275  * This function performs commands that must run at the beginning of every page
276  * request. It throws an exception if the installation should not proceed.
277  *
278  * @param $class_loader
279  *   The class loader. Normally Composer's ClassLoader, as included by the
280  *   front controller, but may also be decorated; e.g.,
281  *   \Symfony\Component\ClassLoader\ApcClassLoader.
282  * @param $install_state
283  *   An array of information about the current installation state. This is
284  *   modified with information gleaned from the beginning of the page request.
285  */
286 function install_begin_request($class_loader, &$install_state) {
287   $request = Request::createFromGlobals();
288
289   // Add any installation parameters passed in via the URL.
290   if ($install_state['interactive']) {
291     $install_state['parameters'] += $request->query->all();
292   }
293
294   // Validate certain core settings that are used throughout the installation.
295   if (!empty($install_state['parameters']['profile'])) {
296     $install_state['parameters']['profile'] = preg_replace('/[^a-zA-Z_0-9]/', '', $install_state['parameters']['profile']);
297   }
298   if (!empty($install_state['parameters']['langcode'])) {
299     $install_state['parameters']['langcode'] = preg_replace('/[^a-zA-Z_0-9\-]/', '', $install_state['parameters']['langcode']);
300   }
301
302   // Allow command line scripts to override server variables used by Drupal.
303   require_once __DIR__ . '/bootstrap.inc';
304
305   // Before having installed the system module and being able to do a module
306   // rebuild, prime the drupal_get_filename() static cache with the module's
307   // exact location.
308   // @todo Remove as part of https://www.drupal.org/node/2186491
309   drupal_get_filename('module', 'system', 'core/modules/system/system.info.yml');
310
311   // If the hash salt leaks, it becomes possible to forge a valid testing user
312   // agent, install a new copy of Drupal, and take over the original site.
313   // The user agent header is used to pass a database prefix in the request when
314   // running tests. However, for security reasons, it is imperative that no
315   // installation be permitted using such a prefix.
316   $user_agent = $request->cookies->get('SIMPLETEST_USER_AGENT') ?: $request->server->get('HTTP_USER_AGENT');
317   if ($install_state['interactive'] && strpos($user_agent, 'simpletest') !== FALSE && !drupal_valid_test_ua()) {
318     header($request->server->get('SERVER_PROTOCOL') . ' 403 Forbidden');
319     exit;
320   }
321   if ($install_state['interactive'] && drupal_valid_test_ua()) {
322     // Set the default timezone. While this doesn't cause any tests to fail, PHP
323     // complains if 'date.timezone' is not set in php.ini. The Australia/Sydney
324     // timezone is chosen so all tests are run using an edge case scenario
325     // (UTC+10  and DST). This choice is made to prevent timezone related
326     // regressions and reduce the fragility of the testing system in general.
327     date_default_timezone_set('Australia/Sydney');
328   }
329
330   $site_path = DrupalKernel::findSitePath($request, FALSE);
331   Settings::initialize(dirname(dirname(__DIR__)), $site_path, $class_loader);
332
333   // Ensure that procedural dependencies are loaded as early as possible,
334   // since the error/exception handlers depend on them.
335   require_once __DIR__ . '/../modules/system/system.install';
336   require_once __DIR__ . '/common.inc';
337   require_once __DIR__ . '/file.inc';
338   require_once __DIR__ . '/install.inc';
339   require_once __DIR__ . '/schema.inc';
340   require_once __DIR__ . '/database.inc';
341   require_once __DIR__ . '/form.inc';
342   require_once __DIR__ . '/batch.inc';
343
344   // Load module basics (needed for hook invokes).
345   include_once __DIR__ . '/module.inc';
346   require_once __DIR__ . '/entity.inc';
347
348   // Create a minimal mocked container to support calls to t() in the pre-kernel
349   // base system verification code paths below. The strings are not actually
350   // used or output for these calls.
351   // @todo Separate API level checks from UI-facing error messages.
352   $container = new ContainerBuilder();
353   $container->setParameter('language.default_values', Language::$defaultValues);
354   $container
355     ->register('language.default', 'Drupal\Core\Language\LanguageDefault')
356     ->addArgument('%language.default_values%');
357   $container
358     ->register('string_translation', 'Drupal\Core\StringTranslation\TranslationManager')
359     ->addArgument(new Reference('language.default'));
360
361   // Register the stream wrapper manager.
362   $container
363     ->register('stream_wrapper_manager', 'Drupal\Core\StreamWrapper\StreamWrapperManager')
364     ->addMethodCall('setContainer', [new Reference('service_container')]);
365   $container
366     ->register('file_system', 'Drupal\Core\File\FileSystem')
367     ->addArgument(new Reference('stream_wrapper_manager'))
368     ->addArgument(Settings::getInstance())
369     ->addArgument((new LoggerChannelFactory())->get('file'));
370
371   \Drupal::setContainer($container);
372
373   // Determine whether base system services are ready to operate.
374   try {
375     $sync_directory = config_get_config_directory(CONFIG_SYNC_DIRECTORY);
376     $install_state['config_verified'] = file_exists($sync_directory);
377   }
378   catch (Exception $e) {
379     $install_state['config_verified'] = FALSE;
380   }
381   $install_state['database_verified'] = install_verify_database_settings($site_path);
382   // A valid settings.php has database settings and a hash_salt value. Other
383   // settings like config_directories will be checked by system_requirements().
384   $install_state['settings_verified'] = $install_state['database_verified'] && (bool) Settings::get('hash_salt', FALSE);
385
386   // Install factory tables only after checking the database.
387   if ($install_state['database_verified'] && $install_state['database_ready']) {
388     $container
389       ->register('path.matcher', 'Drupal\Core\Path\PathMatcher')
390       ->addArgument(new Reference('config.factory'));
391   }
392
393   if ($install_state['settings_verified']) {
394     try {
395       $system_schema = system_schema();
396       end($system_schema);
397       $table = key($system_schema);
398       $install_state['base_system_verified'] = Database::getConnection()->schema()->tableExists($table);
399     }
400     catch (DatabaseExceptionWrapper $e) {
401       // The last defined table of the base system_schema() does not exist yet.
402       // $install_state['base_system_verified'] defaults to FALSE, so the code
403       // following below will use the minimal installer service container.
404       // As soon as the base system is verified here, the installer operates in
405       // a full and regular Drupal environment, without any kind of exceptions.
406     }
407   }
408
409   // Replace services with in-memory and null implementations. This kernel is
410   // replaced with a regular one in drupal_install_system().
411   if (!$install_state['base_system_verified']) {
412     $environment = 'install';
413     $GLOBALS['conf']['container_service_providers']['InstallerServiceProvider'] = 'Drupal\Core\Installer\InstallerServiceProvider';
414   }
415   else {
416     $environment = 'prod';
417   }
418   $GLOBALS['conf']['container_service_providers']['InstallerConfigOverride'] = 'Drupal\Core\Installer\ConfigOverride';
419
420   // Only allow dumping the container once the hash salt has been created.
421   $kernel = InstallerKernel::createFromRequest($request, $class_loader, $environment, (bool) Settings::get('hash_salt', FALSE));
422   $kernel->setSitePath($site_path);
423   $kernel->boot();
424   $container = $kernel->getContainer();
425   // If Drupal is being installed behind a proxy, configure the request.
426   ReverseProxyMiddleware::setSettingsOnRequest($request, Settings::getInstance());
427
428   // Register the file translation service.
429   if (isset($GLOBALS['config']['locale.settings']['translation']['path'])) {
430     $directory = $GLOBALS['config']['locale.settings']['translation']['path'];
431   }
432   else {
433     $directory = $site_path . '/files/translations';
434   }
435   $container->set('string_translator.file_translation', new FileTranslation($directory));
436   $container->get('string_translation')
437     ->addTranslator($container->get('string_translator.file_translation'));
438
439   // Add list of all available profiles to the installation state.
440   $listing = new ExtensionDiscovery($container->get('app.root'));
441   $listing->setProfileDirectories([]);
442   $install_state['profiles'] += $listing->scan('profile');
443
444   // Prime drupal_get_filename()'s static cache.
445   foreach ($install_state['profiles'] as $name => $profile) {
446     drupal_get_filename('profile', $name, $profile->getPathname());
447   }
448
449   if ($profile = _install_select_profile($install_state)) {
450     $install_state['parameters']['profile'] = $profile;
451     install_load_profile($install_state);
452     if (isset($install_state['profile_info']['distribution']['install']['theme'])) {
453       $install_state['theme'] = $install_state['profile_info']['distribution']['install']['theme'];
454     }
455   }
456
457   // Use the language from the profile configuration, if available, to override
458   // the language previously set in the parameters.
459   if (isset($install_state['profile_info']['distribution']['langcode'])) {
460     $install_state['parameters']['langcode'] = $install_state['profile_info']['distribution']['langcode'];
461   }
462
463   // Set the default language to the selected language, if any.
464   if (isset($install_state['parameters']['langcode'])) {
465     $default_language = new Language(['id' => $install_state['parameters']['langcode']]);
466     $container->get('language.default')->set($default_language);
467     \Drupal::translation()->setDefaultLangcode($install_state['parameters']['langcode']);
468   }
469
470   // Override the module list with a minimal set of modules.
471   $module_handler = \Drupal::moduleHandler();
472   if (!$module_handler->moduleExists('system')) {
473     $module_handler->addModule('system', 'core/modules/system');
474   }
475   if ($profile && !$module_handler->moduleExists($profile)) {
476     $module_handler->addProfile($profile, $install_state['profiles'][$profile]->getPath());
477   }
478
479   // Load all modules and perform request related initialization.
480   $kernel->preHandle($request);
481
482   // Initialize a route on this legacy request similar to
483   // \Drupal\Core\DrupalKernel::prepareLegacyRequest() since normal routing
484   // will not happen.
485   $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route('<none>'));
486   $request->attributes->set(RouteObjectInterface::ROUTE_NAME, '<none>');
487
488   // Prepare for themed output. We need to run this at the beginning of the
489   // page request to avoid a different theme accidentally getting set. (We also
490   // need to run it even in the case of command-line installations, to prevent
491   // any code in the installer that happens to initialize the theme system from
492   // accessing the database before it is set up yet.)
493   drupal_maintenance_theme();
494
495   if ($install_state['database_verified']) {
496     // Verify the last completed task in the database, if there is one.
497     $task = install_verify_completed_task();
498   }
499   else {
500     $task = NULL;
501
502     // Do not install over a configured settings.php.
503     if (Database::getConnectionInfo()) {
504       throw new AlreadyInstalledException($container->get('string_translation'));
505     }
506   }
507
508   // Ensure that the active configuration is empty before installation starts.
509   if ($install_state['config_verified'] && empty($task)) {
510     if (count($kernel->getConfigStorage()->listAll())) {
511       $task = NULL;
512       throw new AlreadyInstalledException($container->get('string_translation'));
513     }
514   }
515
516   // Modify the installation state as appropriate.
517   $install_state['completed_task'] = $task;
518 }
519
520 /**
521  * Runs all tasks for the current installation request.
522  *
523  * In the case of an interactive installation, all tasks will be attempted
524  * until one is reached that has output which needs to be displayed to the
525  * user, or until a page redirect is required. Otherwise, tasks will be
526  * attempted until the installation is finished.
527  *
528  * @param $install_state
529  *   An array of information about the current installation state. This is
530  *   passed along to each task, so it can be modified if necessary.
531  *
532  * @return
533  *   HTML output from the last completed task.
534  */
535 function install_run_tasks(&$install_state) {
536   do {
537     // Obtain a list of tasks to perform. The list of tasks itself can be
538     // dynamic (e.g., some might be defined by the installation profile,
539     // which is not necessarily known until the earlier tasks have run),
540     // so we regenerate the remaining tasks based on the installation state,
541     // each time through the loop.
542     $tasks_to_perform = install_tasks_to_perform($install_state);
543     // Run the first task on the list.
544     reset($tasks_to_perform);
545     $task_name = key($tasks_to_perform);
546     $task = array_shift($tasks_to_perform);
547     $install_state['active_task'] = $task_name;
548     $original_parameters = $install_state['parameters'];
549     $output = install_run_task($task, $install_state);
550     // Ensure the maintenance theme is initialized. If the install task has
551     // rebuilt the container the active theme will not be set. This can occur if
552     // the task has installed a module.
553     drupal_maintenance_theme();
554
555     $install_state['parameters_changed'] = ($install_state['parameters'] != $original_parameters);
556     // Store this task as having been performed during the current request,
557     // and save it to the database as completed, if we need to and if the
558     // database is in a state that allows us to do so. Also mark the
559     // installation as 'done' when we have run out of tasks.
560     if (!$install_state['task_not_complete']) {
561       $install_state['tasks_performed'][] = $task_name;
562       $install_state['installation_finished'] = empty($tasks_to_perform);
563       if ($task['run'] == INSTALL_TASK_RUN_IF_NOT_COMPLETED || $install_state['installation_finished']) {
564         \Drupal::state()->set('install_task', $install_state['installation_finished'] ? 'done' : $task_name);
565       }
566     }
567     // Stop when there are no tasks left. In the case of an interactive
568     // installation, also stop if we have some output to send to the browser,
569     // the URL parameters have changed, or an end to the page request was
570     // specifically called for.
571     $finished = empty($tasks_to_perform) || ($install_state['interactive'] && (isset($output) || $install_state['parameters_changed'] || $install_state['stop_page_request']));
572   } while (!$finished);
573   return $output;
574 }
575
576 /**
577  * Runs an individual installation task.
578  *
579  * @param $task
580  *   An array of information about the task to be run as returned by
581  *   hook_install_tasks().
582  * @param $install_state
583  *   An array of information about the current installation state. This is
584  *   passed in by reference so that it can be modified by the task.
585  *
586  * @return
587  *   The output of the task function, if there is any.
588  */
589 function install_run_task($task, &$install_state) {
590   $function = $task['function'];
591
592   if ($task['type'] == 'form') {
593     return install_get_form($function, $install_state);
594   }
595   elseif ($task['type'] == 'batch') {
596     // Start a new batch based on the task function, if one is not running
597     // already.
598     $current_batch = \Drupal::state()->get('install_current_batch');
599     if (!$install_state['interactive'] || !$current_batch) {
600       $batches = $function($install_state);
601       if (empty($batches)) {
602         // If the task did some processing and decided no batch was necessary,
603         // there is nothing more to do here.
604         return;
605       }
606       // Create a one item list of batches if only one batch was provided.
607       if (isset($batches['operations'])) {
608         $batches = [$batches];
609       }
610       foreach ($batches as $batch) {
611         batch_set($batch);
612         // For interactive batches, we need to store the fact that this batch
613         // task is currently running. Otherwise, we need to make sure the batch
614         // will complete in one page request.
615         if ($install_state['interactive']) {
616           \Drupal::state()->set('install_current_batch', $function);
617         }
618         else {
619           $batch =& batch_get();
620           $batch['progressive'] = FALSE;
621         }
622       }
623       // Process the batch. For progressive batches, this will redirect.
624       // Otherwise, the batch will complete.
625       // Disable the default script for the URL and clone the object, as
626       // batch_process() will add additional options to the batch URL.
627       $url = Url::fromUri('base:install.php', ['query' => $install_state['parameters'], 'script' => '']);
628       $response = batch_process($url, clone $url);
629       if ($response instanceof Response) {
630         if ($session = \Drupal::request()->getSession()) {
631           $session->save();
632         }
633         // Send the response.
634         $response->send();
635         exit;
636       }
637     }
638     // If we are in the middle of processing this batch, keep sending back
639     // any output from the batch process, until the task is complete.
640     elseif ($current_batch == $function) {
641       $output = _batch_page(\Drupal::request());
642       // Because Batch API now returns a JSON response for intermediary steps,
643       // but the installer doesn't handle Response objects yet, just send the
644       // output here and emulate the old model.
645       // @todo Replace this when we refactor the installer to use a request-
646       //   response workflow.
647       if ($output instanceof Response) {
648         $output->send();
649         $output = NULL;
650       }
651       // The task is complete when we try to access the batch page and receive
652       // FALSE in return, since this means we are at a URL where we are no
653       // longer requesting a batch ID.
654       if ($output === FALSE) {
655         // Return nothing so the next task will run in the same request.
656         \Drupal::state()->delete('install_current_batch');
657         return;
658       }
659       else {
660         // We need to force the page request to end if the task is not
661         // complete, since the batch API sometimes prints JSON output
662         // rather than returning a themed page.
663         $install_state['task_not_complete'] = $install_state['stop_page_request'] = TRUE;
664         return $output;
665       }
666     }
667   }
668
669   else {
670     // For normal tasks, just return the function result, whatever it is.
671     return $function($install_state);
672   }
673 }
674
675 /**
676  * Returns a list of tasks to perform during the current installation request.
677  *
678  * Note that the list of tasks can change based on the installation state as
679  * the page request evolves (for example, if an installation profile hasn't
680  * been selected yet, we don't yet know which profile tasks need to be run).
681  *
682  * @param $install_state
683  *   An array of information about the current installation state.
684  *
685  * @return
686  *   A list of tasks to be performed, with associated metadata.
687  */
688 function install_tasks_to_perform($install_state) {
689   // Start with a list of all currently available tasks.
690   $tasks = install_tasks($install_state);
691   foreach ($tasks as $name => $task) {
692     // Remove any tasks that were already performed or that never should run.
693     // Also, if we started this page request with an indication of the last
694     // task that was completed, skip that task and all those that come before
695     // it, unless they are marked as always needing to run.
696     if ($task['run'] == INSTALL_TASK_SKIP || in_array($name, $install_state['tasks_performed']) || (!empty($install_state['completed_task']) && empty($completed_task_found) && $task['run'] != INSTALL_TASK_RUN_IF_REACHED)) {
697       unset($tasks[$name]);
698     }
699     if (!empty($install_state['completed_task']) && $name == $install_state['completed_task']) {
700       $completed_task_found = TRUE;
701     }
702   }
703   return $tasks;
704 }
705
706 /**
707  * Returns a list of all tasks the installer currently knows about.
708  *
709  * This function will return tasks regardless of whether or not they are
710  * intended to run on the current page request. However, the list can change
711  * based on the installation state (for example, if an installation profile
712  * hasn't been selected yet, we don't yet know which profile tasks will be
713  * available).
714  *
715  * You can override this using hook_install_tasks() or
716  * hook_install_tasks_alter().
717  *
718  * @param $install_state
719  *   An array of information about the current installation state.
720  *
721  * @return
722  *   A list of tasks, with associated metadata as returned by
723  *   hook_install_tasks().
724  */
725 function install_tasks($install_state) {
726   // Determine whether a translation file must be imported during the
727   // 'install_import_translations' task. Import when a non-English language is
728   // available and selected. Also we will need translations even if the
729   // installer language is English but there are other languages on the system.
730   $needs_translations = (count($install_state['translations']) > 1 && !empty($install_state['parameters']['langcode']) && $install_state['parameters']['langcode'] != 'en') || \Drupal::languageManager()->isMultilingual();
731   // Determine whether a translation file must be downloaded during the
732   // 'install_download_translation' task. Download when a non-English language
733   // is selected, but no translation is yet in the translations directory.
734   $needs_download = isset($install_state['parameters']['langcode']) && !isset($install_state['translations'][$install_state['parameters']['langcode']]) && $install_state['parameters']['langcode'] != 'en';
735
736   // Start with the core installation tasks that run before handing control
737   // to the installation profile.
738   $tasks = [
739     'install_select_language' => [
740       'display_name' => t('Choose language'),
741       'run' => INSTALL_TASK_RUN_IF_REACHED,
742     ],
743     'install_download_translation' => [
744       'run' => $needs_download ? INSTALL_TASK_RUN_IF_REACHED : INSTALL_TASK_SKIP,
745     ],
746     'install_select_profile' => [
747       'display_name' => t('Choose profile'),
748       'display' => empty($install_state['profile_info']['distribution']['name']) && count($install_state['profiles']) != 1,
749       'run' => INSTALL_TASK_RUN_IF_REACHED,
750     ],
751     'install_load_profile' => [
752       'run' => INSTALL_TASK_RUN_IF_REACHED,
753     ],
754     'install_verify_requirements' => [
755       'display_name' => t('Verify requirements'),
756     ],
757     'install_settings_form' => [
758       'display_name' => t('Set up database'),
759       'type' => 'form',
760       // Even though the form only allows the user to enter database settings,
761       // we still need to display it if settings.php is invalid in any way,
762       // since the form submit handler is where settings.php is rewritten.
763       'run' => $install_state['settings_verified'] ? INSTALL_TASK_SKIP : INSTALL_TASK_RUN_IF_NOT_COMPLETED,
764       'function' => 'Drupal\Core\Installer\Form\SiteSettingsForm',
765     ],
766     'install_write_profile' => [],
767     'install_verify_database_ready' => [
768       'run' => $install_state['database_ready'] ? INSTALL_TASK_SKIP : INSTALL_TASK_RUN_IF_NOT_COMPLETED,
769     ],
770     'install_base_system' => [
771       'run' => $install_state['base_system_verified'] ? INSTALL_TASK_SKIP : INSTALL_TASK_RUN_IF_NOT_COMPLETED,
772     ],
773     // All tasks below are executed in a regular, full Drupal environment.
774     'install_bootstrap_full' => [
775       'run' => INSTALL_TASK_RUN_IF_REACHED,
776     ],
777     'install_profile_modules' => [
778       'display_name' => t('Install site'),
779       'type' => 'batch',
780     ],
781     'install_profile_themes' => [],
782     'install_install_profile' => [],
783     'install_import_translations' => [
784       'display_name' => t('Set up translations'),
785       'display' => $needs_translations,
786       'type' => 'batch',
787       'run' => $needs_translations ? INSTALL_TASK_RUN_IF_NOT_COMPLETED : INSTALL_TASK_SKIP,
788     ],
789     'install_configure_form' => [
790       'display_name' => t('Configure site'),
791       'type' => 'form',
792       'function' => 'Drupal\Core\Installer\Form\SiteConfigureForm',
793     ],
794   ];
795
796   // Now add any tasks defined by the installation profile.
797   if (!empty($install_state['parameters']['profile'])) {
798     // Load the profile install file, because it is not always loaded when
799     // hook_install_tasks() is invoked (e.g. batch processing).
800     $profile = $install_state['parameters']['profile'];
801     $profile_install_file = $install_state['profiles'][$profile]->getPath() . '/' . $profile . '.install';
802     if (file_exists($profile_install_file)) {
803       include_once \Drupal::root() . '/' . $profile_install_file;
804     }
805     $function = $install_state['parameters']['profile'] . '_install_tasks';
806     if (function_exists($function)) {
807       $result = $function($install_state);
808       if (is_array($result)) {
809         $tasks += $result;
810       }
811     }
812   }
813
814   // Finish by adding the remaining core tasks.
815   $tasks += [
816     'install_finish_translations' => [
817       'display_name' => t('Finish translations'),
818       'display' => $needs_translations,
819       'type' => 'batch',
820       'run' => $needs_translations ? INSTALL_TASK_RUN_IF_NOT_COMPLETED : INSTALL_TASK_SKIP,
821     ],
822     'install_finished' => [],
823   ];
824
825   // Allow the installation profile to modify the full list of tasks.
826   if (!empty($install_state['parameters']['profile'])) {
827     $profile = $install_state['parameters']['profile'];
828     if ($install_state['profiles'][$profile]->load()) {
829       $function = $install_state['parameters']['profile'] . '_install_tasks_alter';
830       if (function_exists($function)) {
831         $function($tasks, $install_state);
832       }
833     }
834   }
835
836   // Fill in default parameters for each task before returning the list.
837   foreach ($tasks as $task_name => &$task) {
838     $task += [
839       'display_name' => NULL,
840       'display' => !empty($task['display_name']),
841       'type' => 'normal',
842       'run' => INSTALL_TASK_RUN_IF_NOT_COMPLETED,
843       'function' => $task_name,
844     ];
845   }
846   return $tasks;
847 }
848
849 /**
850  * Returns a list of tasks that should be displayed to the end user.
851  *
852  * The output of this function is a list suitable for sending to
853  * maintenance-task-list.html.twig.
854  *
855  * @param $install_state
856  *   An array of information about the current installation state.
857  *
858  * @return
859  *   A list of tasks, with keys equal to the machine-readable task name and
860  *   values equal to the name that should be displayed.
861  *
862  * @see maintenance-task-list.html.twig
863  */
864 function install_tasks_to_display($install_state) {
865   $displayed_tasks = [];
866   foreach (install_tasks($install_state) as $name => $task) {
867     if ($task['display']) {
868       $displayed_tasks[$name] = $task['display_name'];
869     }
870   }
871   return $displayed_tasks;
872 }
873
874 /**
875  * Builds and processes a form for the installer environment.
876  *
877  * Ensures that FormBuilder does not redirect after submitting a form, since the
878  * installer uses a custom step/flow logic via install_run_tasks().
879  *
880  * @param string|array $form_id
881  *   The form ID to build and process.
882  * @param array $install_state
883  *   The current state of the installation.
884  *
885  * @return array|null
886  *   A render array containing the form to render, or NULL in case the form was
887  *   successfully submitted.
888  *
889  * @throws \Drupal\Core\Installer\Exception\InstallerException
890  */
891 function install_get_form($form_id, array &$install_state) {
892   // Ensure the form will not redirect, since install_run_tasks() uses a custom
893   // redirection logic.
894   $form_state = (new FormState())
895     ->addBuildInfo('args', [&$install_state])
896     ->disableRedirect();
897   $form_builder = \Drupal::formBuilder();
898   if ($install_state['interactive']) {
899     $form = $form_builder->buildForm($form_id, $form_state);
900     // If the form submission was not successful, the form needs to be rendered,
901     // which means the task is not complete yet.
902     if (!$form_state->isExecuted()) {
903       $install_state['task_not_complete'] = TRUE;
904       return $form;
905     }
906   }
907   else {
908     // For non-interactive installs, submit the form programmatically with the
909     // values taken from the installation state.
910     $install_form_id = $form_builder->getFormId($form_id, $form_state);
911     if (!empty($install_state['forms'][$install_form_id])) {
912       $form_state->setValues($install_state['forms'][$install_form_id]);
913     }
914     $form_builder->submitForm($form_id, $form_state);
915
916     // Throw an exception in case of any form validation error.
917     if ($errors = $form_state->getErrors()) {
918       throw new InstallerException(implode("\n", $errors));
919     }
920   }
921 }
922
923 /**
924  * Returns the URL that should be redirected to during an installation request.
925  *
926  * The output of this function is suitable for sending to install_goto().
927  *
928  * @param $install_state
929  *   An array of information about the current installation state.
930  *
931  * @return
932  *   The URL to redirect to.
933  *
934  * @see install_full_redirect_url()
935  */
936 function install_redirect_url($install_state) {
937   return 'core/install.php?' . UrlHelper::buildQuery($install_state['parameters']);
938 }
939
940 /**
941  * Returns the complete URL redirected to during an installation request.
942  *
943  * @param $install_state
944  *   An array of information about the current installation state.
945  *
946  * @return
947  *   The complete URL to redirect to.
948  *
949  * @see install_redirect_url()
950  */
951 function install_full_redirect_url($install_state) {
952   global $base_url;
953   return $base_url . '/' . install_redirect_url($install_state);
954 }
955
956 /**
957  * Displays themed installer output and ends the page request.
958  *
959  * Installation tasks should use #title to set the desired page
960  * title, but otherwise this function takes care of theming the overall page
961  * output during every step of the installation.
962  *
963  * @param $output
964  *   The content to display on the main part of the page.
965  * @param $install_state
966  *   An array of information about the current installation state.
967  */
968 function install_display_output($output, $install_state) {
969   // Ensure the maintenance theme is initialized.
970   // The regular initialization call in install_begin_request() may not be
971   // reached in case of an early installer error.
972   drupal_maintenance_theme();
973
974   // Prevent install.php from being indexed when installed in a sub folder.
975   // robots.txt rules are not read if the site is within domain.com/subfolder
976   // resulting in /subfolder/install.php being found through search engines.
977   // When settings.php is writeable this can be used via an external database
978   // leading a malicious user to gain php access to the server.
979   $noindex_meta_tag = [
980     '#tag' => 'meta',
981     '#attributes' => [
982       'name' => 'robots',
983       'content' => 'noindex, nofollow',
984     ],
985   ];
986   $output['#attached']['html_head'][] = [$noindex_meta_tag, 'install_meta_robots'];
987
988   // Only show the task list if there is an active task; otherwise, the page
989   // request has ended before tasks have even been started, so there is nothing
990   // meaningful to show.
991   $regions = [];
992   if (isset($install_state['active_task'])) {
993     // Let the theming function know when every step of the installation has
994     // been completed.
995     $active_task = $install_state['installation_finished'] ? NULL : $install_state['active_task'];
996     $task_list = [
997       '#theme' => 'maintenance_task_list',
998       '#items' => install_tasks_to_display($install_state),
999       '#active' => $active_task,
1000     ];
1001     $regions['sidebar_first'] = $task_list;
1002   }
1003
1004   $bare_html_page_renderer = \Drupal::service('bare_html_page_renderer');
1005   $response = $bare_html_page_renderer->renderBarePage($output, $output['#title'], 'install_page', $regions);
1006   $default_headers = [
1007     'Expires' => 'Sun, 19 Nov 1978 05:00:00 GMT',
1008     'Last-Modified' => gmdate(DATE_RFC1123, REQUEST_TIME),
1009     'Cache-Control' => 'no-cache, must-revalidate',
1010     'ETag' => '"' . REQUEST_TIME . '"',
1011   ];
1012   $response->headers->add($default_headers);
1013   $response->send();
1014   exit;
1015 }
1016
1017 /**
1018  * Verifies the requirements for installing Drupal.
1019  *
1020  * @param $install_state
1021  *   An array of information about the current installation state.
1022  *
1023  * @return
1024  *   A themed status report, or an exception if there are requirement errors.
1025  */
1026 function install_verify_requirements(&$install_state) {
1027   // Check the installation requirements for Drupal and this profile.
1028   $requirements = install_check_requirements($install_state);
1029
1030   // Verify existence of all required modules.
1031   $requirements += drupal_verify_profile($install_state);
1032
1033   return install_display_requirements($install_state, $requirements);
1034 }
1035
1036 /**
1037  * Installation task; install the base functionality Drupal needs to bootstrap.
1038  *
1039  * @param $install_state
1040  *   An array of information about the current installation state.
1041  */
1042 function install_base_system(&$install_state) {
1043   // Install system.module.
1044   drupal_install_system($install_state);
1045
1046   // Prevent the installer from using the system temporary directory after the
1047   // system module has been installed.
1048   if (drupal_valid_test_ua()) {
1049     // While the temporary directory could be preset/enforced in settings.php
1050     // like the public files directory, some tests expect it to be configurable
1051     // in the UI. If declared in settings.php, they would no longer be
1052     // configurable. The temporary directory needs to match what is set in each
1053     // test types ::prepareEnvironment() step.
1054     $temporary_directory = dirname(PublicStream::basePath()) . '/temp';
1055     file_prepare_directory($temporary_directory, FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY);
1056     \Drupal::configFactory()->getEditable('system.file')
1057       ->set('path.temporary', $temporary_directory)
1058       ->save();
1059   }
1060
1061   // Call file_ensure_htaccess() to ensure that all of Drupal's standard
1062   // directories (e.g., the public files directory and config directory) have
1063   // appropriate .htaccess files. These directories will have already been
1064   // created by this point in the installer, since Drupal creates them during
1065   // the install_verify_requirements() task. Note that we cannot call
1066   // file_ensure_access() any earlier than this, since it relies on
1067   // system.module in order to work.
1068   file_ensure_htaccess();
1069
1070   // Prime the drupal_get_filename() static cache with the user module's
1071   // exact location.
1072   // @todo Remove as part of https://www.drupal.org/node/2186491
1073   drupal_get_filename('module', 'user', 'core/modules/user/user.info.yml');
1074
1075   // Enable the user module so that sessions can be recorded during the
1076   // upcoming bootstrap step.
1077   \Drupal::service('module_installer')->install(['user'], FALSE);
1078
1079   // Save the list of other modules to install for the upcoming tasks.
1080   // State can be set to the database now that system.module is installed.
1081   $modules = $install_state['profile_info']['dependencies'];
1082
1083   \Drupal::state()->set('install_profile_modules', array_diff($modules, ['system']));
1084   $install_state['base_system_verified'] = TRUE;
1085 }
1086
1087 /**
1088  * Verifies and returns the last installation task that was completed.
1089  *
1090  * @return
1091  *   The last completed task, if there is one. An exception is thrown if Drupal
1092  *   is already installed.
1093  */
1094 function install_verify_completed_task() {
1095   try {
1096     $task = \Drupal::state()->get('install_task');
1097   }
1098   // Do not trigger an error if the database query fails, since the database
1099   // might not be set up yet.
1100   catch (\Exception $e) {
1101   }
1102   if (isset($task)) {
1103     if ($task == 'done') {
1104       throw new AlreadyInstalledException(\Drupal::service('string_translation'));
1105     }
1106     return $task;
1107   }
1108 }
1109
1110 /**
1111  * Verifies that settings.php specifies a valid database connection.
1112  *
1113  * @param string $site_path
1114  *   The site path.
1115  *
1116  * @return bool
1117  *   TRUE if there are no database errors.
1118  */
1119 function install_verify_database_settings($site_path) {
1120   if ($database = Database::getConnectionInfo()) {
1121     $database = $database['default'];
1122     $settings_file = './' . $site_path . '/settings.php';
1123     $errors = install_database_errors($database, $settings_file);
1124     if (empty($errors)) {
1125       return TRUE;
1126     }
1127   }
1128   return FALSE;
1129 }
1130
1131 /**
1132  * Verify that the database is ready (no existing Drupal installation).
1133  */
1134 function install_verify_database_ready() {
1135   $system_schema = system_schema();
1136   end($system_schema);
1137   $table = key($system_schema);
1138
1139   if ($database = Database::getConnectionInfo()) {
1140     if (Database::getConnection()->schema()->tableExists($table)) {
1141       throw new AlreadyInstalledException(\Drupal::service('string_translation'));
1142     }
1143   }
1144 }
1145
1146 /**
1147  * Checks a database connection and returns any errors.
1148  */
1149 function install_database_errors($database, $settings_file) {
1150   $errors = [];
1151
1152   // Check database type.
1153   $database_types = drupal_get_database_types();
1154   $driver = $database['driver'];
1155   if (!isset($database_types[$driver])) {
1156     $errors['driver'] = t("In your %settings_file file you have configured @drupal to use a %driver server, however your PHP installation currently does not support this database type.", ['%settings_file' => $settings_file, '@drupal' => drupal_install_profile_distribution_name(), '%driver' => $driver]);
1157   }
1158   else {
1159     // Run driver specific validation
1160     $errors += $database_types[$driver]->validateDatabaseSettings($database);
1161     if (!empty($errors)) {
1162       // No point to try further.
1163       return $errors;
1164     }
1165     // Run tasks associated with the database type. Any errors are caught in the
1166     // calling function.
1167     Database::addConnectionInfo('default', 'default', $database);
1168
1169     $errors = db_installer_object($driver)->runTasks();
1170   }
1171   return $errors;
1172 }
1173
1174 /**
1175  * Selects which profile to install.
1176  *
1177  * @param $install_state
1178  *   An array of information about the current installation state. The chosen
1179  *   profile will be added here, if it was not already selected previously, as
1180  *   will a list of all available profiles.
1181  *
1182  * @return
1183  *   For interactive installations, a form allowing the profile to be selected,
1184  *   if the user has a choice that needs to be made. Otherwise, an exception is
1185  *   thrown if a profile cannot be chosen automatically.
1186  */
1187 function install_select_profile(&$install_state) {
1188   if (empty($install_state['parameters']['profile'])) {
1189     // If there are no profiles at all, installation cannot proceed.
1190     if (empty($install_state['profiles'])) {
1191       throw new NoProfilesException(\Drupal::service('string_translation'));
1192     }
1193     // Try to automatically select a profile.
1194     if ($profile = _install_select_profile($install_state)) {
1195       $install_state['parameters']['profile'] = $profile;
1196     }
1197     else {
1198       // The non-interactive installer requires a profile parameter.
1199       if (!$install_state['interactive']) {
1200         throw new InstallerException(t('Missing profile parameter.'));
1201       }
1202       // Otherwise, display a form to select a profile.
1203       return install_get_form('Drupal\Core\Installer\Form\SelectProfileForm', $install_state);
1204     }
1205   }
1206 }
1207
1208 /**
1209  * Determines the installation profile to use in the installer.
1210  *
1211  * A profile will be selected in the following order of conditions:
1212  * - Only one profile is available.
1213  * - A specific profile name is requested in installation parameters:
1214  *   - For interactive installations via request query parameters.
1215  *   - For non-interactive installations via install_drupal() settings.
1216  * - A discovered profile that is a distribution. If multiple profiles are
1217  *   distributions, then the first discovered profile will be selected.
1218  * - Only one visible profile is available.
1219  *
1220  * @param array $install_state
1221  *   The current installer state, containing a 'profiles' key, which is an
1222  *   associative array of profiles with the machine-readable names as keys.
1223  *
1224  * @return
1225  *   The machine-readable name of the selected profile or NULL if no profile was
1226  *   selected.
1227  */
1228 function _install_select_profile(&$install_state) {
1229   // Don't need to choose profile if only one available.
1230   if (count($install_state['profiles']) == 1) {
1231     return key($install_state['profiles']);
1232   }
1233   if (!empty($install_state['parameters']['profile'])) {
1234     $profile = $install_state['parameters']['profile'];
1235     if (isset($install_state['profiles'][$profile])) {
1236       return $profile;
1237     }
1238   }
1239   // Check for a distribution profile.
1240   foreach ($install_state['profiles'] as $profile) {
1241     $profile_info = install_profile_info($profile->getName());
1242     if (!empty($profile_info['distribution'])) {
1243       return $profile->getName();
1244     }
1245   }
1246
1247   // Get all visible (not hidden) profiles.
1248   $visible_profiles = array_filter($install_state['profiles'], function ($profile) {
1249     $profile_info = install_profile_info($profile->getName());
1250     return !isset($profile_info['hidden']) || !$profile_info['hidden'];
1251   });
1252
1253   if (count($visible_profiles) == 1) {
1254     return (key($visible_profiles));
1255   }
1256 }
1257
1258 /**
1259  * Finds all .po files that are useful to the installer.
1260  *
1261  * @return
1262  *   An associative array of file URIs keyed by language code. URIs as
1263  *   returned by file_scan_directory().
1264  *
1265  * @see file_scan_directory()
1266  */
1267 function install_find_translations() {
1268   $translations = [];
1269   $files = \Drupal::service('string_translator.file_translation')->findTranslationFiles();
1270   // English does not need a translation file.
1271   array_unshift($files, (object) ['name' => 'en']);
1272   foreach ($files as $uri => $file) {
1273     // Strip off the file name component before the language code.
1274     $langcode = preg_replace('!^(.+\.)?([^\.]+)$!', '\2', $file->name);
1275     // Language codes cannot exceed 12 characters to fit into the {language}
1276     // table.
1277     if (strlen($langcode) <= 12) {
1278       $translations[$langcode] = $uri;
1279     }
1280   }
1281   return $translations;
1282 }
1283
1284 /**
1285  * Selects which language to use during installation.
1286  *
1287  * @param $install_state
1288  *   An array of information about the current installation state. The chosen
1289  *   langcode will be added here, if it was not already selected previously, as
1290  *   will a list of all available languages.
1291  *
1292  * @return
1293  *   For interactive installations, a form or other page output allowing the
1294  *   language to be selected or providing information about language selection,
1295  *   if a language has not been chosen. Otherwise, an exception is thrown if a
1296  *   language cannot be chosen automatically.
1297  */
1298 function install_select_language(&$install_state) {
1299   // Find all available translation files.
1300   $files = install_find_translations();
1301   $install_state['translations'] += $files;
1302
1303   // If a valid language code is set, continue with the next installation step.
1304   // When translations from the localization server are used, any language code
1305   // is accepted because the standard language list is kept in sync with the
1306   // languages available at http://localize.drupal.org.
1307   // When files from the translation directory are used, we only accept
1308   // languages for which a file is available.
1309   if (!empty($install_state['parameters']['langcode'])) {
1310     $standard_languages = LanguageManager::getStandardLanguageList();
1311     $langcode = $install_state['parameters']['langcode'];
1312     if ($langcode == 'en' || isset($files[$langcode]) || isset($standard_languages[$langcode])) {
1313       $install_state['parameters']['langcode'] = $langcode;
1314       return;
1315     }
1316   }
1317
1318   if (empty($install_state['parameters']['langcode'])) {
1319     // If we are performing an interactive installation, we display a form to
1320     // select a right language. If no translation files were found in the
1321     // translations directory, the form shows a list of standard languages. If
1322     // translation files were found the form shows a select list of the
1323     // corresponding languages to choose from.
1324     if ($install_state['interactive']) {
1325       return install_get_form('Drupal\Core\Installer\Form\SelectLanguageForm', $install_state);
1326     }
1327     // If we are performing a non-interactive installation. If only one language
1328     // (English) is available, assume the user knows what he is doing. Otherwise
1329     // throw an error.
1330     else {
1331       if (count($files) == 1) {
1332         $install_state['parameters']['langcode'] = current(array_keys($files));
1333         return;
1334       }
1335       else {
1336         throw new InstallerException(t('You must select a language to continue the installation.'));
1337       }
1338     }
1339   }
1340 }
1341
1342 /**
1343  * Download a translation file for the selected language.
1344  *
1345  * @param array $install_state
1346  *   An array of information about the current installation state.
1347  *
1348  * @return string
1349  *   A themed status report, or an exception if there are requirement errors.
1350  *   Upon successful download the page is reloaded and no output is returned.
1351  */
1352 function install_download_translation(&$install_state) {
1353   // Check whether all conditions are met to download. Download the translation
1354   // if possible.
1355   $requirements = install_check_translations($install_state['parameters']['langcode'], $install_state['server_pattern']);
1356   if ($output = install_display_requirements($install_state, $requirements)) {
1357     return $output;
1358   }
1359
1360   // The download was successful, reload the page in the new language.
1361   $install_state['translations'][$install_state['parameters']['langcode']] = TRUE;
1362   if ($install_state['interactive']) {
1363     install_goto(install_redirect_url($install_state));
1364   }
1365 }
1366
1367 /**
1368  * Attempts to get a file using a HTTP request and to store it locally.
1369  *
1370  * @param string $uri
1371  *   The URI of the file to grab.
1372  * @param string $destination
1373  *   Stream wrapper URI specifying where the file should be placed. If a
1374  *   directory path is provided, the file is saved into that directory under its
1375  *   original name. If the path contains a filename as well, that one will be
1376  *   used instead.
1377  *
1378  * @return bool
1379  *   TRUE on success, FALSE on failure.
1380  */
1381 function install_retrieve_file($uri, $destination) {
1382   $parsed_url = parse_url($uri);
1383   if (is_dir(drupal_realpath($destination))) {
1384     // Prevent URIs with triple slashes when gluing parts together.
1385     $path = str_replace('///', '//', "$destination/") . drupal_basename($parsed_url['path']);
1386   }
1387   else {
1388     $path = $destination;
1389   }
1390
1391   try {
1392     $response = \Drupal::httpClient()->get($uri, ['headers' => ['Accept' => 'text/plain']]);
1393     $data = (string) $response->getBody();
1394     if (empty($data)) {
1395       return FALSE;
1396     }
1397   }
1398   catch (RequestException $e) {
1399     return FALSE;
1400   }
1401   return file_put_contents($path, $data) !== FALSE;
1402 }
1403
1404 /**
1405  * Checks if the localization server can be contacted.
1406  *
1407  * @param string $uri
1408  *   The URI to contact.
1409  *
1410  * @return string
1411  *   TRUE if the URI was contacted successfully, FALSE if not.
1412  */
1413 function install_check_localization_server($uri) {
1414   try {
1415     \Drupal::httpClient()->head($uri);
1416     return TRUE;
1417   }
1418   catch (RequestException $e) {
1419     return FALSE;
1420   }
1421 }
1422
1423 /**
1424  * Extracts version information from a drupal core version string.
1425  *
1426  * @param string $version
1427  *   Version info string (e.g., 8.0.0, 8.1.0, 8.0.0-dev, 8.0.0-unstable1,
1428  *   8.0.0-alpha2, 8.0.0-beta3, and 8.0.0-rc4).
1429  *
1430  * @return array
1431  *   Associative array of version info:
1432  *   - major: Major version (e.g., "8").
1433  *   - minor: Minor version (e.g., "0").
1434  *   - patch: Patch version (e.g., "0").
1435  *   - extra: Extra version info (e.g., "alpha2").
1436  *   - extra_text: The text part of "extra" (e.g., "alpha").
1437  *   - extra_number: The number part of "extra" (e.g., "2").
1438  */
1439 function _install_get_version_info($version) {
1440   preg_match('/
1441     (
1442       (?P<major>[0-9]+)    # Major release number.
1443       \.          # .
1444       (?P<minor>[0-9]+)    # Minor release number.
1445       \.          # .
1446       (?P<patch>[0-9]+)    # Patch release number.
1447     )             #
1448     (             #
1449       -           # - separator for "extra" version information.
1450       (?P<extra>   #
1451         (?P<extra_text>[a-z]+)  # Release extra text (e.g., "alpha").
1452         (?P<extra_number>[0-9]*)  # Release extra number (no separator between text and number).
1453       )           #
1454       |           # OR no "extra" information.
1455     )
1456     /sx', $version, $matches);
1457
1458   return $matches;
1459 }
1460
1461 /**
1462  * Loads information about the chosen profile during installation.
1463  *
1464  * @param $install_state
1465  *   An array of information about the current installation state. The loaded
1466  *   profile information will be added here.
1467  */
1468 function install_load_profile(&$install_state) {
1469   $profile = $install_state['parameters']['profile'];
1470   $install_state['profiles'][$profile]->load();
1471   $install_state['profile_info'] = install_profile_info($profile, isset($install_state['parameters']['langcode']) ? $install_state['parameters']['langcode'] : 'en');
1472 }
1473
1474 /**
1475  * Performs a full bootstrap of Drupal during installation.
1476  */
1477 function install_bootstrap_full() {
1478   // Store the session on the request object and start it.
1479   /** @var \Symfony\Component\HttpFoundation\Session\SessionInterface $session */
1480   $session = \Drupal::service('session');
1481   \Drupal::request()->setSession($session);
1482   $session->start();
1483 }
1484
1485 /**
1486  * Installs required modules via a batch process.
1487  *
1488  * @param $install_state
1489  *   An array of information about the current installation state.
1490  *
1491  * @return
1492  *   The batch definition.
1493  */
1494 function install_profile_modules(&$install_state) {
1495   // We need to manually trigger the installation of core-provided entity types,
1496   // as those will not be handled by the module installer.
1497   install_core_entity_type_definitions();
1498
1499   $modules = \Drupal::state()->get('install_profile_modules') ?: [];
1500   $files = system_rebuild_module_data();
1501   \Drupal::state()->delete('install_profile_modules');
1502
1503   // Always install required modules first. Respect the dependencies between
1504   // the modules.
1505   $required = [];
1506   $non_required = [];
1507
1508   // Add modules that other modules depend on.
1509   foreach ($modules as $module) {
1510     if ($files[$module]->requires) {
1511       $modules = array_merge($modules, array_keys($files[$module]->requires));
1512     }
1513   }
1514   $modules = array_unique($modules);
1515   foreach ($modules as $module) {
1516     if (!empty($files[$module]->info['required'])) {
1517       $required[$module] = $files[$module]->sort;
1518     }
1519     else {
1520       $non_required[$module] = $files[$module]->sort;
1521     }
1522   }
1523   arsort($required);
1524   arsort($non_required);
1525
1526   $operations = [];
1527   foreach ($required + $non_required as $module => $weight) {
1528     $operations[] = ['_install_module_batch', [$module, $files[$module]->info['name']]];
1529   }
1530   $batch = [
1531     'operations' => $operations,
1532     'title' => t('Installing @drupal', ['@drupal' => drupal_install_profile_distribution_name()]),
1533     'error_message' => t('The installation has encountered an error.'),
1534   ];
1535   return $batch;
1536 }
1537
1538 /**
1539  * Installs entity type definitions provided by core.
1540  */
1541 function install_core_entity_type_definitions() {
1542   $update_manager = \Drupal::entityDefinitionUpdateManager();
1543   foreach (\Drupal::entityManager()->getDefinitions() as $entity_type) {
1544     if ($entity_type->getProvider() == 'core') {
1545       $update_manager->installEntityType($entity_type);
1546     }
1547   }
1548 }
1549
1550 /**
1551  * Installs themes.
1552  *
1553  * This does not use a batch, since installing themes is faster than modules and
1554  * because an installation profile typically installs 1-3 themes only (default
1555  * theme, base theme, admin theme).
1556  *
1557  * @param $install_state
1558  *   An array of information about the current installation state.
1559  */
1560 function install_profile_themes(&$install_state) {
1561   // Install the themes specified by the installation profile.
1562   $themes = $install_state['profile_info']['themes'];
1563   \Drupal::service('theme_handler')->install($themes);
1564
1565   // Ensure that the install profile's theme is used.
1566   // @see _drupal_maintenance_theme()
1567   \Drupal::theme()->resetActiveTheme();
1568 }
1569
1570 /**
1571  * Installs the install profile.
1572  *
1573  * @param $install_state
1574  *   An array of information about the current installation state.
1575  */
1576 function install_install_profile(&$install_state) {
1577   \Drupal::service('module_installer')->install([drupal_get_profile()], FALSE);
1578   // Install all available optional config. During installation the module order
1579   // is determined by dependencies. If there are no dependencies between modules
1580   // then the order in which they are installed is dependent on random factors
1581   // like PHP version. Optional configuration therefore might or might not be
1582   // created depending on this order. Ensuring that we have installed all of the
1583   // optional configuration whose dependencies can be met at this point removes
1584   // any disparities that this creates.
1585   \Drupal::service('config.installer')->installOptionalConfig();
1586
1587   // Ensure that the install profile's theme is used.
1588   // @see _drupal_maintenance_theme()
1589   \Drupal::theme()->resetActiveTheme();
1590 }
1591
1592 /**
1593  * Prepares the system for import and downloads additional translations.
1594  *
1595  * @param $install_state
1596  *   An array of information about the current installation state.
1597  *
1598  * @return
1599  *   The batch definition, if there are language files to download.
1600  */
1601 function install_download_additional_translations_operations(&$install_state) {
1602   \Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc');
1603
1604   $langcode = $install_state['parameters']['langcode'];
1605   if (!($language = ConfigurableLanguage::load($langcode))) {
1606     // Create the language if not already shipped with a profile.
1607     $language = ConfigurableLanguage::createFromLangcode($langcode);
1608   }
1609   $language->save();
1610
1611   // If a non-English language was selected, change the default language and
1612   // remove English.
1613   if ($langcode != 'en') {
1614     \Drupal::configFactory()->getEditable('system.site')
1615       ->set('langcode', $langcode)
1616       ->set('default_langcode', $langcode)
1617       ->save();
1618     \Drupal::service('language.default')->set($language);
1619     if (empty($install_state['profile_info']['keep_english'])) {
1620       entity_delete_multiple('configurable_language', ['en']);
1621     }
1622   }
1623
1624   // If there is more than one language or the single one is not English, we
1625   // should download/import translations.
1626   $languages = \Drupal::languageManager()->getLanguages();
1627   $operations = [];
1628   foreach ($languages as $langcode => $language) {
1629     // The installer language was already downloaded. Check downloads for the
1630     // other languages if any. Ignore any download errors here, since we
1631     // are in the middle of an install process and there is no way back. We
1632     // will not import what we cannot download.
1633     if ($langcode != 'en' && $langcode != $install_state['parameters']['langcode']) {
1634       $operations[] = ['install_check_translations', [$langcode, $install_state['server_pattern']]];
1635     }
1636   }
1637   return $operations;
1638 }
1639
1640 /**
1641  * Imports languages via a batch process during installation.
1642  *
1643  * @param $install_state
1644  *   An array of information about the current installation state.
1645  *
1646  * @return
1647  *   The batch definition, if there are language files to import.
1648  */
1649 function install_import_translations(&$install_state) {
1650   \Drupal::moduleHandler()->loadInclude('locale', 'translation.inc');
1651
1652   // If there is more than one language or the single one is not English, we
1653   // should import translations.
1654   $operations = install_download_additional_translations_operations($install_state);
1655   $languages = \Drupal::languageManager()->getLanguages();
1656   if (count($languages) > 1 || !isset($languages['en'])) {
1657     $operations[] = ['_install_prepare_import', [array_keys($languages), $install_state['server_pattern']]];
1658
1659     // Set up a batch to import translations for drupal core. Translation import
1660     // for contrib modules happens in install_import_translations_remaining.
1661     foreach ($languages as $language) {
1662       if (locale_translation_use_remote_source()) {
1663         $operations[] = ['locale_translation_batch_fetch_download', ['drupal', $language->getId()]];
1664       }
1665       $operations[] = ['locale_translation_batch_fetch_import', ['drupal', $language->getId(), []]];
1666     }
1667
1668     module_load_include('fetch.inc', 'locale');
1669     $batch = [
1670       'operations' => $operations,
1671       'title' => t('Updating translations.'),
1672       'progress_message' => '',
1673       'error_message' => t('Error importing translation files'),
1674       'finished' => 'locale_translation_batch_fetch_finished',
1675       'file' => drupal_get_path('module', 'locale') . '/locale.batch.inc',
1676     ];
1677     return $batch;
1678   }
1679 }
1680
1681 /**
1682  * Tells the translation import process that Drupal core is installed.
1683  *
1684  * @param array $langcodes
1685  *   Language codes used for the translations.
1686  * @param string $server_pattern
1687  *   Server access pattern (to replace language code, version number, etc. in).
1688  */
1689 function _install_prepare_import($langcodes, $server_pattern) {
1690   \Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc');
1691   $matches = [];
1692
1693   foreach ($langcodes as $langcode) {
1694     // Get the translation files located in the translations directory.
1695     $files = locale_translate_get_interface_translation_files(['drupal'], [$langcode]);
1696     // Pick the first file which matches the language, if any.
1697     $file = reset($files);
1698     if (is_object($file)) {
1699       $filename = $file->filename;
1700       preg_match('/drupal-([0-9a-z\.-]+)\.' . $langcode . '\.po/', $filename, $matches);
1701       // Get the version information.
1702       if ($version = $matches[1]) {
1703         $info = _install_get_version_info($version);
1704         // Picking the first file does not necessarily result in the right file. So
1705         // we check if at least the major version number is available.
1706         if ($info['major']) {
1707           $core = $info['major'] . '.x';
1708           $data = [
1709             'name' => 'drupal',
1710             'project_type' => 'module',
1711             'core' => $core,
1712             'version' => $version,
1713             'server_pattern' => $server_pattern,
1714             'status' => 1,
1715           ];
1716           \Drupal::service('locale.project')->set($data['name'], $data);
1717           module_load_include('compare.inc', 'locale');
1718           locale_translation_check_projects_local(['drupal'], [$langcode]);
1719         }
1720       }
1721     }
1722   }
1723 }
1724
1725 /**
1726  * Finishes importing files at end of installation.
1727  *
1728  * If other projects besides Drupal core have been installed, their translation
1729  * will be imported here.
1730  *
1731  * @param $install_state
1732  *   An array of information about the current installation state.
1733  *
1734  * @return array
1735  *   An array of batch definitions.
1736  */
1737 function install_finish_translations(&$install_state) {
1738   \Drupal::moduleHandler()->loadInclude('locale', 'fetch.inc');
1739   \Drupal::moduleHandler()->loadInclude('locale', 'compare.inc');
1740   \Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc');
1741
1742   // Build a fresh list of installed projects. When more projects than core are
1743   // installed, their translations will be downloaded (if required) and imported
1744   // using a batch.
1745   $projects = locale_translation_build_projects();
1746   $languages = \Drupal::languageManager()->getLanguages();
1747   $batches = [];
1748   if (count($projects) > 1) {
1749     $options = _locale_translation_default_update_options();
1750     if ($batch = locale_translation_batch_update_build([], array_keys($languages), $options)) {
1751       $batches[] = $batch;
1752     }
1753   }
1754
1755   // Creates configuration translations.
1756   $batches[] = locale_config_batch_update_components([], array_keys($languages));
1757   return $batches;
1758 }
1759
1760 /**
1761  * Performs final installation steps and displays a 'finished' page.
1762  *
1763  * @param $install_state
1764  *   An array of information about the current installation state.
1765  *
1766  * @return
1767  *   A message informing the user that the installation is complete.
1768  */
1769 function install_finished(&$install_state) {
1770   $profile = drupal_get_profile();
1771
1772   // Installation profiles are always loaded last.
1773   module_set_weight($profile, 1000);
1774
1775   // Build the router once after installing all modules.
1776   // This would normally happen upon KernelEvents::TERMINATE, but since the
1777   // installer does not use an HttpKernel, that event is never triggered.
1778   \Drupal::service('router.builder')->rebuild();
1779
1780   // Run cron to populate update status tables (if available) so that users
1781   // will be warned if they've installed an out of date Drupal version.
1782   // Will also trigger indexing of profile-supplied content or feeds.
1783   \Drupal::service('cron')->run();
1784
1785   if ($install_state['interactive']) {
1786     // Load current user and perform final login tasks.
1787     // This has to be done after drupal_flush_all_caches()
1788     // to avoid session regeneration.
1789     $account = User::load(1);
1790     user_login_finalize($account);
1791   }
1792
1793   $success_message = t('Congratulations, you installed @drupal!', [
1794     '@drupal' => drupal_install_profile_distribution_name(),
1795   ]);
1796   drupal_set_message($success_message);
1797 }
1798
1799 /**
1800  * Implements callback_batch_operation().
1801  *
1802  * Performs batch installation of modules.
1803  */
1804 function _install_module_batch($module, $module_name, &$context) {
1805   \Drupal::service('module_installer')->install([$module], FALSE);
1806   $context['results'][] = $module;
1807   $context['message'] = t('Installed %module module.', ['%module' => $module_name]);
1808 }
1809
1810 /**
1811  * Checks installation requirements and reports any errors.
1812  *
1813  * @param string $langcode
1814  *   Language code to check for download.
1815  * @param string $server_pattern
1816  *   Server access pattern (to replace language code, version number, etc. in).
1817  *
1818  * @return array|null
1819  *   Requirements compliance array. If the translation was downloaded
1820  *   successfully then an empty array is returned. Otherwise the requirements
1821  *   error with detailed information. NULL if the file already exists for this
1822  *   language code.
1823  */
1824 function install_check_translations($langcode, $server_pattern) {
1825   $requirements = [];
1826
1827   $readable = FALSE;
1828   $writable = FALSE;
1829   // @todo: Make this configurable.
1830   $site_path = \Drupal::service('site.path');
1831   $files_directory = $site_path . '/files';
1832   $translations_directory = $site_path . '/files/translations';
1833   $translations_directory_exists = FALSE;
1834   $online = FALSE;
1835
1836   // First attempt to create or make writable the files directory.
1837   file_prepare_directory($files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
1838   // Then, attempt to create or make writable the translations directory.
1839   file_prepare_directory($translations_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
1840
1841   // Get values so the requirements errors can be specific.
1842   if (drupal_verify_install_file($translations_directory, FILE_EXIST, 'dir')) {
1843     $readable = is_readable($translations_directory);
1844     $writable = is_writable($translations_directory);
1845     $translations_directory_exists = TRUE;
1846   }
1847
1848   // The file already exists, no need to attempt to download.
1849   if ($existing_file = glob($translations_directory . '/drupal-*.' . $langcode . '.po')) {
1850     return;
1851   }
1852
1853   // Build URL for the translation file and the translation server.
1854   $variables = [
1855     '%project' => 'drupal',
1856     '%version' => \Drupal::VERSION,
1857     '%core' => \Drupal::CORE_COMPATIBILITY,
1858     '%language' => $langcode,
1859   ];
1860   $translation_url = strtr($server_pattern, $variables);
1861
1862   $elements = parse_url($translation_url);
1863   $server_url = $elements['scheme'] . '://' . $elements['host'];
1864
1865   // Build the language name for display.
1866   $languages = LanguageManager::getStandardLanguageList();
1867   $language = isset($languages[$langcode]) ? $languages[$langcode][0] : $langcode;
1868
1869   // Check if any of the desired translation files are available or if the
1870   // translation server can be reached. In other words, check if we are online
1871   // and have an internet connection.
1872   if ($translation_available = install_check_localization_server($translation_url)) {
1873     $online = TRUE;
1874   }
1875   if (!$translation_available) {
1876     if (install_check_localization_server($server_url)) {
1877       $online = TRUE;
1878     }
1879   }
1880
1881   // If the translations directory does not exists, throw an error.
1882   if (!$translations_directory_exists) {
1883     $requirements['translations directory exists'] = [
1884       'title'       => t('Translations directory'),
1885       'value'       => t('The translations directory does not exist.'),
1886       'severity'    => REQUIREMENT_ERROR,
1887       'description' => t('The installer requires that you create a translations directory as part of the installation process. Create the directory %translations_directory . More details about installing Drupal are available in <a href=":install_txt">INSTALL.txt</a>.', ['%translations_directory' => $translations_directory, ':install_txt' => base_path() . 'core/INSTALL.txt']),
1888     ];
1889   }
1890   else {
1891     $requirements['translations directory exists'] = [
1892       'title'       => t('Translations directory'),
1893       'value'       => t('The directory %translations_directory exists.', ['%translations_directory' => $translations_directory]),
1894     ];
1895     // If the translations directory is not readable, throw an error.
1896     if (!$readable) {
1897       $requirements['translations directory readable'] = [
1898         'title'       => t('Translations directory'),
1899         'value'       => t('The translations directory is not readable.'),
1900         'severity'    => REQUIREMENT_ERROR,
1901         'description' => t('The installer requires read permissions to %translations_directory at all times. The <a href=":handbook_url">webhosting issues</a> documentation section offers help on this and other topics.', ['%translations_directory' => $translations_directory, ':handbook_url' => 'https://www.drupal.org/server-permissions']),
1902       ];
1903     }
1904     // If translations directory is not writable, throw an error.
1905     if (!$writable) {
1906       $requirements['translations directory writable'] = [
1907         'title'       => t('Translations directory'),
1908         'value'       => t('The translations directory is not writable.'),
1909         'severity'    => REQUIREMENT_ERROR,
1910         'description' => t('The installer requires write permissions to %translations_directory during the installation process. The <a href=":handbook_url">webhosting issues</a> documentation section offers help on this and other topics.', ['%translations_directory' => $translations_directory, ':handbook_url' => 'https://www.drupal.org/server-permissions']),
1911       ];
1912     }
1913     else {
1914       $requirements['translations directory writable'] = [
1915         'title'       => t('Translations directory'),
1916         'value'       => t('The translations directory is writable.'),
1917       ];
1918     }
1919   }
1920
1921   // If the translations server can not be contacted, throw an error.
1922   if (!$online) {
1923     $requirements['online'] = [
1924       'title'       => t('Internet'),
1925       'value'       => t('The translation server is offline.'),
1926       'severity'    => REQUIREMENT_ERROR,
1927       'description' => t('The installer requires to contact the translation server to download a translation file. Check your internet connection and verify that your website can reach the translation server at <a href=":server_url">@server_url</a>.', [':server_url' => $server_url, '@server_url' => $server_url]),
1928     ];
1929   }
1930   else {
1931     $requirements['online'] = [
1932       'title'       => t('Internet'),
1933       'value'       => t('The translation server is online.'),
1934     ];
1935     // If translation file is not found at the translation server, throw an
1936     // error.
1937     if (!$translation_available) {
1938       $requirements['translation available'] = [
1939         'title'       => t('Translation'),
1940         'value'       => t('The %language translation is not available.', ['%language' => $language]),
1941         'severity'    => REQUIREMENT_ERROR,
1942         'description' => t('The %language translation file is not available at the translation server. <a href=":url">Choose a different language</a> or select English and translate your website later.', ['%language' => $language, ':url' => $_SERVER['SCRIPT_NAME']]),
1943       ];
1944     }
1945     else {
1946       $requirements['translation available'] = [
1947         'title'       => t('Translation'),
1948         'value'       => t('The %language translation is available.', ['%language' => $language]),
1949       ];
1950     }
1951   }
1952
1953   if ($translations_directory_exists && $readable && $writable && $translation_available) {
1954     $translation_downloaded = install_retrieve_file($translation_url, $translations_directory);
1955
1956     if (!$translation_downloaded) {
1957       $requirements['translation downloaded'] = [
1958         'title'       => t('Translation'),
1959         'value'       => t('The %language translation could not be downloaded.', ['%language' => $language]),
1960         'severity'    => REQUIREMENT_ERROR,
1961         'description' => t('The %language translation file could not be downloaded. <a href=":url">Choose a different language</a> or select English and translate your website later.', ['%language' => $language, ':url' => $_SERVER['SCRIPT_NAME']]),
1962       ];
1963     }
1964   }
1965
1966   return $requirements;
1967 }
1968
1969 /**
1970  * Checks installation requirements and reports any errors.
1971  */
1972 function install_check_requirements($install_state) {
1973   $profile = $install_state['parameters']['profile'];
1974
1975   // Check the profile requirements.
1976   $requirements = drupal_check_profile($profile);
1977
1978   if ($install_state['settings_verified']) {
1979     return $requirements;
1980   }
1981
1982   // If Drupal is not set up already, we need to try to create the default
1983   // settings and services files.
1984   $default_files = [];
1985   $default_files['settings.php'] = [
1986     'file' => 'settings.php',
1987     'file_default' => 'default.settings.php',
1988     'title_default' => t('Default settings file'),
1989     'description_default' => t('The default settings file does not exist.'),
1990     'title' => t('Settings file'),
1991   ];
1992
1993   foreach ($default_files as $default_file_info) {
1994     $readable = FALSE;
1995     $writable = FALSE;
1996     $site_path = './' . \Drupal::service('site.path');
1997     $file = $site_path . "/{$default_file_info['file']}";
1998     $default_file = "./sites/default/{$default_file_info['file_default']}";
1999     $exists = FALSE;
2000     // Verify that the directory exists.
2001     if (drupal_verify_install_file($site_path, FILE_EXIST, 'dir')) {
2002       if (drupal_verify_install_file($file, FILE_EXIST)) {
2003         // If it does, make sure it is writable.
2004         $readable = drupal_verify_install_file($file, FILE_READABLE);
2005         $writable = drupal_verify_install_file($file, FILE_WRITABLE);
2006         $exists = TRUE;
2007       }
2008     }
2009
2010     // If the default $default_file does not exist, or is not readable,
2011     // report an error.
2012     if (!drupal_verify_install_file($default_file, FILE_EXIST | FILE_READABLE)) {
2013       $requirements["default $file file exists"] = [
2014         'title' => $default_file_info['title_default'],
2015         'value' => $default_file_info['description_default'],
2016         'severity' => REQUIREMENT_ERROR,
2017         'description' => t('The @drupal installer requires that the %default-file file not be modified in any way from the original download.', [
2018             '@drupal' => drupal_install_profile_distribution_name(),
2019             '%default-file' => $default_file
2020           ]),
2021       ];
2022     }
2023     // Otherwise, if $file does not exist yet, we can try to copy
2024     // $default_file to create it.
2025     elseif (!$exists) {
2026       $copied = drupal_verify_install_file($site_path, FILE_EXIST | FILE_WRITABLE, 'dir') && @copy($default_file, $file);
2027       if ($copied) {
2028         // If the new $file file has the same owner as $default_file this means
2029         // $default_file is owned by the webserver user. This is an inherent
2030         // security weakness because it allows a malicious webserver process to
2031         // append arbitrary PHP code and then execute it. However, it is also a
2032         // common configuration on shared hosting, and there is nothing Drupal
2033         // can do to prevent it. In this situation, having $file also owned by
2034         // the webserver does not introduce any additional security risk, so we
2035         // keep the file in place. Additionally, this situation also occurs when
2036         // the test runner is being run be different user than the webserver.
2037         if (fileowner($default_file) === fileowner($file) || DRUPAL_TEST_IN_CHILD_SITE) {
2038           $readable = drupal_verify_install_file($file, FILE_READABLE);
2039           $writable = drupal_verify_install_file($file, FILE_WRITABLE);
2040           $exists = TRUE;
2041         }
2042         // If $file and $default_file have different owners, this probably means
2043         // the server is set up "securely" (with the webserver running as its
2044         // own user, distinct from the user who owns all the Drupal PHP files),
2045         // although with either a group or world writable sites directory.
2046         // Keeping $file owned by the webserver would therefore introduce a
2047         // security risk. It would also cause a usability problem, since site
2048         // owners who do not have root access to the file system would be unable
2049         // to edit their settings file later on. We therefore must delete the
2050         // file we just created and force the administrator to log on to the
2051         // server and create it manually.
2052         else {
2053           $deleted = @drupal_unlink($file);
2054           // We expect deleting the file to be successful (since we just
2055           // created it ourselves above), but if it fails somehow, we set a
2056           // variable so we can display a one-time error message to the
2057           // administrator at the bottom of the requirements list. We also try
2058           // to make the file writable, to eliminate any conflicting error
2059           // messages in the requirements list.
2060           $exists = !$deleted;
2061           if ($exists) {
2062             $settings_file_ownership_error = TRUE;
2063             $readable = drupal_verify_install_file($file, FILE_READABLE);
2064             $writable = drupal_verify_install_file($file, FILE_WRITABLE);
2065           }
2066         }
2067       }
2068     }
2069
2070     // If the $file does not exist, throw an error.
2071     if (!$exists) {
2072       $requirements["$file file exists"] = [
2073         'title' => $default_file_info['title'],
2074         'value' => t('The %file does not exist.', ['%file' => $default_file_info['title']]),
2075         'severity' => REQUIREMENT_ERROR,
2076         'description' => t('The @drupal installer requires that you create a %file as part of the installation process. Copy the %default_file file to %file. More details about installing Drupal are available in <a href=":install_txt">INSTALL.txt</a>.', [
2077             '@drupal' => drupal_install_profile_distribution_name(),
2078             '%file' => $file,
2079             '%default_file' => $default_file,
2080             ':install_txt' => base_path() . 'core/INSTALL.txt'
2081           ]),
2082       ];
2083     }
2084     else {
2085       $requirements["$file file exists"] = [
2086         'title' => $default_file_info['title'],
2087         'value' => t('The %file exists.', ['%file' => $file]),
2088       ];
2089       // If the $file is not readable, throw an error.
2090       if (!$readable) {
2091         $requirements["$file file readable"] = [
2092           'title' => $default_file_info['title'],
2093           'value' => t('The %file is not readable.', ['%file' => $default_file_info['title']]),
2094           'severity' => REQUIREMENT_ERROR,
2095           'description' => t('@drupal requires read permissions to %file at all times. The <a href=":handbook_url">webhosting issues</a> documentation section offers help on this and other topics.', [
2096               '@drupal' => drupal_install_profile_distribution_name(),
2097               '%file' => $file,
2098               ':handbook_url' => 'https://www.drupal.org/server-permissions'
2099             ]),
2100         ];
2101       }
2102       // If the $file is not writable, throw an error.
2103       if (!$writable) {
2104         $requirements["$file file writeable"] = [
2105           'title' => $default_file_info['title'],
2106           'value' => t('The %file is not writable.', ['%file' => $default_file_info['title']]),
2107           'severity' => REQUIREMENT_ERROR,
2108           'description' => t('The @drupal installer requires write permissions to %file during the installation process. The <a href=":handbook_url">webhosting issues</a> documentation section offers help on this and other topics.', [
2109               '@drupal' => drupal_install_profile_distribution_name(),
2110               '%file' => $file,
2111               ':handbook_url' => 'https://www.drupal.org/server-permissions'
2112             ]),
2113         ];
2114       }
2115       else {
2116         $requirements["$file file"] = [
2117           'title' => $default_file_info['title'],
2118           'value' => t('The @file is writable.', ['@file' => $default_file_info['title']]),
2119         ];
2120       }
2121       if (!empty($settings_file_ownership_error)) {
2122         $requirements["$file file ownership"] = [
2123           'title' => $default_file_info['title'],
2124           'value' => t('The @file is owned by the web server.', ['@file' => $default_file_info['title']]),
2125           'severity' => REQUIREMENT_ERROR,
2126           'description' => t('The @drupal installer failed to create a %file file with proper file ownership. Log on to your web server, remove the existing %file file, and create a new one by copying the %default_file file to %file. More details about installing Drupal are available in <a href=":install_txt">INSTALL.txt</a>. The <a href=":handbook_url">webhosting issues</a> documentation section offers help on this and other topics.', [
2127               '@drupal' => drupal_install_profile_distribution_name(),
2128               '%file' => $file,
2129               '%default_file' => $default_file,
2130               ':install_txt' => base_path() . 'core/INSTALL.txt',
2131               ':handbook_url' => 'https://www.drupal.org/server-permissions'
2132             ]),
2133         ];
2134       }
2135     }
2136   }
2137   return $requirements;
2138 }
2139
2140 /**
2141  * Displays installation requirements.
2142  *
2143  * @param array $install_state
2144  *   An array of information about the current installation state.
2145  * @param array $requirements
2146  *   An array of requirements, in the same format as is returned by
2147  *   hook_requirements().
2148  *
2149  * @return
2150  *   A themed status report, or an exception if there are requirement errors.
2151  *   If there are only requirement warnings, a themed status report is shown
2152  *   initially, but the user is allowed to bypass it by providing 'continue=1'
2153  *   in the URL. Otherwise, no output is returned, so that the next task can be
2154  *   run in the same page request.
2155  *
2156  * @throws \Drupal\Core\Installer\Exception\InstallerException
2157  */
2158 function install_display_requirements($install_state, $requirements) {
2159   // Check the severity of the requirements reported.
2160   $severity = drupal_requirements_severity($requirements);
2161
2162   // If there are errors, always display them. If there are only warnings, skip
2163   // them if the user has provided a URL parameter acknowledging the warnings
2164   // and indicating a desire to continue anyway. See drupal_requirements_url().
2165   if ($severity == REQUIREMENT_ERROR || ($severity == REQUIREMENT_WARNING && empty($install_state['parameters']['continue']))) {
2166     if ($install_state['interactive']) {
2167       $build['report']['#type'] = 'status_report';
2168       $build['report']['#requirements'] = $requirements;
2169       if ($severity == REQUIREMENT_WARNING) {
2170         $build['#title'] = t('Requirements review');
2171         $build['#suffix'] = t('Check the messages and <a href=":retry">retry</a>, or you may choose to <a href=":cont">continue anyway</a>.', [':retry' => drupal_requirements_url(REQUIREMENT_ERROR), ':cont' => drupal_requirements_url($severity)]);
2172       }
2173       else {
2174         $build['#title'] = t('Requirements problem');
2175         $build['#suffix'] = t('Check the messages and <a href=":url">try again</a>.', [':url' => drupal_requirements_url($severity)]);
2176       }
2177       return $build;
2178     }
2179     else {
2180       // Throw an exception showing any unmet requirements.
2181       $failures = [];
2182       foreach ($requirements as $requirement) {
2183         // Skip warnings altogether for non-interactive installations; these
2184         // proceed in a single request so there is no good opportunity (and no
2185         // good method) to warn the user anyway.
2186         if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) {
2187           $failures[] = $requirement['title'] . ': ' . $requirement['value'] . "\n\n" . $requirement['description'];
2188         }
2189       }
2190       if (!empty($failures)) {
2191         throw new InstallerException(implode("\n\n", $failures));
2192       }
2193     }
2194   }
2195 }
2196
2197 /**
2198  * Installation task; writes profile to settings.php if possible.
2199  *
2200  * @param array $install_state
2201  *   An array of information about the current installation state.
2202  *
2203  * @see _install_select_profile()
2204  *
2205  * @throws \Drupal\Core\Installer\Exception\InstallProfileMismatchException
2206  *
2207  * @deprecated in Drupal 8.3.0 and will be removed before Drupal 9.0.0. The
2208  *    install profile is written to core.extension.
2209  */
2210 function install_write_profile($install_state) {
2211   // Only need to write to settings.php if it is possible. The primary storage
2212   // for the install profile is the core.extension configuration.
2213   $settings_path = \Drupal::service('site.path') . '/settings.php';
2214   if (is_writable($settings_path)) {
2215     // Remember the profile which was used.
2216     $settings['settings']['install_profile'] = (object) [
2217       'value' => $install_state['parameters']['profile'],
2218       'required' => TRUE,
2219     ];
2220     drupal_rewrite_settings($settings);
2221   }
2222   elseif (($settings_profile = Settings::get('install_profile')) && $settings_profile !== $install_state['parameters']['profile']) {
2223     throw new InstallProfileMismatchException($install_state['parameters']['profile'], $settings_profile, $settings_path, \Drupal::translation());
2224   }
2225 }