update.inc 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786
  1. <?php
  2. /**
  3. * @file
  4. * Drupal database update API.
  5. *
  6. * This file contains functions to perform database updates for a Drupal
  7. * installation. It is included and used extensively by update.php.
  8. */
  9. use Drupal\Component\Graph\Graph;
  10. use Drupal\Core\Extension\Exception\UnknownExtensionException;
  11. use Drupal\Core\Update\UpdateKernel;
  12. use Drupal\Core\Utility\Error;
  13. /**
  14. * Disables any extensions that are incompatible with the current core version.
  15. *
  16. * @deprecated in Drupal 8.8.5 and is removed from Drupal 9.0.0.
  17. *
  18. * @see https://www.drupal.org/node/3026100
  19. */
  20. function update_fix_compatibility() {
  21. @trigger_error(__FUNCTION__ . '() is deprecated in Drupal 8.8.5 and will be removed before Drupal 9.0.0. There is no replacement. See https://www.drupal.org/node/3026100', E_USER_DEPRECATED);
  22. // Fix extension objects if the update is being done via Drush 8. In non-Drush
  23. // environments this will already be fixed by the UpdateKernel this point.
  24. UpdateKernel::fixSerializedExtensionObjects(\Drupal::getContainer());
  25. $extension_config = \Drupal::configFactory()->getEditable('core.extension');
  26. $save = FALSE;
  27. foreach (['module', 'theme'] as $type) {
  28. foreach ($extension_config->get($type) as $name => $weight) {
  29. if (update_check_incompatibility($name, $type)) {
  30. $extension_config->clear("$type.$name");
  31. $save = TRUE;
  32. }
  33. }
  34. }
  35. if ($save) {
  36. $extension_config->set('module', module_config_sort($extension_config->get('module')));
  37. $extension_config->save();
  38. }
  39. }
  40. /**
  41. * Tests the compatibility of a module or theme.
  42. */
  43. function update_check_incompatibility($name, $type = 'module') {
  44. static $themes, $modules;
  45. // Store values of expensive functions for future use.
  46. if (empty($themes) || empty($modules)) {
  47. // We need to do a full rebuild here to make sure the database reflects any
  48. // code changes that were made in the filesystem before the update script
  49. // was initiated.
  50. $themes = \Drupal::service('theme_handler')->rebuildThemeData();
  51. $modules = \Drupal::service('extension.list.module')->reset()->getList();
  52. }
  53. if ($type == 'module' && isset($modules[$name])) {
  54. $file = $modules[$name];
  55. }
  56. elseif ($type == 'theme' && isset($themes[$name])) {
  57. $file = $themes[$name];
  58. }
  59. if (!isset($file)
  60. || $file->info['core_incompatible']
  61. || version_compare(phpversion(), $file->info['php']) < 0) {
  62. return TRUE;
  63. }
  64. return FALSE;
  65. }
  66. /**
  67. * Returns whether the minimum schema requirement has been satisfied.
  68. *
  69. * @return array
  70. * A requirements info array.
  71. */
  72. function update_system_schema_requirements() {
  73. $requirements = [];
  74. $system_schema = drupal_get_installed_schema_version('system');
  75. $requirements['minimum schema']['title'] = 'Minimum schema version';
  76. if ($system_schema >= \Drupal::CORE_MINIMUM_SCHEMA_VERSION) {
  77. $requirements['minimum schema'] += [
  78. 'value' => 'The installed schema version meets the minimum.',
  79. 'description' => 'Schema version: ' . $system_schema,
  80. ];
  81. }
  82. else {
  83. $requirements['minimum schema'] += [
  84. 'value' => 'The installed schema version does not meet the minimum.',
  85. 'severity' => REQUIREMENT_ERROR,
  86. 'description' => 'Your system schema version is ' . $system_schema . '. Updating directly from a schema version prior to 8000 is not supported. You must upgrade your site to Drupal 8 first, see https://www.drupal.org/docs/8/upgrade.',
  87. ];
  88. }
  89. return $requirements;
  90. }
  91. /**
  92. * Checks update requirements and reports errors and (optionally) warnings.
  93. */
  94. function update_check_requirements() {
  95. // Because this is one of the earliest points in the update process,
  96. // detect and fix missing schema versions for modules here to ensure
  97. // it runs on all update code paths.
  98. _update_fix_missing_schema();
  99. // Check requirements of all loaded modules.
  100. $requirements = \Drupal::moduleHandler()->invokeAll('requirements', ['update']);
  101. $requirements += update_system_schema_requirements();
  102. return $requirements;
  103. }
  104. /**
  105. * Helper to detect and fix 'missing' schema information.
  106. *
  107. * Repairs the case where a module has no schema version recorded.
  108. * This has to be done prior to updates being run, otherwise the update
  109. * system would detect and attempt to run all historical updates for a
  110. * module.
  111. *
  112. * @todo: remove in a major version after
  113. * https://www.drupal.org/project/drupal/issues/3130037 has been fixed.
  114. */
  115. function _update_fix_missing_schema() {
  116. $versions = \Drupal::keyValue('system.schema')->getAll();
  117. $module_handler = \Drupal::moduleHandler();
  118. $enabled_modules = $module_handler->getModuleList();
  119. foreach (array_keys($enabled_modules) as $module) {
  120. // All modules should have a recorded schema version, but when they
  121. // don't, detect and fix the problem.
  122. if (!isset($versions[$module])) {
  123. // Ensure the .install file is loaded.
  124. module_load_install($module);
  125. $all_updates = drupal_get_schema_versions($module);
  126. // If the schema version of a module hasn't been recorded, we cannot
  127. // know the actual schema version a module is at, because
  128. // no updates will ever have been run on the site and it was not set
  129. // correctly when the module was installed, so instead set it to
  130. // the same as the last update. This means that updates will proceed
  131. // again the next time the module is updated and a new update is
  132. // added. Updates added in between the module being installed and the
  133. // schema version being fixed here (if any have been added) will never
  134. // be run, but we have no way to identify which updates these are.
  135. if ($all_updates) {
  136. $last_update = max($all_updates);
  137. }
  138. else {
  139. $last_update = \Drupal::CORE_MINIMUM_SCHEMA_VERSION;
  140. }
  141. // If the module implements hook_update_last_removed() use the
  142. // value of that if it's higher than the schema versions found so
  143. // far.
  144. if ($last_removed = $module_handler->invoke($module, 'update_last_removed')) {
  145. $last_update = max($last_update, $last_removed);
  146. }
  147. drupal_set_installed_schema_version($module, $last_update);
  148. $args = ['%module' => $module, '%last_update_hook' => $module . '_update_' . $last_update . '()'];
  149. \Drupal::messenger()->addWarning(t('Schema information for module %module was missing from the database. You should manually review the module updates and your database to check if any updates have been skipped up to, and including, %last_update_hook.', $args));
  150. \Drupal::logger('update')->warning('Schema information for module %module was missing from the database. You should manually review the module updates and your database to check if any updates have been skipped up to, and including, %last_update_hook.', $args);
  151. }
  152. }
  153. }
  154. /**
  155. * Forces a module to a given schema version.
  156. *
  157. * This function is rarely necessary.
  158. *
  159. * @param string $module
  160. * Name of the module.
  161. * @param string $schema_version
  162. * The schema version the module should be set to.
  163. */
  164. function update_set_schema($module, $schema_version) {
  165. \Drupal::keyValue('system.schema')->set($module, $schema_version);
  166. \Drupal::service('extension.list.profile')->reset();
  167. \Drupal::service('extension.list.module')->reset();
  168. \Drupal::service('extension.list.theme_engine')->reset();
  169. \Drupal::service('extension.list.theme')->reset();
  170. drupal_static_reset('drupal_get_installed_schema_version');
  171. }
  172. /**
  173. * Implements callback_batch_operation().
  174. *
  175. * Performs one update and stores the results for display on the results page.
  176. *
  177. * If an update function completes successfully, it should return a message
  178. * as a string indicating success, for example:
  179. * @code
  180. * return t('New index added successfully.');
  181. * @endcode
  182. *
  183. * Alternatively, it may return nothing. In that case, no message
  184. * will be displayed at all.
  185. *
  186. * If it fails for whatever reason, it should throw an instance of
  187. * Drupal\Core\Utility\UpdateException with an appropriate error message, for
  188. * example:
  189. * @code
  190. * use Drupal\Core\Utility\UpdateException;
  191. * throw new UpdateException('Description of what went wrong');
  192. * @endcode
  193. *
  194. * If an exception is thrown, the current update and all updates that depend on
  195. * it will be aborted. The schema version will not be updated in this case, and
  196. * all the aborted updates will continue to appear on update.php as updates
  197. * that have not yet been run.
  198. *
  199. * If an update function needs to be re-run as part of a batch process, it
  200. * should accept the $sandbox array by reference as its first parameter
  201. * and set the #finished property to the percentage completed that it is, as a
  202. * fraction of 1.
  203. *
  204. * @param $module
  205. * The module whose update will be run.
  206. * @param $number
  207. * The update number to run.
  208. * @param $dependency_map
  209. * An array whose keys are the names of all update functions that will be
  210. * performed during this batch process, and whose values are arrays of other
  211. * update functions that each one depends on.
  212. * @param $context
  213. * The batch context array.
  214. *
  215. * @see update_resolve_dependencies()
  216. */
  217. function update_do_one($module, $number, $dependency_map, &$context) {
  218. $function = $module . '_update_' . $number;
  219. // If this update was aborted in a previous step, or has a dependency that
  220. // was aborted in a previous step, go no further.
  221. if (!empty($context['results']['#abort']) && array_intersect($context['results']['#abort'], array_merge($dependency_map, [$function]))) {
  222. return;
  223. }
  224. $ret = [];
  225. if (function_exists($function)) {
  226. try {
  227. $ret['results']['query'] = $function($context['sandbox']);
  228. $ret['results']['success'] = TRUE;
  229. }
  230. // @TODO We may want to do different error handling for different
  231. // exception types, but for now we'll just log the exception and
  232. // return the message for printing.
  233. // @see https://www.drupal.org/node/2564311
  234. catch (Exception $e) {
  235. watchdog_exception('update', $e);
  236. $variables = Error::decodeException($e);
  237. unset($variables['backtrace']);
  238. $ret['#abort'] = ['success' => FALSE, 'query' => t('%type: @message in %function (line %line of %file).', $variables)];
  239. }
  240. }
  241. if (isset($context['sandbox']['#finished'])) {
  242. $context['finished'] = $context['sandbox']['#finished'];
  243. unset($context['sandbox']['#finished']);
  244. }
  245. if (!isset($context['results'][$module])) {
  246. $context['results'][$module] = [];
  247. }
  248. if (!isset($context['results'][$module][$number])) {
  249. $context['results'][$module][$number] = [];
  250. }
  251. $context['results'][$module][$number] = array_merge($context['results'][$module][$number], $ret);
  252. if (!empty($ret['#abort'])) {
  253. // Record this function in the list of updates that were aborted.
  254. $context['results']['#abort'][] = $function;
  255. }
  256. // Record the schema update if it was completed successfully.
  257. if ($context['finished'] == 1 && empty($ret['#abort'])) {
  258. drupal_set_installed_schema_version($module, $number);
  259. }
  260. $context['message'] = t('Updating @module', ['@module' => $module]);
  261. }
  262. /**
  263. * Executes a single hook_post_update_NAME().
  264. *
  265. * @param string $function
  266. * The function name, that should be executed.
  267. * @param array $context
  268. * The batch context array.
  269. */
  270. function update_invoke_post_update($function, &$context) {
  271. $ret = [];
  272. // If this update was aborted in a previous step, or has a dependency that was
  273. // aborted in a previous step, go no further.
  274. if (!empty($context['results']['#abort'])) {
  275. return;
  276. }
  277. list($module, $name) = explode('_post_update_', $function, 2);
  278. module_load_include('php', $module, $module . '.post_update');
  279. if (function_exists($function)) {
  280. try {
  281. $ret['results']['query'] = $function($context['sandbox']);
  282. $ret['results']['success'] = TRUE;
  283. if (!isset($context['sandbox']['#finished']) || (isset($context['sandbox']['#finished']) && $context['sandbox']['#finished'] >= 1)) {
  284. \Drupal::service('update.post_update_registry')->registerInvokedUpdates([$function]);
  285. }
  286. }
  287. // @TODO We may want to do different error handling for different exception
  288. // types, but for now we'll just log the exception and return the message
  289. // for printing.
  290. // @see https://www.drupal.org/node/2564311
  291. catch (Exception $e) {
  292. watchdog_exception('update', $e);
  293. $variables = Error::decodeException($e);
  294. unset($variables['backtrace']);
  295. $ret['#abort'] = [
  296. 'success' => FALSE,
  297. 'query' => t('%type: @message in %function (line %line of %file).', $variables),
  298. ];
  299. }
  300. }
  301. if (isset($context['sandbox']['#finished'])) {
  302. $context['finished'] = $context['sandbox']['#finished'];
  303. unset($context['sandbox']['#finished']);
  304. }
  305. if (!isset($context['results'][$module][$name])) {
  306. $context['results'][$module][$name] = [];
  307. }
  308. $context['results'][$module][$name] = array_merge($context['results'][$module][$name], $ret);
  309. if (!empty($ret['#abort'])) {
  310. // Record this function in the list of updates that were aborted.
  311. $context['results']['#abort'][] = $function;
  312. }
  313. $context['message'] = t('Post updating @module', ['@module' => $module]);
  314. }
  315. /**
  316. * Returns a list of all the pending database updates.
  317. *
  318. * @return
  319. * An associative array keyed by module name which contains all information
  320. * about database updates that need to be run, and any updates that are not
  321. * going to proceed due to missing requirements. The system module will
  322. * always be listed first.
  323. *
  324. * The subarray for each module can contain the following keys:
  325. * - start: The starting update that is to be processed. If this does not
  326. * exist then do not process any updates for this module as there are
  327. * other requirements that need to be resolved.
  328. * - warning: Any warnings about why this module can not be updated.
  329. * - pending: An array of all the pending updates for the module including
  330. * the update number and the description from source code comment for
  331. * each update function. This array is keyed by the update number.
  332. */
  333. function update_get_update_list() {
  334. // Make sure that the system module is first in the list of updates.
  335. $ret = ['system' => []];
  336. $modules = drupal_get_installed_schema_version(NULL, FALSE, TRUE);
  337. /** @var \Drupal\Core\Extension\ExtensionList $extension_list */
  338. $extension_list = \Drupal::service('extension.list.module');
  339. /** @var array $installed_module_info */
  340. $installed_module_info = $extension_list->getAllInstalledInfo();
  341. foreach ($modules as $module => $schema_version) {
  342. // Skip uninstalled and incompatible modules.
  343. try {
  344. if ($schema_version == SCHEMA_UNINSTALLED || $extension_list->checkIncompatibility($module)) {
  345. continue;
  346. }
  347. }
  348. // It is possible that the system schema has orphaned entries, so the
  349. // incompatibility checking might throw an exception.
  350. catch (UnknownExtensionException $e) {
  351. $args = [
  352. '%name' => $module,
  353. ':url' => 'https://www.drupal.org/node/3137656',
  354. ];
  355. \Drupal::messenger()->addWarning(t('Module %name has an entry in the system.schema key/value storage, but is missing from your site. <a href=":url">More information about this error</a>.', $args));
  356. \Drupal::logger('system')->notice('Module %name has an entry in the system.schema key/value storage, but is missing from your site. <a href=":url">More information about this error</a>.', $args);
  357. continue;
  358. }
  359. // There might be orphaned entries for modules that are in the filesystem
  360. // but not installed. Also skip those, but warn site admins about it.
  361. if (empty($installed_module_info[$module])) {
  362. $args = [
  363. '%name' => $module,
  364. ':url' => 'https://www.drupal.org/node/3137656',
  365. ];
  366. \Drupal::messenger()->addWarning(t('Module %name has an entry in the system.schema key/value storage, but is not installed. <a href=":url">More information about this error</a>.', $args));
  367. \Drupal::logger('system')->notice('Module %name has an entry in the system.schema key/value storage, but is not installed. <a href=":url">More information about this error</a>.', $args);
  368. continue;
  369. }
  370. // Display a requirements error if the user somehow has a schema version
  371. // from the previous Drupal major version.
  372. if ($schema_version < \Drupal::CORE_MINIMUM_SCHEMA_VERSION) {
  373. $ret[$module]['warning'] = '<em>' . $module . '</em> module cannot be updated. Its schema version is ' . $schema_version . ', which is from an earlier major release of Drupal. You will need to <a href="https://www.drupal.org/node/2127611">migrate the data for this module</a> instead.';
  374. continue;
  375. }
  376. // Otherwise, get the list of updates defined by this module.
  377. $updates = drupal_get_schema_versions($module);
  378. if ($updates !== FALSE) {
  379. foreach ($updates as $update) {
  380. if ($update == \Drupal::CORE_MINIMUM_SCHEMA_VERSION) {
  381. $ret[$module]['warning'] = '<em>' . $module . '</em> module cannot be updated. It contains an update numbered as ' . \Drupal::CORE_MINIMUM_SCHEMA_VERSION . ' which is reserved for the earliest installation of a module in Drupal ' . \Drupal::CORE_COMPATIBILITY . ', before any updates. In order to update <em>' . $module . '</em> module, you will need to install a version of the module with valid updates.';
  382. continue 2;
  383. }
  384. if ($update > $schema_version) {
  385. // The description for an update comes from its Doxygen.
  386. $func = new ReflectionFunction($module . '_update_' . $update);
  387. $description = str_replace(["\n", '*', '/'], '', $func->getDocComment());
  388. $ret[$module]['pending'][$update] = "$update - $description";
  389. if (!isset($ret[$module]['start'])) {
  390. $ret[$module]['start'] = $update;
  391. }
  392. }
  393. }
  394. if (!isset($ret[$module]['start']) && isset($ret[$module]['pending'])) {
  395. $ret[$module]['start'] = $schema_version;
  396. }
  397. }
  398. }
  399. if (empty($ret['system'])) {
  400. unset($ret['system']);
  401. }
  402. return $ret;
  403. }
  404. /**
  405. * Resolves dependencies in a set of module updates, and orders them correctly.
  406. *
  407. * This function receives a list of requested module updates and determines an
  408. * appropriate order to run them in such that all update dependencies are met.
  409. * Any updates whose dependencies cannot be met are included in the returned
  410. * array but have the key 'allowed' set to FALSE; the calling function should
  411. * take responsibility for ensuring that these updates are ultimately not
  412. * performed.
  413. *
  414. * In addition, the returned array also includes detailed information about the
  415. * dependency chain for each update, as provided by the depth-first search
  416. * algorithm in Drupal\Component\Graph\Graph::searchAndSort().
  417. *
  418. * @param $starting_updates
  419. * An array whose keys contain the names of modules with updates to be run
  420. * and whose values contain the number of the first requested update for that
  421. * module.
  422. *
  423. * @return
  424. * An array whose keys are the names of all update functions within the
  425. * provided modules that would need to be run in order to fulfill the
  426. * request, arranged in the order in which the update functions should be
  427. * run. (This includes the provided starting update for each module and all
  428. * subsequent updates that are available.) The values are themselves arrays
  429. * containing all the keys provided by the
  430. * Drupal\Component\Graph\Graph::searchAndSort() algorithm, which encode
  431. * detailed information about the dependency chain for this update function
  432. * (for example: 'paths', 'reverse_paths', 'weight', and 'component'), as
  433. * well as the following additional keys:
  434. * - 'allowed': A boolean which is TRUE when the update function's
  435. * dependencies are met, and FALSE otherwise. Calling functions should
  436. * inspect this value before running the update.
  437. * - 'missing_dependencies': An array containing the names of any other
  438. * update functions that are required by this one but that are unavailable
  439. * to be run. This array will be empty when 'allowed' is TRUE.
  440. * - 'module': The name of the module that this update function belongs to.
  441. * - 'number': The number of this update function within that module.
  442. *
  443. * @see \Drupal\Component\Graph\Graph::searchAndSort()
  444. */
  445. function update_resolve_dependencies($starting_updates) {
  446. // Obtain a dependency graph for the requested update functions.
  447. $update_functions = update_get_update_function_list($starting_updates);
  448. $graph = update_build_dependency_graph($update_functions);
  449. // Perform the depth-first search and sort on the results.
  450. $graph_object = new Graph($graph);
  451. $graph = $graph_object->searchAndSort();
  452. uasort($graph, ['Drupal\Component\Utility\SortArray', 'sortByWeightElement']);
  453. foreach ($graph as $function => &$data) {
  454. $module = $data['module'];
  455. $number = $data['number'];
  456. // If the update function is missing and has not yet been performed, mark
  457. // it and everything that ultimately depends on it as disallowed.
  458. if (update_is_missing($module, $number, $update_functions) && !update_already_performed($module, $number)) {
  459. $data['allowed'] = FALSE;
  460. foreach (array_keys($data['paths']) as $dependent) {
  461. $graph[$dependent]['allowed'] = FALSE;
  462. $graph[$dependent]['missing_dependencies'][] = $function;
  463. }
  464. }
  465. elseif (!isset($data['allowed'])) {
  466. $data['allowed'] = TRUE;
  467. $data['missing_dependencies'] = [];
  468. }
  469. // Now that we have finished processing this function, remove it from the
  470. // graph if it was not part of the original list. This ensures that we
  471. // never try to run any updates that were not specifically requested.
  472. if (!isset($update_functions[$module][$number])) {
  473. unset($graph[$function]);
  474. }
  475. }
  476. return $graph;
  477. }
  478. /**
  479. * Returns an organized list of update functions for a set of modules.
  480. *
  481. * @param $starting_updates
  482. * An array whose keys contain the names of modules and whose values contain
  483. * the number of the first requested update for that module.
  484. *
  485. * @return
  486. * An array containing all the update functions that should be run for each
  487. * module, including the provided starting update and all subsequent updates
  488. * that are available. The keys of the array contain the module names, and
  489. * each value is an ordered array of update functions, keyed by the update
  490. * number.
  491. *
  492. * @see update_resolve_dependencies()
  493. */
  494. function update_get_update_function_list($starting_updates) {
  495. // Go through each module and find all updates that we need (including the
  496. // first update that was requested and any updates that run after it).
  497. $update_functions = [];
  498. foreach ($starting_updates as $module => $version) {
  499. $update_functions[$module] = [];
  500. $updates = drupal_get_schema_versions($module);
  501. if ($updates !== FALSE) {
  502. $max_version = max($updates);
  503. if ($version <= $max_version) {
  504. foreach ($updates as $update) {
  505. if ($update >= $version) {
  506. $update_functions[$module][$update] = $module . '_update_' . $update;
  507. }
  508. }
  509. }
  510. }
  511. }
  512. return $update_functions;
  513. }
  514. /**
  515. * Constructs a graph which encodes the dependencies between module updates.
  516. *
  517. * This function returns an associative array which contains a "directed graph"
  518. * representation of the dependencies between a provided list of update
  519. * functions, as well as any outside update functions that they directly depend
  520. * on but that were not in the provided list. The vertices of the graph
  521. * represent the update functions themselves, and each edge represents a
  522. * requirement that the first update function needs to run before the second.
  523. * For example, consider this graph:
  524. *
  525. * system_update_8001 ---> system_update_8002 ---> system_update_8003
  526. *
  527. * Visually, this indicates that system_update_8001() must run before
  528. * system_update_8002(), which in turn must run before system_update_8003().
  529. *
  530. * The function takes into account standard dependencies within each module, as
  531. * shown above (i.e., the fact that each module's updates must run in numerical
  532. * order), but also finds any cross-module dependencies that are defined by
  533. * modules which implement hook_update_dependencies(), and builds them into the
  534. * graph as well.
  535. *
  536. * @param $update_functions
  537. * An organized array of update functions, in the format returned by
  538. * update_get_update_function_list().
  539. *
  540. * @return
  541. * A multidimensional array representing the dependency graph, suitable for
  542. * passing in to Drupal\Component\Graph\Graph::searchAndSort(), but with extra
  543. * information about each update function also included. Each array key
  544. * contains the name of an update function, including all update functions
  545. * from the provided list as well as any outside update functions which they
  546. * directly depend on. Each value is an associative array containing the
  547. * following keys:
  548. * - 'edges': A representation of any other update functions that immediately
  549. * depend on this one. See Drupal\Component\Graph\Graph::searchAndSort() for
  550. * more details on the format.
  551. * - 'module': The name of the module that this update function belongs to.
  552. * - 'number': The number of this update function within that module.
  553. *
  554. * @see \Drupal\Component\Graph\Graph::searchAndSort()
  555. * @see update_resolve_dependencies()
  556. */
  557. function update_build_dependency_graph($update_functions) {
  558. // Initialize an array that will define a directed graph representing the
  559. // dependencies between update functions.
  560. $graph = [];
  561. // Go through each update function and build an initial list of dependencies.
  562. foreach ($update_functions as $module => $functions) {
  563. $previous_function = NULL;
  564. foreach ($functions as $number => $function) {
  565. // Add an edge to the directed graph representing the fact that each
  566. // update function in a given module must run after the update that
  567. // numerically precedes it.
  568. if ($previous_function) {
  569. $graph[$previous_function]['edges'][$function] = TRUE;
  570. }
  571. $previous_function = $function;
  572. // Define the module and update number associated with this function.
  573. $graph[$function]['module'] = $module;
  574. $graph[$function]['number'] = $number;
  575. }
  576. }
  577. // Now add any explicit update dependencies declared by modules.
  578. $update_dependencies = update_retrieve_dependencies();
  579. foreach ($graph as $function => $data) {
  580. if (!empty($update_dependencies[$data['module']][$data['number']])) {
  581. foreach ($update_dependencies[$data['module']][$data['number']] as $module => $number) {
  582. $dependency = $module . '_update_' . $number;
  583. $graph[$dependency]['edges'][$function] = TRUE;
  584. $graph[$dependency]['module'] = $module;
  585. $graph[$dependency]['number'] = $number;
  586. }
  587. }
  588. }
  589. return $graph;
  590. }
  591. /**
  592. * Determines if a module update is missing or unavailable.
  593. *
  594. * @param $module
  595. * The name of the module.
  596. * @param $number
  597. * The number of the update within that module.
  598. * @param $update_functions
  599. * An organized array of update functions, in the format returned by
  600. * update_get_update_function_list(). This should represent all module
  601. * updates that are requested to run at the time this function is called.
  602. *
  603. * @return
  604. * TRUE if the provided module update is not installed or is not in the
  605. * provided list of updates to run; FALSE otherwise.
  606. */
  607. function update_is_missing($module, $number, $update_functions) {
  608. return !isset($update_functions[$module][$number]) || !function_exists($update_functions[$module][$number]);
  609. }
  610. /**
  611. * Determines if a module update has already been performed.
  612. *
  613. * @param $module
  614. * The name of the module.
  615. * @param $number
  616. * The number of the update within that module.
  617. *
  618. * @return
  619. * TRUE if the database schema indicates that the update has already been
  620. * performed; FALSE otherwise.
  621. */
  622. function update_already_performed($module, $number) {
  623. return $number <= drupal_get_installed_schema_version($module);
  624. }
  625. /**
  626. * Invokes hook_update_dependencies() in all installed modules.
  627. *
  628. * This function is similar to \Drupal::moduleHandler()->invokeAll(), with the
  629. * main difference that it does not require that a module be enabled to invoke
  630. * its hook, only that it be installed. This allows the update system to
  631. * properly perform updates even on modules that are currently disabled.
  632. *
  633. * @return
  634. * An array of return values obtained by merging the results of the
  635. * hook_update_dependencies() implementations in all installed modules.
  636. *
  637. * @see \Drupal\Core\Extension\ModuleHandlerInterface::invokeAll()
  638. * @see hook_update_dependencies()
  639. */
  640. function update_retrieve_dependencies() {
  641. $return = [];
  642. /** @var \Drupal\Core\Extension\ModuleExtensionList */
  643. $extension_list = \Drupal::service('extension.list.module');
  644. // Get a list of installed modules, arranged so that we invoke their hooks in
  645. // the same order that \Drupal::moduleHandler()->invokeAll() does.
  646. foreach (\Drupal::keyValue('system.schema')->getAll() as $module => $schema) {
  647. // Skip modules that are entirely missing from the filesystem here, since
  648. // module_load_install() will call trigger_error() if invoked on a module
  649. // that doesn't exist. There's no way to catch() that, so avoid it entirely.
  650. // This can happen when there are orphaned entries in the system.schema k/v
  651. // store for modules that have been removed from a site without first being
  652. // cleanly uninstalled. We don't care here if the module has been installed
  653. // or not, since we'll filter those out in update_get_update_list().
  654. if ($schema == SCHEMA_UNINSTALLED || !$extension_list->exists($module)) {
  655. // Nothing to upgrade.
  656. continue;
  657. }
  658. $function = $module . '_update_dependencies';
  659. // Ensure install file is loaded.
  660. module_load_install($module);
  661. if (function_exists($function)) {
  662. $updated_dependencies = $function();
  663. // Each implementation of hook_update_dependencies() returns a
  664. // multidimensional, associative array containing some keys that
  665. // represent module names (which are strings) and other keys that
  666. // represent update function numbers (which are integers). We cannot use
  667. // array_merge_recursive() to properly merge these results, since it
  668. // treats strings and integers differently. Therefore, we have to
  669. // explicitly loop through the expected array structure here and perform
  670. // the merge manually.
  671. if (isset($updated_dependencies) && is_array($updated_dependencies)) {
  672. foreach ($updated_dependencies as $module_name => $module_data) {
  673. foreach ($module_data as $update_version => $update_data) {
  674. foreach ($update_data as $module_dependency => $update_dependency) {
  675. // If there are redundant dependencies declared for the same
  676. // update function (so that it is declared to depend on more than
  677. // one update from a particular module), record the dependency on
  678. // the highest numbered update here, since that automatically
  679. // implies the previous ones. For example, if one module's
  680. // implementation of hook_update_dependencies() required this
  681. // ordering:
  682. //
  683. // system_update_8002 ---> user_update_8001
  684. //
  685. // but another module's implementation of the hook required this
  686. // one:
  687. //
  688. // system_update_8003 ---> user_update_8001
  689. //
  690. // we record the second one, since system_update_8002() is always
  691. // guaranteed to run before system_update_8003() anyway (within
  692. // an individual module, updates are always run in numerical
  693. // order).
  694. if (!isset($return[$module_name][$update_version][$module_dependency]) || $update_dependency > $return[$module_name][$update_version][$module_dependency]) {
  695. $return[$module_name][$update_version][$module_dependency] = $update_dependency;
  696. }
  697. }
  698. }
  699. }
  700. }
  701. }
  702. }
  703. return $return;
  704. }
  705. /**
  706. * Replace permissions during update.
  707. *
  708. * This function can replace one permission to several or even delete an old
  709. * one.
  710. *
  711. * @param array $replace
  712. * An associative array. The keys are the old permissions the values are lists
  713. * of new permissions. If the list is an empty array, the old permission is
  714. * removed.
  715. */
  716. function update_replace_permissions($replace) {
  717. $prefix = 'user.role.';
  718. $cut = strlen($prefix);
  719. $role_names = \Drupal::service('config.storage')->listAll($prefix);
  720. foreach ($role_names as $role_name) {
  721. $rid = substr($role_name, $cut);
  722. $config = \Drupal::config("user.role.$rid");
  723. $permissions = $config->get('permissions') ?: [];
  724. foreach ($replace as $old_permission => $new_permissions) {
  725. if (($index = array_search($old_permission, $permissions)) !== FALSE) {
  726. unset($permissions[$index]);
  727. $permissions = array_unique(array_merge($permissions, $new_permissions));
  728. }
  729. }
  730. $config
  731. ->set('permissions', $permissions)
  732. ->save();
  733. }
  734. }