diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc index c67e981..0234129 100644 --- a/core/includes/ajax.inc +++ b/core/includes/ajax.inc @@ -5,6 +5,8 @@ * Functions for use with Drupal's Ajax framework. */ +use Drupal\Component\Utility\NestedArray; + /** * @defgroup ajax Ajax framework * @{ @@ -290,7 +292,7 @@ function ajax_render($commands = array()) { $scripts = drupal_add_js(); if (!empty($scripts['settings'])) { $settings = $scripts['settings']; - array_unshift($commands, ajax_command_settings(call_user_func_array('array_merge_recursive', $settings['data']), TRUE)); + array_unshift($commands, ajax_command_settings(NestedArray::mergeDeepArray($settings['data']), TRUE)); } // Allow modules to alter any Ajax response. diff --git a/core/includes/file.inc b/core/includes/file.inc index 26dfb64..feaf999 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -1352,19 +1352,11 @@ function file_download() { $uri = $scheme . '://' . $target; if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) { // Let other modules provide headers and controls access to the file. - // module_invoke_all() uses array_merge_recursive() which merges header - // values into a new array. To avoid that and allow modules to override - // headers instead, use array_merge() to merge the returned arrays. - $headers = array(); - foreach (module_implements('file_download') as $module) { - $function = $module . '_file_download'; - $result = $function($uri); + $headers = module_invoke_all('file_download', $uri); + foreach ($headers as $result) { if ($result == -1) { throw new AccessDeniedHttpException(); } - if (isset($result) && is_array($result)) { - $headers = array_merge($headers, $result); - } } if (count($headers)) { return file_transfer($uri, $headers); diff --git a/core/includes/module.inc b/core/includes/module.inc index 9be96cd..f6d47b6 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -6,6 +6,7 @@ */ use Drupal\Component\Graph\Graph; +use Drupal\Component\Utility\NestedArray; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -921,7 +922,7 @@ function module_hook_info() { if (function_exists($function)) { $result = $function(); if (isset($result) && is_array($result)) { - $hook_info = array_merge_recursive($hook_info, $result); + $hook_info = NestedArray::mergeDeep($hook_info, $result); } } } @@ -1002,7 +1003,7 @@ function module_invoke_all($hook) { if (function_exists($function)) { $result = call_user_func_array($function, $args); if (isset($result) && is_array($result)) { - $return = array_merge_recursive($return, $result); + $return = NestedArray::mergeDeep($return, $result); } elseif (isset($result)) { $return[] = $result; diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php index 492665e..516325b 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxResponse.php +++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php @@ -9,6 +9,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Drupal\Component\Utility\NestedArray; /** * JSON response object for AJAX requests. @@ -134,7 +135,7 @@ protected function ajaxRender(Request $request) { $scripts = drupal_add_js(); if (!empty($scripts['settings'])) { $settings = $scripts['settings']; - $this->addCommand(new SettingsCommand(call_user_func_array('array_merge_recursive', $settings['data']), TRUE), TRUE); + $this->addCommand(new SettingsCommand(NestedArray::mergeDeepArray($settings['data']), TRUE), TRUE); } $commands = $this->commands; diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php index 16b5a56..7fd8245 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php @@ -11,6 +11,7 @@ use Drupal\Core\Entity\Query\QueryInterface; use Exception; use Drupal\Component\Uuid\Uuid; +use Drupal\Component\Utility\NestedArray; /** @@ -671,9 +672,9 @@ public function getFieldDefinitions(array $constraints) { // Invoke hooks. $result = module_invoke_all($this->entityType . '_property_info'); - $this->entityFieldInfo = array_merge_recursive($this->entityFieldInfo, $result); + $this->entityFieldInfo = NestedArray::mergeDeep($this->entityFieldInfo, $result); $result = module_invoke_all('entity_field_info', $this->entityType); - $this->entityFieldInfo = array_merge_recursive($this->entityFieldInfo, $result); + $this->entityFieldInfo = NestedArray::mergeDeep($this->entityFieldInfo, $result); $hooks = array('entity_field_info', $this->entityType . '_property_info'); drupal_alter($hooks, $this->entityFieldInfo, $this->entityType); diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 193f442..1bcfcaa 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -786,7 +786,7 @@ function file_ajax_upload() { $output = theme('status_messages') . drupal_render($form); $js = drupal_add_js(); - $settings = call_user_func_array('array_merge_recursive', $js['settings']['data']); + $settings = NestedArray::mergeDeepArray($js['settings']['data']); $response = new AjaxResponse(); return $response->addCommand(new ReplaceCommand(NULL, $output, $settings)); diff --git a/core/modules/node/lib/Drupal/node/NodeFormController.php b/core/modules/node/lib/Drupal/node/NodeFormController.php index 0b892eb..31981a2 100644 --- a/core/modules/node/lib/Drupal/node/NodeFormController.php +++ b/core/modules/node/lib/Drupal/node/NodeFormController.php @@ -7,6 +7,7 @@ namespace Drupal\node; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityFormController; @@ -91,7 +92,7 @@ public function form(array $form, array &$form_state, EntityInterface $node) { // handlers to the form buttons below. Remove hook_form() entirely. $function = node_hook($node->type, 'form'); if ($function && ($extra = $function($node, $form_state))) { - $form = array_merge_recursive($form, $extra); + $form = NestedArray::mergeDeep($form, $extra); } // If the node type has a title, and the node type form defined no special // weight for it, we default to a weight of -5 for consistency. diff --git a/core/modules/rdf/lib/Drupal/rdf/Tests/GetNamespacesTest.php b/core/modules/rdf/lib/Drupal/rdf/Tests/GetNamespacesTest.php index 4a68e52..a133230 100644 --- a/core/modules/rdf/lib/Drupal/rdf/Tests/GetNamespacesTest.php +++ b/core/modules/rdf/lib/Drupal/rdf/Tests/GetNamespacesTest.php @@ -52,8 +52,8 @@ function testGetRdfNamespaces() { $this->assertTrue(!empty($element), 'Two prefixes can be assigned the same namespace.'); $element = $this->xpath('//html[contains(@prefix, :prefix_binding)]', array( - ':prefix_binding' => 'dc: ', + ':prefix_binding' => 'dc: http://purl.org/dc/terms/', )); - $this->assertTrue(empty($element), 'A prefix with conflicting namespaces is discarded.'); + $this->assertTrue(!empty($element), 'When a prefix has conflicting namespaces, the first declared one is used.'); } } diff --git a/core/modules/rdf/lib/Drupal/rdf/Tests/GetRdfNamespacesTest.php b/core/modules/rdf/lib/Drupal/rdf/Tests/GetRdfNamespacesTest.php index 997e41a..362a416 100644 --- a/core/modules/rdf/lib/Drupal/rdf/Tests/GetRdfNamespacesTest.php +++ b/core/modules/rdf/lib/Drupal/rdf/Tests/GetRdfNamespacesTest.php @@ -39,6 +39,6 @@ function testGetRdfNamespaces() { $this->assertEqual($ns['rdfs'], 'http://www.w3.org/2000/01/rdf-schema#', 'A prefix declared once is included.'); $this->assertEqual($ns['foaf'], 'http://xmlns.com/foaf/0.1/', 'The same prefix declared in several implementations of hook_rdf_namespaces() is valid as long as all the namespaces are the same.'); $this->assertEqual($ns['foaf1'], 'http://xmlns.com/foaf/0.1/', 'Two prefixes can be assigned the same namespace.'); - $this->assertTrue(!isset($ns['dc']), 'A prefix with conflicting namespaces is discarded.'); + $this->assertEqual($ns['dc'], 'http://purl.org/dc/terms/', 'When a prefix has conflicting namespaces, the first declared one is used.'); } } diff --git a/core/modules/rdf/rdf.module b/core/modules/rdf/rdf.module index 264a090..ffc3a0b 100644 --- a/core/modules/rdf/rdf.module +++ b/core/modules/rdf/rdf.module @@ -5,6 +5,7 @@ * Enables semantically enriched output for Drupal sites in the form of RDFa. */ +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Template\Attribute; /** @@ -99,27 +100,16 @@ function rdf_rdf_namespaces() { * implement it. */ function rdf_get_namespaces() { - $rdf_namespaces = module_invoke_all('rdf_namespaces'); - // module_invoke_all() uses array_merge_recursive() which might return nested - // arrays if several modules redefine the same prefix multiple times. We need - // to ensure the array of namespaces is flat and only contains strings as - // URIs. - foreach ($rdf_namespaces as $prefix => $uri) { - if (is_array($uri)) { - if (count(array_unique($uri)) == 1) { - // All namespaces declared for this prefix are the same, merge them all - // into a single namespace. - $rdf_namespaces[$prefix] = $uri[0]; - } - else { - // There are conflicting namespaces for this prefix, do not include - // duplicates in order to avoid asserting any inaccurate RDF - // statements. - unset($rdf_namespaces[$prefix]); - } + $namespaces = array(); + // In order to resolve duplicate namespaces by using the earliest defined + // namespace, do not use module_invoke_all(). + foreach (module_implements('rdf_namespaces') as $module) { + $function = $module . '_rdf_namespaces'; + if (function_exists($function)) { + $namespaces = NestedArray::mergeDeep($function(), $namespaces); } } - return $rdf_namespaces; + return $namespaces; } /** @@ -703,7 +693,7 @@ function rdf_preprocess_username(&$variables) { // (see http://www.w3.org/TR/rdfa-syntax/#rdfa-attributes). // Therefore, merge rather than override so as not to clobber values set by // earlier preprocess functions. - $variables['attributes'] = array_merge_recursive($variables['attributes'], $attributes); + $variables['attributes'] = NestedArray::mergeDeep($variables['attributes'], $attributes); } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/Ajax/CommandsTest.php b/core/modules/system/lib/Drupal/system/Tests/Ajax/CommandsTest.php index 5a455b7..60ef056 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Ajax/CommandsTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/CommandsTest.php @@ -158,6 +158,14 @@ function testAjaxCommands() { ); $this->assertCommand($commands, $expected, "'settings' AJAX command issued with correct data"); + // Test that the settings command merges settings properly. + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'settings' command with setting merging"))); + $expected = array( + 'command' => 'settings', + 'settings' => array('ajax_forms_test' => array('foo' => 9001)), + ); + $this->assertCommand($commands, $expected, "'settings' AJAX command with setting merging"); + // Tests the 'add_css' command. $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'add_css' command"))); $expected = array( diff --git a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module index 96288f9..12c87dd 100644 --- a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module +++ b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module @@ -245,6 +245,18 @@ function ajax_forms_test_ajax_commands_form($form, &$form_state) { ), ); + // Tests the 'settings' command with a callback which sets the same + // setting multiple times. This is used to check that settings are + // merged properly (e.g., array_merge_recursive() merges settings + // incorrectly, #1356170). + $form['settings_command_with_merging_example'] = array( + '#type' => 'submit', + '#value' => t("AJAX 'settings' command with setting merging"), + '#ajax' => array( + 'callback' => 'ajax_forms_test_advanced_commands_settings_with_merging_callback', + ), + ); + $form['submit'] = array( '#type' => 'submit', '#value' => t('Submit'), @@ -408,6 +420,16 @@ function ajax_forms_test_advanced_commands_add_css_callback($form, $form_state) } /** + * Ajax callback for 'settings' but with setting overrides. + */ +function ajax_forms_test_advanced_commands_settings_with_merging_callback($form, $form_state) { + drupal_add_js(array('ajax_forms_test' => array('foo' => 42)), 'setting'); + drupal_add_js(array('ajax_forms_test' => array('foo' => 9001)), 'setting'); + + return array('#type' => 'ajax', '#commands' => array()); +} + +/** * Form constructor for AJAX validation form. * * This form and its related submit and callback functions demonstrate diff --git a/core/modules/translation/translation.pages.inc b/core/modules/translation/translation.pages.inc index 7a9880c..53b91c8 100644 --- a/core/modules/translation/translation.pages.inc +++ b/core/modules/translation/translation.pages.inc @@ -5,6 +5,7 @@ * User page callbacks for the Translation module. */ +use Drupal\Component\Utility\NestedArray; use Drupal\node\Plugin\Core\Entity\Node; /** @@ -71,7 +72,7 @@ function translation_node_overview(Node $node) { $options['add'] = array( 'title' => t('add translation'), ) + $links->links[$langcode]; - $options['add'] = array_merge_recursive($options['add'], $query); + $options['add'] = NestedArray::mergeDeep($options['add'], $query); } } $status = t('Not translated'); diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 1e14a0d..41916c3 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -921,6 +921,9 @@ function template_process_username(&$variables) { // to use the former unless they want to add attributes on the link only. // If a link is being rendered, these need to be merged. Some attributes are // themselves arrays, so the merging needs to be recursive. + // This purposefully does not use + // \Drupal\Component\Utility\NestedArray::mergeDeep() for performance + // reasons, since it is potentially called very often. $variables['link_options']['attributes'] = array_merge_recursive($variables['link_attributes'], $variables['attributes']); } }