install.inc 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254
  1. <?php
  2. /**
  3. * @file
  4. * API functions for installing modules and themes.
  5. */
  6. use Drupal\Component\Utility\Crypt;
  7. use Drupal\Component\Utility\OpCodeCache;
  8. use Drupal\Component\Utility\Unicode;
  9. use Drupal\Component\Utility\UrlHelper;
  10. use Drupal\Core\Extension\Dependency;
  11. use Drupal\Core\Extension\ExtensionDiscovery;
  12. use Drupal\Core\File\FileSystemInterface;
  13. use Drupal\Core\Installer\InstallerKernel;
  14. use Drupal\Core\Site\Settings;
  15. use Symfony\Component\HttpFoundation\RedirectResponse;
  16. /**
  17. * Requirement severity -- Informational message only.
  18. */
  19. const REQUIREMENT_INFO = -1;
  20. /**
  21. * Requirement severity -- Requirement successfully met.
  22. */
  23. const REQUIREMENT_OK = 0;
  24. /**
  25. * Requirement severity -- Warning condition; proceed but flag warning.
  26. */
  27. const REQUIREMENT_WARNING = 1;
  28. /**
  29. * Requirement severity -- Error condition; abort installation.
  30. */
  31. const REQUIREMENT_ERROR = 2;
  32. /**
  33. * File permission check -- File exists.
  34. */
  35. const FILE_EXIST = 1;
  36. /**
  37. * File permission check -- File is readable.
  38. */
  39. const FILE_READABLE = 2;
  40. /**
  41. * File permission check -- File is writable.
  42. */
  43. const FILE_WRITABLE = 4;
  44. /**
  45. * File permission check -- File is executable.
  46. */
  47. const FILE_EXECUTABLE = 8;
  48. /**
  49. * File permission check -- File does not exist.
  50. */
  51. const FILE_NOT_EXIST = 16;
  52. /**
  53. * File permission check -- File is not readable.
  54. */
  55. const FILE_NOT_READABLE = 32;
  56. /**
  57. * File permission check -- File is not writable.
  58. */
  59. const FILE_NOT_WRITABLE = 64;
  60. /**
  61. * File permission check -- File is not executable.
  62. */
  63. const FILE_NOT_EXECUTABLE = 128;
  64. /**
  65. * Loads .install files for installed modules to initialize the update system.
  66. */
  67. function drupal_load_updates() {
  68. /** @var \Drupal\Core\Extension\ModuleExtensionList $extension_list_module */
  69. $extension_list_module = \Drupal::service('extension.list.module');
  70. foreach (drupal_get_installed_schema_version(NULL, FALSE, TRUE) as $module => $schema_version) {
  71. if ($extension_list_module->exists($module) && !$extension_list_module->checkIncompatibility($module)) {
  72. if ($schema_version > -1) {
  73. module_load_install($module);
  74. }
  75. }
  76. }
  77. }
  78. /**
  79. * Loads the installation profile, extracting its defined distribution name.
  80. *
  81. * @return
  82. * The distribution name defined in the profile's .info.yml file. Defaults to
  83. * "Drupal" if none is explicitly provided by the installation profile.
  84. *
  85. * @see install_profile_info()
  86. */
  87. function drupal_install_profile_distribution_name() {
  88. // During installation, the profile information is stored in the global
  89. // installation state (it might not be saved anywhere yet).
  90. $info = [];
  91. if (InstallerKernel::installationAttempted()) {
  92. global $install_state;
  93. if (isset($install_state['profile_info'])) {
  94. $info = $install_state['profile_info'];
  95. }
  96. }
  97. // At all other times, we load the profile via standard methods.
  98. else {
  99. $profile = \Drupal::installProfile();
  100. $info = \Drupal::service('extension.list.profile')->getExtensionInfo($profile);
  101. }
  102. return isset($info['distribution']['name']) ? $info['distribution']['name'] : 'Drupal';
  103. }
  104. /**
  105. * Loads the installation profile, extracting its defined version.
  106. *
  107. * @return string
  108. * Distribution version defined in the profile's .info.yml file.
  109. * Defaults to \Drupal::VERSION if no version is explicitly provided by the
  110. * installation profile.
  111. *
  112. * @see install_profile_info()
  113. */
  114. function drupal_install_profile_distribution_version() {
  115. // During installation, the profile information is stored in the global
  116. // installation state (it might not be saved anywhere yet).
  117. if (InstallerKernel::installationAttempted()) {
  118. global $install_state;
  119. return isset($install_state['profile_info']['version']) ? $install_state['profile_info']['version'] : \Drupal::VERSION;
  120. }
  121. // At all other times, we load the profile via standard methods.
  122. else {
  123. $profile = \Drupal::installProfile();
  124. $info = \Drupal::service('extension.list.profile')->getExtensionInfo($profile);
  125. return $info['version'];
  126. }
  127. }
  128. /**
  129. * Detects all supported databases that are compiled into PHP.
  130. *
  131. * @return
  132. * An array of database types compiled into PHP.
  133. */
  134. function drupal_detect_database_types() {
  135. $databases = drupal_get_database_types();
  136. foreach ($databases as $driver => $installer) {
  137. $databases[$driver] = $installer->name();
  138. }
  139. return $databases;
  140. }
  141. /**
  142. * Returns all supported database driver installer objects.
  143. *
  144. * @return \Drupal\Core\Database\Install\Tasks[]
  145. * An array of available database driver installer objects.
  146. */
  147. function drupal_get_database_types() {
  148. $databases = [];
  149. $drivers = [];
  150. // The internal database driver name is any valid PHP identifier.
  151. $mask = ExtensionDiscovery::PHP_FUNCTION_PATTERN;
  152. // Find drivers in the Drupal\Core and Drupal\Driver namespaces.
  153. /** @var \Drupal\Core\File\FileSystemInterface $file_system */
  154. $file_system = \Drupal::service('file_system');
  155. $files = $file_system->scanDirectory(DRUPAL_ROOT . '/core/lib/Drupal/Core/Database/Driver', $mask, ['recurse' => FALSE]);
  156. if (is_dir(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database')) {
  157. $files += $file_system->scanDirectory(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database/', $mask, ['recurse' => FALSE]);
  158. }
  159. foreach ($files as $file) {
  160. if (file_exists($file->uri . '/Install/Tasks.php')) {
  161. // The namespace doesn't need to be added here, because
  162. // db_installer_object() will find it.
  163. $drivers[$file->filename] = NULL;
  164. }
  165. }
  166. // Find drivers in Drupal module namespaces.
  167. /** @var \Composer\Autoload\ClassLoader $class_loader */
  168. $class_loader = \Drupal::service('class_loader');
  169. // We cannot use the file cache because it does not always exist.
  170. $extension_discovery = new ExtensionDiscovery(DRUPAL_ROOT, FALSE, []);
  171. $modules = $extension_discovery->scan('module');
  172. foreach ($modules as $module) {
  173. $module_driver_path = DRUPAL_ROOT . '/' . $module->getPath() . '/src/Driver/Database';
  174. if (is_dir($module_driver_path)) {
  175. $driver_files = $file_system->scanDirectory($module_driver_path, $mask, ['recurse' => FALSE]);
  176. foreach ($driver_files as $driver_file) {
  177. $tasks_file = $module_driver_path . '/' . $driver_file->filename . '/Install/Tasks.php';
  178. if (file_exists($tasks_file)) {
  179. $namespace = 'Drupal\\' . $module->getName() . '\\Driver\\Database\\' . $driver_file->filename;
  180. // The namespace needs to be added for db_installer_object() to find
  181. // it.
  182. $drivers[$driver_file->filename] = $namespace;
  183. // The directory needs to be added to the autoloader, because this is
  184. // early in the installation process: the module hasn't been enabled
  185. // yet and the database connection info array (including its 'autoload'
  186. // key) hasn't been created yet.
  187. $class_loader->addPsr4($namespace . '\\', $module->getPath() . '/src/Driver/Database/' . $driver_file->filename);
  188. }
  189. }
  190. }
  191. }
  192. foreach ($drivers as $driver => $namespace) {
  193. $installer = db_installer_object($driver, $namespace);
  194. if ($installer->installable()) {
  195. $databases[$driver] = $installer;
  196. }
  197. }
  198. // Usability: unconditionally put the MySQL driver on top.
  199. if (isset($databases['mysql'])) {
  200. $mysql_database = $databases['mysql'];
  201. unset($databases['mysql']);
  202. $databases = ['mysql' => $mysql_database] + $databases;
  203. }
  204. return $databases;
  205. }
  206. /**
  207. * Replaces values in settings.php with values in the submitted array.
  208. *
  209. * This function replaces values in place if possible, even for
  210. * multidimensional arrays. This way the old settings do not linger,
  211. * overridden and also the doxygen on a value remains where it should be.
  212. *
  213. * @param $settings
  214. * An array of settings that need to be updated. Multidimensional arrays
  215. * are dumped up to a stdClass object. The object can have value, required
  216. * and comment properties.
  217. * @code
  218. * $settings['settings']['config_sync_directory'] = (object) array(
  219. * 'value' => 'config_hash/sync',
  220. * 'required' => TRUE,
  221. * );
  222. * @endcode
  223. * gets dumped as:
  224. * @code
  225. * $settings['config_sync_directory'] = 'config_hash/sync'
  226. * @endcode
  227. */
  228. function drupal_rewrite_settings($settings = [], $settings_file = NULL) {
  229. if (!isset($settings_file)) {
  230. $settings_file = \Drupal::service('site.path') . '/settings.php';
  231. }
  232. // Build list of setting names and insert the values into the global namespace.
  233. $variable_names = [];
  234. $settings_settings = [];
  235. foreach ($settings as $setting => $data) {
  236. if ($setting != 'settings') {
  237. _drupal_rewrite_settings_global($GLOBALS[$setting], $data);
  238. }
  239. else {
  240. _drupal_rewrite_settings_global($settings_settings, $data);
  241. }
  242. $variable_names['$' . $setting] = $setting;
  243. }
  244. $contents = file_get_contents($settings_file);
  245. if ($contents !== FALSE) {
  246. // Initialize the contents for the settings.php file if it is empty.
  247. if (trim($contents) === '') {
  248. $contents = "<?php\n";
  249. }
  250. // Step through each token in settings.php and replace any variables that
  251. // are in the passed-in array.
  252. $buffer = '';
  253. $state = 'default';
  254. foreach (token_get_all($contents) as $token) {
  255. if (is_array($token)) {
  256. list($type, $value) = $token;
  257. }
  258. else {
  259. $type = -1;
  260. $value = $token;
  261. }
  262. // Do not operate on whitespace.
  263. if (!in_array($type, [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT])) {
  264. switch ($state) {
  265. case 'default':
  266. if ($type === T_VARIABLE && isset($variable_names[$value])) {
  267. // This will be necessary to unset the dumped variable.
  268. $parent = &$settings;
  269. // This is the current index in parent.
  270. $index = $variable_names[$value];
  271. // This will be necessary for descending into the array.
  272. $current = &$parent[$index];
  273. $state = 'candidate_left';
  274. }
  275. break;
  276. case 'candidate_left':
  277. if ($value == '[') {
  278. $state = 'array_index';
  279. }
  280. if ($value == '=') {
  281. $state = 'candidate_right';
  282. }
  283. break;
  284. case 'array_index':
  285. if (_drupal_rewrite_settings_is_array_index($type, $value)) {
  286. $index = trim($value, '\'"');
  287. $state = 'right_bracket';
  288. }
  289. else {
  290. // $a[foo()] or $a[$bar] or something like that.
  291. throw new Exception('invalid array index');
  292. }
  293. break;
  294. case 'right_bracket':
  295. if ($value == ']') {
  296. if (isset($current[$index])) {
  297. // If the new settings has this index, descend into it.
  298. $parent = &$current;
  299. $current = &$parent[$index];
  300. $state = 'candidate_left';
  301. }
  302. else {
  303. // Otherwise, jump back to the default state.
  304. $state = 'wait_for_semicolon';
  305. }
  306. }
  307. else {
  308. // $a[1 + 2].
  309. throw new Exception('] expected');
  310. }
  311. break;
  312. case 'candidate_right':
  313. if (_drupal_rewrite_settings_is_simple($type, $value)) {
  314. $value = _drupal_rewrite_settings_dump_one($current);
  315. // Unsetting $current would not affect $settings at all.
  316. unset($parent[$index]);
  317. // Skip the semicolon because _drupal_rewrite_settings_dump_one() added one.
  318. $state = 'semicolon_skip';
  319. }
  320. else {
  321. $state = 'wait_for_semicolon';
  322. }
  323. break;
  324. case 'wait_for_semicolon':
  325. if ($value == ';') {
  326. $state = 'default';
  327. }
  328. break;
  329. case 'semicolon_skip':
  330. if ($value == ';') {
  331. $value = '';
  332. $state = 'default';
  333. }
  334. else {
  335. // If the expression was $a = 1 + 2; then we replaced 1 and
  336. // the + is unexpected.
  337. throw new Exception('Unexpected token after replacing value.');
  338. }
  339. break;
  340. }
  341. }
  342. $buffer .= $value;
  343. }
  344. foreach ($settings as $name => $setting) {
  345. $buffer .= _drupal_rewrite_settings_dump($setting, '$' . $name);
  346. }
  347. // Write the new settings file.
  348. if (file_put_contents($settings_file, $buffer) === FALSE) {
  349. throw new Exception("Failed to modify '$settings_file'. Verify the file permissions.");
  350. }
  351. else {
  352. // In case any $settings variables were written, import them into the
  353. // Settings singleton.
  354. if (!empty($settings_settings)) {
  355. $old_settings = Settings::getAll();
  356. new Settings($settings_settings + $old_settings);
  357. }
  358. // The existing settings.php file might have been included already. In
  359. // case an opcode cache is enabled, the rewritten contents of the file
  360. // will not be reflected in this process. Ensure to invalidate the file
  361. // in case an opcode cache is enabled.
  362. OpCodeCache::invalidate(DRUPAL_ROOT . '/' . $settings_file);
  363. }
  364. }
  365. else {
  366. throw new Exception("Failed to open '$settings_file'. Verify the file permissions.");
  367. }
  368. }
  369. /**
  370. * Helper for drupal_rewrite_settings().
  371. *
  372. * Checks whether this token represents a scalar or NULL.
  373. *
  374. * @param int $type
  375. * The token type.
  376. * @param string $value
  377. * The value of the token.
  378. *
  379. * @return bool
  380. * TRUE if this token represents a scalar or NULL.
  381. *
  382. * @see token_name()
  383. */
  384. function _drupal_rewrite_settings_is_simple($type, $value) {
  385. $is_integer = $type == T_LNUMBER;
  386. $is_float = $type == T_DNUMBER;
  387. $is_string = $type == T_CONSTANT_ENCAPSED_STRING;
  388. $is_boolean_or_null = $type == T_STRING && in_array(strtoupper($value), ['TRUE', 'FALSE', 'NULL']);
  389. return $is_integer || $is_float || $is_string || $is_boolean_or_null;
  390. }
  391. /**
  392. * Helper for drupal_rewrite_settings().
  393. *
  394. * Checks whether this token represents a valid array index: a number or a
  395. * string.
  396. *
  397. * @param int $type
  398. * The token type.
  399. *
  400. * @return bool
  401. * TRUE if this token represents a number or a string.
  402. *
  403. * @see token_name()
  404. */
  405. function _drupal_rewrite_settings_is_array_index($type) {
  406. $is_integer = $type == T_LNUMBER;
  407. $is_float = $type == T_DNUMBER;
  408. $is_string = $type == T_CONSTANT_ENCAPSED_STRING;
  409. return $is_integer || $is_float || $is_string;
  410. }
  411. /**
  412. * Helper for drupal_rewrite_settings().
  413. *
  414. * Makes the new settings global.
  415. *
  416. * @param array|null $ref
  417. * A reference to a nested index in $GLOBALS.
  418. * @param array|object $variable
  419. * The nested value of the setting being copied.
  420. */
  421. function _drupal_rewrite_settings_global(&$ref, $variable) {
  422. if (is_object($variable)) {
  423. $ref = $variable->value;
  424. }
  425. else {
  426. foreach ($variable as $k => $v) {
  427. _drupal_rewrite_settings_global($ref[$k], $v);
  428. }
  429. }
  430. }
  431. /**
  432. * Helper for drupal_rewrite_settings().
  433. *
  434. * Dump the relevant value properties.
  435. *
  436. * @param array|object $variable
  437. * The container for variable values.
  438. * @param string $variable_name
  439. * Name of variable.
  440. * @return string
  441. * A string containing valid PHP code of the variable suitable for placing
  442. * into settings.php.
  443. */
  444. function _drupal_rewrite_settings_dump($variable, $variable_name) {
  445. $return = '';
  446. if (is_object($variable)) {
  447. if (!empty($variable->required)) {
  448. $return .= _drupal_rewrite_settings_dump_one($variable, "$variable_name = ", "\n");
  449. }
  450. }
  451. else {
  452. foreach ($variable as $k => $v) {
  453. $return .= _drupal_rewrite_settings_dump($v, $variable_name . "['" . $k . "']");
  454. }
  455. }
  456. return $return;
  457. }
  458. /**
  459. * Helper for drupal_rewrite_settings().
  460. *
  461. * Dump the value of a value property and adds the comment if it exists.
  462. *
  463. * @param object $variable
  464. * A stdClass object with at least a value property.
  465. * @param string $prefix
  466. * A string to prepend to the variable's value.
  467. * @param string $suffix
  468. * A string to append to the variable's value.
  469. * @return string
  470. * A string containing valid PHP code of the variable suitable for placing
  471. * into settings.php.
  472. */
  473. function _drupal_rewrite_settings_dump_one(\stdClass $variable, $prefix = '', $suffix = '') {
  474. $return = $prefix . var_export($variable->value, TRUE) . ';';
  475. if (!empty($variable->comment)) {
  476. $return .= ' // ' . $variable->comment;
  477. }
  478. $return .= $suffix;
  479. return $return;
  480. }
  481. /**
  482. * Creates the config directory and ensures it is operational.
  483. *
  484. * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. There is no
  485. * replacement.
  486. *
  487. * @see https://www.drupal.org/node/3018145
  488. */
  489. function drupal_install_config_directories() {
  490. @trigger_error('drupal_install_config_directories() is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. There is no replacement. See https://www.drupal.org/node/3018145.', E_USER_DEPRECATED);
  491. global $config_directories, $install_state;
  492. // If settings.php does not contain a config sync directory name we need to
  493. // configure one.
  494. if (empty($config_directories[CONFIG_SYNC_DIRECTORY])) {
  495. if (empty($install_state['config_install_path'])) {
  496. // Add a randomized config directory name to settings.php
  497. $config_directories[CONFIG_SYNC_DIRECTORY] = \Drupal::service('site.path') . '/files/config_' . Crypt::randomBytesBase64(55) . '/sync';
  498. }
  499. else {
  500. // Install profiles can contain a config sync directory. If they do,
  501. // 'config_install_path' is a path to the directory.
  502. $config_directories[CONFIG_SYNC_DIRECTORY] = $install_state['config_install_path'];
  503. }
  504. $settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) [
  505. 'value' => $config_directories[CONFIG_SYNC_DIRECTORY],
  506. 'required' => TRUE,
  507. ];
  508. // Rewrite settings.php, which also sets the value as global variable.
  509. drupal_rewrite_settings($settings);
  510. }
  511. // This should never fail, since if the config directory was specified in
  512. // settings.php it will have already been created and verified earlier, and
  513. // if it wasn't specified in settings.php, it is created here inside the
  514. // public files directory, which has already been verified to be writable
  515. // itself. But if it somehow fails anyway, the installation cannot proceed.
  516. // Bail out using a similar error message as in system_requirements().
  517. if (!\Drupal::service('file_system')->prepareDirectory($config_directories[CONFIG_SYNC_DIRECTORY], FileSystemInterface::CREATE_DIRECTORY)
  518. && !file_exists($config_directories[CONFIG_SYNC_DIRECTORY])) {
  519. throw new Exception("The directory '" . config_get_config_directory(CONFIG_SYNC_DIRECTORY) . "' could not be created. To proceed with the installation, either create the directory or ensure that the installer has the permissions to create it automatically. For more information, see the <a href='https://www.drupal.org/server-permissions'>online handbook</a>.");
  520. }
  521. elseif (is_writable($config_directories[CONFIG_SYNC_DIRECTORY])) {
  522. // Put a README.txt into the sync config directory. This is required so that
  523. // they can later be added to git. Since this directory is auto-created, we
  524. // have to write out the README rather than just adding it to the drupal core
  525. // repo.
  526. $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';
  527. file_put_contents(config_get_config_directory(CONFIG_SYNC_DIRECTORY) . '/README.txt', "$text\n");
  528. }
  529. }
  530. /**
  531. * Ensures that the config directory exists and is writable, or can be made so.
  532. *
  533. * @param string $type
  534. * Type of config directory to return. Drupal core provides 'sync'.
  535. *
  536. * @return bool
  537. * TRUE if the config directory exists and is writable.
  538. *
  539. * @deprecated in drupal:8.1.0 and is removed from drupal:9.0.0. Use
  540. * config_get_config_directory() and
  541. * \Drupal\Core\File\FileSystemInterface::prepareDirectory() instead.
  542. *
  543. * @see https://www.drupal.org/node/2501187
  544. */
  545. function install_ensure_config_directory($type) {
  546. @trigger_error('install_ensure_config_directory() is deprecated in Drupal 8.1.0 and will be removed before Drupal 9.0.0. Use config_get_config_directory() and \Drupal\Core\File\FileSystemInterface::prepareDirectory() instead. See https://www.drupal.org/node/2501187.', E_USER_DEPRECATED);
  547. // The config directory must be defined in settings.php.
  548. global $config_directories;
  549. if (!isset($config_directories[$type])) {
  550. return FALSE;
  551. }
  552. // The logic here is similar to that used by system_requirements() for other
  553. // directories that the installer creates.
  554. else {
  555. $config_directory = config_get_config_directory($type);
  556. return \Drupal::service('file_system')->prepareDirectory($config_directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
  557. }
  558. }
  559. /**
  560. * Verifies that all dependencies are met for a given installation profile.
  561. *
  562. * @param $install_state
  563. * An array of information about the current installation state.
  564. *
  565. * @return
  566. * The list of modules to install.
  567. */
  568. function drupal_verify_profile($install_state) {
  569. $profile = $install_state['parameters']['profile'];
  570. $info = $install_state['profile_info'];
  571. // Get the list of available modules for the selected installation profile.
  572. $listing = new ExtensionDiscovery(\Drupal::root());
  573. $present_modules = [];
  574. foreach ($listing->scan('module') as $present_module) {
  575. $present_modules[] = $present_module->getName();
  576. }
  577. // The installation profile is also a module, which needs to be installed
  578. // after all the other dependencies have been installed.
  579. $present_modules[] = $profile;
  580. // Verify that all of the profile's required modules are present.
  581. $missing_modules = array_diff($info['install'], $present_modules);
  582. $requirements = [];
  583. if ($missing_modules) {
  584. $build = [
  585. '#theme' => 'item_list',
  586. '#context' => ['list_style' => 'comma-list'],
  587. ];
  588. foreach ($missing_modules as $module) {
  589. $build['#items'][] = ['#markup' => '<span class="admin-missing">' . Unicode::ucfirst($module) . '</span>'];
  590. }
  591. $modules_list = \Drupal::service('renderer')->renderPlain($build);
  592. $requirements['required_modules'] = [
  593. 'title' => t('Required modules'),
  594. 'value' => t('Required modules not found.'),
  595. 'severity' => REQUIREMENT_ERROR,
  596. 'description' => t('The following modules are required but were not found. Move them into the appropriate modules subdirectory, such as <em>/modules</em>. Missing modules: @modules', ['@modules' => $modules_list]),
  597. ];
  598. }
  599. return $requirements;
  600. }
  601. /**
  602. * Installs the system module.
  603. *
  604. * Separated from the installation of other modules so core system
  605. * functions can be made available while other modules are installed.
  606. *
  607. * @param array $install_state
  608. * An array of information about the current installation state. This is used
  609. * to set the default language.
  610. */
  611. function drupal_install_system($install_state) {
  612. // Remove the service provider of the early installer.
  613. unset($GLOBALS['conf']['container_service_providers']['InstallerServiceProvider']);
  614. // Add the normal installer service provider.
  615. $GLOBALS['conf']['container_service_providers']['InstallerServiceProvider'] = 'Drupal\Core\Installer\NormalInstallerServiceProvider';
  616. // Get the existing request.
  617. $request = \Drupal::request();
  618. // Reboot into a full production environment to continue the installation.
  619. /** @var \Drupal\Core\Installer\InstallerKernel $kernel */
  620. $kernel = \Drupal::service('kernel');
  621. $kernel->shutdown();
  622. // Have installer rebuild from the disk, rather then building from scratch.
  623. $kernel->rebuildContainer(FALSE);
  624. // Reboot the kernel with new container.
  625. $kernel->boot();
  626. $kernel->preHandle($request);
  627. // Ensure our request includes the session if appropriate.
  628. if (PHP_SAPI !== 'cli') {
  629. $request->setSession($kernel->getContainer()->get('session'));
  630. }
  631. // Before having installed the system module and being able to do a module
  632. // rebuild, prime the \Drupal\Core\Extension\ModuleExtensionList static cache
  633. // with the module's location.
  634. // @todo Try to install system as any other module, see
  635. // https://www.drupal.org/node/2719315.
  636. \Drupal::service('extension.list.module')->setPathname('system', 'core/modules/system/system.info.yml');
  637. // Install base system configuration.
  638. \Drupal::service('config.installer')->installDefaultConfig('core', 'core');
  639. // Store the installation profile in configuration to populate the
  640. // 'install_profile' container parameter.
  641. \Drupal::configFactory()->getEditable('core.extension')
  642. ->set('profile', $install_state['parameters']['profile'])
  643. ->save();
  644. // Install System module and rebuild the newly available routes.
  645. $kernel->getContainer()->get('module_installer')->install(['system'], FALSE);
  646. \Drupal::service('router.builder')->rebuild();
  647. // Ensure default language is saved.
  648. if (isset($install_state['parameters']['langcode'])) {
  649. \Drupal::configFactory()->getEditable('system.site')
  650. ->set('langcode', (string) $install_state['parameters']['langcode'])
  651. ->set('default_langcode', (string) $install_state['parameters']['langcode'])
  652. ->save(TRUE);
  653. }
  654. }
  655. /**
  656. * Verifies the state of the specified file.
  657. *
  658. * @param $file
  659. * The file to check for.
  660. * @param $mask
  661. * An optional bitmask created from various FILE_* constants.
  662. * @param $type
  663. * The type of file. Can be file (default), dir, or link.
  664. * @param bool $autofix
  665. * (optional) Determines whether to attempt fixing the permissions according
  666. * to the provided $mask. Defaults to TRUE.
  667. *
  668. * @return
  669. * TRUE on success or FALSE on failure. A message is set for the latter.
  670. */
  671. function drupal_verify_install_file($file, $mask = NULL, $type = 'file', $autofix = TRUE) {
  672. $return = TRUE;
  673. // Check for files that shouldn't be there.
  674. if (isset($mask) && ($mask & FILE_NOT_EXIST) && file_exists($file)) {
  675. return FALSE;
  676. }
  677. // Verify that the file is the type of file it is supposed to be.
  678. if (isset($type) && file_exists($file)) {
  679. $check = 'is_' . $type;
  680. if (!function_exists($check) || !$check($file)) {
  681. $return = FALSE;
  682. }
  683. }
  684. // Verify file permissions.
  685. if (isset($mask)) {
  686. $masks = [FILE_EXIST, FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE];
  687. foreach ($masks as $current_mask) {
  688. if ($mask & $current_mask) {
  689. switch ($current_mask) {
  690. case FILE_EXIST:
  691. if (!file_exists($file)) {
  692. if ($type == 'dir' && $autofix) {
  693. drupal_install_mkdir($file, $mask);
  694. }
  695. if (!file_exists($file)) {
  696. $return = FALSE;
  697. }
  698. }
  699. break;
  700. case FILE_READABLE:
  701. if (!is_readable($file)) {
  702. $return = FALSE;
  703. }
  704. break;
  705. case FILE_WRITABLE:
  706. if (!is_writable($file)) {
  707. $return = FALSE;
  708. }
  709. break;
  710. case FILE_EXECUTABLE:
  711. if (!is_executable($file)) {
  712. $return = FALSE;
  713. }
  714. break;
  715. case FILE_NOT_READABLE:
  716. if (is_readable($file)) {
  717. $return = FALSE;
  718. }
  719. break;
  720. case FILE_NOT_WRITABLE:
  721. if (is_writable($file)) {
  722. $return = FALSE;
  723. }
  724. break;
  725. case FILE_NOT_EXECUTABLE:
  726. if (is_executable($file)) {
  727. $return = FALSE;
  728. }
  729. break;
  730. }
  731. }
  732. }
  733. }
  734. if (!$return && $autofix) {
  735. return drupal_install_fix_file($file, $mask);
  736. }
  737. return $return;
  738. }
  739. /**
  740. * Creates a directory with the specified permissions.
  741. *
  742. * @param $file
  743. * The name of the directory to create;
  744. * @param $mask
  745. * The permissions of the directory to create.
  746. * @param $message
  747. * (optional) Whether to output messages. Defaults to TRUE.
  748. *
  749. * @return
  750. * TRUE/FALSE whether or not the directory was successfully created.
  751. */
  752. function drupal_install_mkdir($file, $mask, $message = TRUE) {
  753. $mod = 0;
  754. $masks = [FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE];
  755. foreach ($masks as $m) {
  756. if ($mask & $m) {
  757. switch ($m) {
  758. case FILE_READABLE:
  759. $mod |= 0444;
  760. break;
  761. case FILE_WRITABLE:
  762. $mod |= 0222;
  763. break;
  764. case FILE_EXECUTABLE:
  765. $mod |= 0111;
  766. break;
  767. }
  768. }
  769. }
  770. if (@\Drupal::service('file_system')->mkdir($file, $mod)) {
  771. return TRUE;
  772. }
  773. else {
  774. return FALSE;
  775. }
  776. }
  777. /**
  778. * Attempts to fix file permissions.
  779. *
  780. * The general approach here is that, because we do not know the security
  781. * setup of the webserver, we apply our permission changes to all three
  782. * digits of the file permission (i.e. user, group and all).
  783. *
  784. * To ensure that the values behave as expected (and numbers don't carry
  785. * from one digit to the next) we do the calculation on the octal value
  786. * using bitwise operations. This lets us remove, for example, 0222 from
  787. * 0700 and get the correct value of 0500.
  788. *
  789. * @param $file
  790. * The name of the file with permissions to fix.
  791. * @param $mask
  792. * The desired permissions for the file.
  793. * @param $message
  794. * (optional) Whether to output messages. Defaults to TRUE.
  795. *
  796. * @return
  797. * TRUE/FALSE whether or not we were able to fix the file's permissions.
  798. */
  799. function drupal_install_fix_file($file, $mask, $message = TRUE) {
  800. // If $file does not exist, fileperms() issues a PHP warning.
  801. if (!file_exists($file)) {
  802. return FALSE;
  803. }
  804. $mod = fileperms($file) & 0777;
  805. $masks = [FILE_READABLE, FILE_WRITABLE, FILE_EXECUTABLE, FILE_NOT_READABLE, FILE_NOT_WRITABLE, FILE_NOT_EXECUTABLE];
  806. // FILE_READABLE, FILE_WRITABLE, and FILE_EXECUTABLE permission strings
  807. // can theoretically be 0400, 0200, and 0100 respectively, but to be safe
  808. // we set all three access types in case the administrator intends to
  809. // change the owner of settings.php after installation.
  810. foreach ($masks as $m) {
  811. if ($mask & $m) {
  812. switch ($m) {
  813. case FILE_READABLE:
  814. if (!is_readable($file)) {
  815. $mod |= 0444;
  816. }
  817. break;
  818. case FILE_WRITABLE:
  819. if (!is_writable($file)) {
  820. $mod |= 0222;
  821. }
  822. break;
  823. case FILE_EXECUTABLE:
  824. if (!is_executable($file)) {
  825. $mod |= 0111;
  826. }
  827. break;
  828. case FILE_NOT_READABLE:
  829. if (is_readable($file)) {
  830. $mod &= ~0444;
  831. }
  832. break;
  833. case FILE_NOT_WRITABLE:
  834. if (is_writable($file)) {
  835. $mod &= ~0222;
  836. }
  837. break;
  838. case FILE_NOT_EXECUTABLE:
  839. if (is_executable($file)) {
  840. $mod &= ~0111;
  841. }
  842. break;
  843. }
  844. }
  845. }
  846. // chmod() will work if the web server is running as owner of the file.
  847. if (@chmod($file, $mod)) {
  848. return TRUE;
  849. }
  850. else {
  851. return FALSE;
  852. }
  853. }
  854. /**
  855. * Sends the user to a different installer page.
  856. *
  857. * This issues an on-site HTTP redirect. Messages (and errors) are erased.
  858. *
  859. * @param $path
  860. * An installer path.
  861. */
  862. function install_goto($path) {
  863. global $base_url;
  864. $headers = [
  865. // Not a permanent redirect.
  866. 'Cache-Control' => 'no-cache',
  867. ];
  868. $response = new RedirectResponse($base_url . '/' . $path, 302, $headers);
  869. $response->send();
  870. }
  871. /**
  872. * Returns the URL of the current script, with modified query parameters.
  873. *
  874. * This function can be called by low-level scripts (such as install.php and
  875. * update.php) and returns the URL of the current script. Existing query
  876. * parameters are preserved by default, but new ones can optionally be merged
  877. * in.
  878. *
  879. * This function is used when the script must maintain certain query parameters
  880. * over multiple page requests in order to work correctly. In such cases (for
  881. * example, update.php, which requires the 'continue=1' parameter to remain in
  882. * the URL throughout the update process if there are any requirement warnings
  883. * that need to be bypassed), using this function to generate the URL for links
  884. * to the next steps of the script ensures that the links will work correctly.
  885. *
  886. * @param $query
  887. * (optional) An array of query parameters to merge in to the existing ones.
  888. *
  889. * @return
  890. * The URL of the current script, with query parameters modified by the
  891. * passed-in $query. The URL is not sanitized, so it still needs to be run
  892. * through \Drupal\Component\Utility\UrlHelper::filterBadProtocol() if it will be
  893. * used as an HTML attribute value.
  894. *
  895. * @see drupal_requirements_url()
  896. * @see Drupal\Component\Utility\UrlHelper::filterBadProtocol()
  897. */
  898. function drupal_current_script_url($query = []) {
  899. $uri = $_SERVER['SCRIPT_NAME'];
  900. $query = array_merge(UrlHelper::filterQueryParameters(\Drupal::request()->query->all()), $query);
  901. if (!empty($query)) {
  902. $uri .= '?' . UrlHelper::buildQuery($query);
  903. }
  904. return $uri;
  905. }
  906. /**
  907. * Returns a URL for proceeding to the next page after a requirements problem.
  908. *
  909. * This function can be called by low-level scripts (such as install.php and
  910. * update.php) and returns a URL that can be used to attempt to proceed to the
  911. * next step of the script.
  912. *
  913. * @param $severity
  914. * The severity of the requirements problem, as returned by
  915. * drupal_requirements_severity().
  916. *
  917. * @return
  918. * A URL for attempting to proceed to the next step of the script. The URL is
  919. * not sanitized, so it still needs to be run through
  920. * \Drupal\Component\Utility\UrlHelper::filterBadProtocol() if it will be used
  921. * as an HTML attribute value.
  922. *
  923. * @see drupal_current_script_url()
  924. * @see \Drupal\Component\Utility\UrlHelper::filterBadProtocol()
  925. */
  926. function drupal_requirements_url($severity) {
  927. $query = [];
  928. // If there are no errors, only warnings, append 'continue=1' to the URL so
  929. // the user can bypass this screen on the next page load.
  930. if ($severity == REQUIREMENT_WARNING) {
  931. $query['continue'] = 1;
  932. }
  933. return drupal_current_script_url($query);
  934. }
  935. /**
  936. * Checks an installation profile's requirements.
  937. *
  938. * @param string $profile
  939. * Name of installation profile to check.
  940. *
  941. * @return array
  942. * Array of the installation profile's requirements.
  943. */
  944. function drupal_check_profile($profile) {
  945. $info = install_profile_info($profile);
  946. // Collect requirement testing results.
  947. $requirements = [];
  948. // Performs an ExtensionDiscovery scan as the system module is unavailable and
  949. // we don't yet know where all the modules are located.
  950. // @todo Remove as part of https://www.drupal.org/node/2186491
  951. $drupal_root = \Drupal::root();
  952. $module_list = (new ExtensionDiscovery($drupal_root))->scan('module');
  953. foreach ($info['install'] as $module) {
  954. // If the module is in the module list we know it exists and we can continue
  955. // including and registering it.
  956. // @see \Drupal\Core\Extension\ExtensionDiscovery::scanDirectory()
  957. if (isset($module_list[$module])) {
  958. $function = $module . '_requirements';
  959. $module_path = $module_list[$module]->getPath();
  960. $install_file = "$drupal_root/$module_path/$module.install";
  961. if (is_file($install_file)) {
  962. require_once $install_file;
  963. }
  964. \Drupal::service('class_loader')->addPsr4('Drupal\\' . $module . '\\', \Drupal::root() . "/$module_path/src");
  965. if (function_exists($function)) {
  966. $requirements = array_merge($requirements, $function('install'));
  967. }
  968. }
  969. }
  970. // Add the profile requirements.
  971. $function = $profile . '_requirements';
  972. if (function_exists($function)) {
  973. $requirements = array_merge($requirements, $function('install'));
  974. }
  975. return $requirements;
  976. }
  977. /**
  978. * Extracts the highest severity from the requirements array.
  979. *
  980. * @param $requirements
  981. * An array of requirements, in the same format as is returned by
  982. * hook_requirements().
  983. *
  984. * @return
  985. * The highest severity in the array.
  986. */
  987. function drupal_requirements_severity(&$requirements) {
  988. $severity = REQUIREMENT_OK;
  989. foreach ($requirements as $requirement) {
  990. if (isset($requirement['severity'])) {
  991. $severity = max($severity, $requirement['severity']);
  992. }
  993. }
  994. return $severity;
  995. }
  996. /**
  997. * Checks a module's requirements.
  998. *
  999. * @param $module
  1000. * Machine name of module to check.
  1001. *
  1002. * @return
  1003. * TRUE or FALSE, depending on whether the requirements are met.
  1004. */
  1005. function drupal_check_module($module) {
  1006. module_load_install($module);
  1007. // Check requirements
  1008. $requirements = \Drupal::moduleHandler()->invoke($module, 'requirements', ['install']);
  1009. if (is_array($requirements) && drupal_requirements_severity($requirements) == REQUIREMENT_ERROR) {
  1010. // Print any error messages
  1011. foreach ($requirements as $requirement) {
  1012. if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) {
  1013. $message = $requirement['description'];
  1014. if (isset($requirement['value']) && $requirement['value']) {
  1015. $message = t('@requirements_message (Currently using @item version @version)', ['@requirements_message' => $requirement['description'], '@item' => $requirement['title'], '@version' => $requirement['value']]);
  1016. }
  1017. \Drupal::messenger()->addError($message);
  1018. }
  1019. }
  1020. return FALSE;
  1021. }
  1022. return TRUE;
  1023. }
  1024. /**
  1025. * Retrieves information about an installation profile from its .info.yml file.
  1026. *
  1027. * The information stored in a profile .info.yml file is similar to that stored
  1028. * in a normal Drupal module .info.yml file. For example:
  1029. * - name: The real name of the installation profile for display purposes.
  1030. * - description: A brief description of the profile.
  1031. * - dependencies: An array of shortnames of other modules that this install
  1032. * profile requires.
  1033. * - install: An array of shortname of other modules to install that are not
  1034. * required by this install profile.
  1035. *
  1036. * Additional, less commonly-used information that can appear in a
  1037. * profile.info.yml file but not in a normal Drupal module .info.yml file
  1038. * includes:
  1039. *
  1040. * - distribution: Existence of this key denotes that the installation profile
  1041. * is intended to be the only eligible choice in a distribution and will be
  1042. * auto-selected during installation, whereas the installation profile
  1043. * selection screen will be skipped. If more than one distribution profile is
  1044. * found then the first one discovered will be selected.
  1045. * The following subproperties may be set:
  1046. * - name: The name of the distribution that is being installed, to be shown
  1047. * throughout the installation process. If omitted,
  1048. * drupal_install_profile_distribution_name() defaults to 'Drupal'.
  1049. * - install: Optional parameters to override the installer:
  1050. * - theme: The machine name of a theme to use in the installer instead of
  1051. * Drupal's default installer theme.
  1052. * - finish_url: A destination to visit after the installation of the
  1053. * distribution is finished
  1054. *
  1055. * Note that this function does an expensive file system scan to get info file
  1056. * information for dependencies. If you only need information from the info
  1057. * file itself, use
  1058. * \Drupal::service('extension.list.profile')->getExtensionInfo().
  1059. *
  1060. * Example of .info.yml file:
  1061. * @code
  1062. * name: Minimal
  1063. * description: Start fresh, with only a few modules enabled.
  1064. * install:
  1065. * - block
  1066. * - dblog
  1067. * @endcode
  1068. *
  1069. * @param $profile
  1070. * Name of profile.
  1071. * @param $langcode
  1072. * Language code (if any).
  1073. *
  1074. * @return
  1075. * The info array.
  1076. */
  1077. function install_profile_info($profile, $langcode = 'en') {
  1078. static $cache = [];
  1079. if (!isset($cache[$profile][$langcode])) {
  1080. // Set defaults for module info.
  1081. $defaults = [
  1082. 'dependencies' => [],
  1083. 'install' => [],
  1084. 'themes' => ['stark'],
  1085. 'description' => '',
  1086. 'version' => NULL,
  1087. 'hidden' => FALSE,
  1088. 'php' => DRUPAL_MINIMUM_PHP,
  1089. 'config_install_path' => NULL,
  1090. ];
  1091. $profile_path = drupal_get_path('profile', $profile);
  1092. $info = \Drupal::service('info_parser')->parse("$profile_path/$profile.info.yml");
  1093. $info += $defaults;
  1094. $dependency_name_function = function ($dependency) {
  1095. return Dependency::createFromString($dependency)->getName();
  1096. };
  1097. // Convert dependencies in [project:module] format.
  1098. $info['dependencies'] = array_map($dependency_name_function, $info['dependencies']);
  1099. // Convert install key in [project:module] format.
  1100. $info['install'] = array_map($dependency_name_function, $info['install']);
  1101. // drupal_required_modules() includes the current profile as a dependency.
  1102. // Remove that dependency, since a module cannot depend on itself.
  1103. $required = array_diff(drupal_required_modules(), [$profile]);
  1104. $locale = !empty($langcode) && $langcode != 'en' ? ['locale'] : [];
  1105. // Merge dependencies, required modules and locale into install list and
  1106. // remove any duplicates.
  1107. $info['install'] = array_unique(array_merge($info['install'], $required, $info['dependencies'], $locale));
  1108. // If the profile has a config/sync directory use that to install drupal.
  1109. if (is_dir($profile_path . '/config/sync')) {
  1110. $info['config_install_path'] = $profile_path . '/config/sync';
  1111. }
  1112. $cache[$profile][$langcode] = $info;
  1113. }
  1114. return $cache[$profile][$langcode];
  1115. }
  1116. /**
  1117. * Returns a database installer object.
  1118. *
  1119. * Before calling this function it is important the database installer object
  1120. * is autoloadable. Database drivers provided by contributed modules are added
  1121. * to the autoloader in drupal_get_database_types() and Settings::initialize().
  1122. *
  1123. * @param $driver
  1124. * The name of the driver.
  1125. * @param string $namespace
  1126. * (optional) The database driver namespace.
  1127. *
  1128. * @return \Drupal\Core\Database\Install\Tasks
  1129. * A class defining the requirements and tasks for installing the database.
  1130. *
  1131. * @see drupal_get_database_types()
  1132. * @see \Drupal\Core\Site\Settings::initialize()
  1133. */
  1134. function db_installer_object($driver, $namespace = NULL) {
  1135. // We cannot use Database::getConnection->getDriverClass() here, because
  1136. // the connection object is not yet functional.
  1137. if ($namespace) {
  1138. $task_class = $namespace . "\\Install\\Tasks";
  1139. return new $task_class();
  1140. }
  1141. // Old Drupal 8 style contrib namespace.
  1142. $task_class = "Drupal\\Driver\\Database\\{$driver}\\Install\\Tasks";
  1143. if (class_exists($task_class)) {
  1144. return new $task_class();
  1145. }
  1146. else {
  1147. // Core provided driver.
  1148. $task_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\Install\\Tasks";
  1149. return new $task_class();
  1150. }
  1151. }