SiteSettingsForm.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. <?php
  2. namespace Drupal\Core\Installer\Form;
  3. use Drupal\Component\Utility\Crypt;
  4. use Drupal\Core\Database\Database;
  5. use Drupal\Core\File\FileSystemInterface;
  6. use Drupal\Core\Form\FormBase;
  7. use Drupal\Core\Form\FormStateInterface;
  8. use Drupal\Core\Render\RendererInterface;
  9. use Drupal\Core\Site\Settings;
  10. use Symfony\Component\DependencyInjection\ContainerInterface;
  11. /**
  12. * Provides a form to configure and rewrite settings.php.
  13. *
  14. * @internal
  15. */
  16. class SiteSettingsForm extends FormBase {
  17. /**
  18. * The site path.
  19. *
  20. * @var string
  21. */
  22. protected $sitePath;
  23. /**
  24. * The renderer.
  25. *
  26. * @var \Drupal\Core\Render\RendererInterface
  27. */
  28. protected $renderer;
  29. /**
  30. * Constructs a new SiteSettingsForm.
  31. *
  32. * @param string $site_path
  33. * The site path.
  34. */
  35. public function __construct($site_path, RendererInterface $renderer) {
  36. $this->sitePath = $site_path;
  37. $this->renderer = $renderer;
  38. }
  39. /**
  40. * {@inheritdoc}
  41. */
  42. public static function create(ContainerInterface $container) {
  43. return new static(
  44. $container->get('site.path'),
  45. $container->get('renderer')
  46. );
  47. }
  48. /**
  49. * {@inheritdoc}
  50. */
  51. public function getFormId() {
  52. return 'install_settings_form';
  53. }
  54. /**
  55. * {@inheritdoc}
  56. */
  57. public function buildForm(array $form, FormStateInterface $form_state) {
  58. $settings_file = './' . $this->sitePath . '/settings.php';
  59. $form['#title'] = $this->t('Database configuration');
  60. $drivers = drupal_get_database_types();
  61. $drivers_keys = array_keys($drivers);
  62. // Unless there is input for this form (for a non-interactive installation,
  63. // input originates from the $settings array passed into install_drupal()),
  64. // check whether database connection settings have been prepared in
  65. // settings.php already.
  66. // Note: The installer even executes this form if there is a valid database
  67. // connection already, since the submit handler of this form is responsible
  68. // for writing all $settings to settings.php (not limited to $databases).
  69. $input = &$form_state->getUserInput();
  70. if (!isset($input['driver']) && $database = Database::getConnectionInfo()) {
  71. $input['driver'] = $database['default']['driver'];
  72. $input[$database['default']['driver']] = $database['default'];
  73. }
  74. if (isset($input['driver'])) {
  75. $default_driver = $input['driver'];
  76. // In case of database connection info from settings.php, as well as for a
  77. // programmed form submission (non-interactive installer), the table prefix
  78. // information is usually normalized into an array already, but the form
  79. // element only allows to configure one default prefix for all tables.
  80. $prefix = &$input[$default_driver]['prefix'];
  81. if (isset($prefix) && is_array($prefix)) {
  82. $prefix = $prefix['default'];
  83. }
  84. $default_options = $input[$default_driver];
  85. }
  86. // If there is no database information yet, suggest the first available driver
  87. // as default value, so that its settings form is made visible via #states
  88. // when JavaScript is enabled (see below).
  89. else {
  90. $default_driver = current($drivers_keys);
  91. $default_options = [];
  92. }
  93. $form['driver'] = [
  94. '#type' => 'radios',
  95. '#title' => $this->t('Database type'),
  96. '#required' => TRUE,
  97. '#default_value' => $default_driver,
  98. ];
  99. if (count($drivers) == 1) {
  100. $form['driver']['#disabled'] = TRUE;
  101. }
  102. // Add driver specific configuration options.
  103. foreach ($drivers as $key => $driver) {
  104. $form['driver']['#options'][$key] = $driver->name();
  105. $form['settings'][$key] = $driver->getFormOptions($default_options);
  106. $form['settings'][$key]['#prefix'] = '<h2 class="js-hide">' . $this->t('@driver_name settings', ['@driver_name' => $driver->name()]) . '</h2>';
  107. $form['settings'][$key]['#type'] = 'container';
  108. $form['settings'][$key]['#tree'] = TRUE;
  109. $form['settings'][$key]['advanced_options']['#parents'] = [$key];
  110. $form['settings'][$key]['#states'] = [
  111. 'visible' => [
  112. ':input[name=driver]' => ['value' => $key],
  113. ],
  114. ];
  115. }
  116. $form['actions'] = ['#type' => 'actions'];
  117. $form['actions']['save'] = [
  118. '#type' => 'submit',
  119. '#value' => $this->t('Save and continue'),
  120. '#button_type' => 'primary',
  121. '#limit_validation_errors' => [
  122. ['driver'],
  123. [$default_driver],
  124. ],
  125. '#submit' => ['::submitForm'],
  126. ];
  127. $form['errors'] = [];
  128. $form['settings_file'] = ['#type' => 'value', '#value' => $settings_file];
  129. return $form;
  130. }
  131. /**
  132. * {@inheritdoc}
  133. */
  134. public function validateForm(array &$form, FormStateInterface $form_state) {
  135. $driver = $form_state->getValue('driver');
  136. $database = $form_state->getValue($driver);
  137. $drivers = drupal_get_database_types();
  138. $reflection = new \ReflectionClass($drivers[$driver]);
  139. $install_namespace = $reflection->getNamespaceName();
  140. // Cut the trailing \Install from namespace.
  141. $database['namespace'] = substr($install_namespace, 0, strrpos($install_namespace, '\\'));
  142. $database['driver'] = $driver;
  143. // See default.settings.php for an explanation of the 'autoload' key.
  144. if ($autoload = Database::findDriverAutoloadDirectory($database['namespace'], DRUPAL_ROOT)) {
  145. $database['autoload'] = $autoload;
  146. }
  147. $form_state->set('database', $database);
  148. foreach ($this->getDatabaseErrors($database, $form_state->getValue('settings_file')) as $name => $message) {
  149. $form_state->setErrorByName($name, $message);
  150. }
  151. }
  152. /**
  153. * Get any database errors and links them to a form element.
  154. *
  155. * @param array $database
  156. * An array of database settings.
  157. * @param string $settings_file
  158. * The settings file that contains the database settings.
  159. *
  160. * @return array
  161. * An array of form errors keyed by the element name and parents.
  162. */
  163. protected function getDatabaseErrors(array $database, $settings_file) {
  164. $errors = install_database_errors($database, $settings_file);
  165. $form_errors = array_filter($errors, function ($value) {
  166. // Errors keyed by something other than an integer already are linked to
  167. // form elements.
  168. return is_int($value);
  169. });
  170. // Find the generic errors.
  171. $errors = array_diff_key($errors, $form_errors);
  172. if (count($errors)) {
  173. $error_message = static::getDatabaseErrorsTemplate($errors);
  174. // These are generic errors, so we do not have any specific key of the
  175. // database connection array to attach them to; therefore, we just put
  176. // them in the error array with standard numeric keys.
  177. $form_errors[$database['driver'] . '][0'] = $this->renderer->renderPlain($error_message);
  178. }
  179. return $form_errors;
  180. }
  181. /**
  182. * Gets the inline template render array to display the database errors.
  183. *
  184. * @param \Drupal\Core\StringTranslation\TranslatableMarkup[] $errors
  185. * The database errors to list.
  186. *
  187. * @return mixed[]
  188. * The inline template render array to display the database errors.
  189. */
  190. public static function getDatabaseErrorsTemplate(array $errors) {
  191. return [
  192. '#type' => 'inline_template',
  193. '#template' => '{% trans %}Resolve all issues below to continue the installation. For help configuring your database server, see the <a href="https://www.drupal.org/docs/8/install">installation handbook</a>, or contact your hosting provider.{% endtrans %}{{ errors }}',
  194. '#context' => [
  195. 'errors' => [
  196. '#theme' => 'item_list',
  197. '#items' => $errors,
  198. ],
  199. ],
  200. ];
  201. }
  202. /**
  203. * {@inheritdoc}
  204. */
  205. public function submitForm(array &$form, FormStateInterface $form_state) {
  206. global $install_state;
  207. // Update global settings array and save.
  208. $settings = [];
  209. $database = $form_state->get('database');
  210. $settings['databases']['default']['default'] = (object) [
  211. 'value' => $database,
  212. 'required' => TRUE,
  213. ];
  214. $settings['settings']['hash_salt'] = (object) [
  215. 'value' => Crypt::randomBytesBase64(55),
  216. 'required' => TRUE,
  217. ];
  218. // If settings.php does not contain a config sync directory name we need to
  219. // configure one.
  220. if (empty(Settings::get('config_sync_directory'))) {
  221. if (empty($install_state['config_install_path'])) {
  222. // Add a randomized config directory name to settings.php
  223. $config_sync_directory = $this->createRandomConfigDirectory();
  224. }
  225. else {
  226. // Install profiles can contain a config sync directory. If they do,
  227. // 'config_install_path' is a path to the directory.
  228. $config_sync_directory = $install_state['config_install_path'];
  229. }
  230. $settings['settings']['config_sync_directory'] = (object) [
  231. 'value' => $config_sync_directory,
  232. 'required' => TRUE,
  233. ];
  234. }
  235. drupal_rewrite_settings($settings);
  236. // Indicate that the settings file has been verified, and check the database
  237. // for the last completed task, now that we have a valid connection. This
  238. // last step is important since we want to trigger an error if the new
  239. // database already has Drupal installed.
  240. $install_state['settings_verified'] = TRUE;
  241. $install_state['config_verified'] = TRUE;
  242. $install_state['database_verified'] = TRUE;
  243. $install_state['completed_task'] = install_verify_completed_task();
  244. }
  245. /**
  246. * Create a random config sync directory.
  247. *
  248. * @return string
  249. * The path to the generated config sync directory.
  250. */
  251. protected function createRandomConfigDirectory() {
  252. $config_sync_directory = \Drupal::service('site.path') . '/files/config_' . Crypt::randomBytesBase64(55) . '/sync';
  253. // This should never fail, it is created here inside the public files
  254. // directory, which has already been verified to be writable itself.
  255. if (\Drupal::service('file_system')->prepareDirectory($config_sync_directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
  256. // Put a README.txt into the sync config directory. This is required so
  257. // that they can later be added to git. Since this directory is
  258. // auto-created, we have to write out the README rather than just adding
  259. // it to the drupal core repo.
  260. $text = 'This directory contains configuration to be imported into your Drupal site. To make this configuration active, visit admin/config/development/configuration/sync.' . ' For information about deploying configuration between servers, see https://www.drupal.org/documentation/administer/config';
  261. file_put_contents($config_sync_directory . '/README.txt', $text);
  262. }
  263. return $config_sync_directory;
  264. }
  265. }