/** * {@inheritdoc} */ public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL) { /** @var \Drupal\block\BlockInterface[] $entities */ $build = array(); foreach ($entities as $entity) { $entity_id = $entity->id(); $plugin = $entity->getPlugin(); $plugin_id = $plugin->getPluginId(); $base_id = $plugin->getBaseId(); $derivative_id = $plugin->getDerivativeId(); $configuration = $plugin->getConfiguration(); // Create the render array for the block as a whole. // @see template_preprocess_block(). $build[$entity_id] = array('#theme' => 'block', '#attributes' => array(), '#contextual_links' => array('block' => array('route_parameters' => array('block' => $entity->id()))), '#weight' => $entity->get('weight'), '#configuration' => $configuration, '#plugin_id' => $plugin_id, '#base_plugin_id' => $base_id, '#derivative_plugin_id' => $derivative_id, '#id' => $entity->id(), '#block' => $entity); $build[$entity_id]['#configuration']['label'] = String::checkPlain($configuration['label']); // Set cache tags; these always need to be set, whether the block is // cacheable or not, so that the page cache is correctly informed. $build[$entity_id]['#cache']['tags'] = NestedArray::mergeDeepArray(array($this->getCacheTag(), $entity->getCacheTag(), $entity->getListCacheTags(), $plugin->getCacheTags())); if ($plugin->isCacheable()) { $build[$entity_id]['#pre_render'][] = array($this, 'buildBlock'); // Generic cache keys, with the block plugin's custom keys appended // (usually cache context keys like 'cache_context.user.roles'). $default_cache_keys = array('entity_view', 'block', $entity->id(), $entity->language()->getId(), 'cache_context.theme'); $max_age = $plugin->getCacheMaxAge(); $build[$entity_id]['#cache'] += array('keys' => array_merge($default_cache_keys, $plugin->getCacheKeys()), 'bin' => $plugin->getCacheBin(), 'expire' => $max_age === Cache::PERMANENT ? Cache::PERMANENT : REQUEST_TIME + $max_age); } else { $build[$entity_id] = $this->buildBlock($build[$entity_id]); } // Don't run in ::buildBlock() to ensure cache keys can be altered. If an // alter hook wants to modify the block contents, it can append another // #pre_render hook. $this->moduleHandler()->alter(array('block_view', "block_view_{$base_id}"), $build[$entity_id], $plugin); } return $build; }
/** * {@inheritdoc} */ public function alter(&$libraries, &$extension = NULL, &$context2 = NULL) { if ($extension === 'bootstrap') { $provider = $this->theme->getProvider(); if ($assets = $provider->getAssets()) { $libraries['base-theme'] = NestedArray::mergeDeepArray([$assets, $libraries['base-theme']], TRUE); // Add a specific version and theme CSS overrides file. // @todo This should be retrieved by the Provider API. $version = $this->theme->getSetting('cdn_' . $provider->getPluginId() . '_version') ?: Bootstrap::FRAMEWORK_VERSION; $libraries['base-theme']['version'] = $version; $provider_theme = $this->theme->getSetting('cdn_' . $provider->getPluginId() . '_theme') ?: 'bootstrap'; $provider_theme = $provider_theme === 'bootstrap' || $provider_theme === 'bootstrap_theme' ? '' : "-{$provider_theme}"; foreach ($this->theme->getAncestry(TRUE) as $ancestor) { $overrides = $ancestor->getPath() . "/css/{$version}/overrides{$provider_theme}.min.css"; if (file_exists($overrides)) { // Since this uses a relative path to the ancestor from DRUPAL_ROOT, // we must prefix the entire path with / so it doesn't append the // active theme's path (which would duplicate the prefix). $libraries['base-theme']['css']['theme']["/{$overrides}"] = []; break; } } } } }
/** * Drush execution method. Runs imports on the supplied manifest. */ public function import() { /** @var \Drupal\migrate\MigrateTemplateStorage $template_storage */ $template_storage = \Drupal::service('migrate.template_storage'); $this->setupLegacyDb(); $migration_ids = []; $migrations = []; foreach ($this->migrationList as $migration_info) { if (is_array($migration_info)) { // The migration is stored as the key in the info array. $migration_id = key($migration_info); } else { // If it wasn't an array then the info is just the migration_id. $migration_id = $migration_info; } $template = $template_storage->getTemplateByName($migration_id) ?: []; if (is_array($migration_info)) { // If there is some existing global overrides then we merge them in. if (isset($GLOBALS['config'][$migration_id])) { $migration_info = NestedArray::mergeDeep($GLOBALS['config'][$migration_id], $migration_info); } $migration_info = NestedArray::mergeDeepArray([$template, $migration_info], TRUE); } else { $migration_info = $template; } if ($migration_info) { /** @var \Drupal\migrate\Entity\Migration[] $migrations */ $migrations = \Drupal::service('migrate.migration_builder')->createMigrations([$migration_id => $migration_info]); foreach ($migrations as $migration) { $migration_ids[] = $migration->id(); if (!Migration::load($migration->id())) { $migration->save(); } } // We use these migration_ids to run migrations. If we didn't create // anything we pass it on and this will trigger non-existent migrations // messages or resolved by migration loading. // @todo this can return false positives in the non-existent migration // logic if the builder explicitly returned no results. For example, no // taxonomies will cause some things to be empty. if (!$migrations) { $migration_ids[] = $migration_id; } } } // Load all the migrations at once so they're correctly ordered. foreach (Migration::loadMultiple($migration_ids) as $migration) { $executable = $this->executeMigration($migration); // Store all the migrations for later. $migrations[$migration->id()] = array('executable' => $executable, 'migration' => $migration, 'source' => $migration->get('source'), 'destination' => $migration->get('destination')); } // Warn the user if any migrations were not found. $nonexistent_migrations = array_diff($migration_ids, array_keys($migrations)); if (count($nonexistent_migrations) > 0) { drush_log(dt('The following migrations were not found: @migrations', array('@migrations' => implode(', ', $nonexistent_migrations))), 'warning'); } return $migrations; }
/** * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize() */ public function normalize($field, $format = NULL, array $context = array()) { $normalized_field_items = array(); $entity = $field->getEntity(); $field_name = $field->getName(); $field_definition = $field->getFieldDefinition(); $normalized_field_items = $this->normalizeFieldItems($field, $format, $context); $normalized = NestedArray::mergeDeepArray($normalized_field_items); return $normalized; }
/** * Returns the display objects used to render a set of entities. * * Depending on the configuration of the view mode for each bundle, this can * be either the display object associated to the view mode, or the 'default' * display. * * This method should only be used internally when rendering an entity. When * assigning suggested display options for a component in a given view mode, * entity_get_display() should be used instead, in order to avoid * inadvertently modifying the output of other view modes that might happen to * use the 'default' display too. Those options will then be effectively * applied only if the view mode is configured to use them. * * hook_entity_view_display_alter() is invoked on each display, allowing 3rd * party code to alter the display options held in the display before they are * used to generate render arrays. * * @param \Drupal\Core\Entity\FieldableEntityInterface[] $entities * The entities being rendered. They should all be of the same entity type. * @param string $view_mode * The view mode being rendered. * * @return \Drupal\Core\Entity\Display\EntityViewDisplayInterface[] * The display objects to use to render the entities, keyed by entity * bundle. * * @see entity_get_display() * @see hook_entity_view_display_alter() */ public static function collectRenderDisplays($entities, $view_mode) { if (empty($entities)) { return array(); } // Collect entity type and bundles. $entity_type = current($entities)->getEntityTypeId(); $bundles = array(); foreach ($entities as $entity) { $bundles[$entity->bundle()] = TRUE; } $bundles = array_keys($bundles); // For each bundle, check the existence and status of: // - the display for the view mode, // - the 'default' display. $candidate_ids = array(); foreach ($bundles as $bundle) { if ($view_mode != 'default') { $candidate_ids[$bundle][] = $entity_type . '.' . $bundle . '.' . $view_mode; } $candidate_ids[$bundle][] = $entity_type . '.' . $bundle . '.default'; } $results = \Drupal::entityQuery('entity_view_display')->condition('id', NestedArray::mergeDeepArray($candidate_ids))->condition('status', TRUE)->execute(); // For each bundle, select the first valid candidate display, if any. $load_ids = array(); foreach ($bundles as $bundle) { foreach ($candidate_ids[$bundle] as $candidate_id) { if (isset($results[$candidate_id])) { $load_ids[$bundle] = $candidate_id; break; } } } // Load the selected displays. $storage = \Drupal::entityManager()->getStorage('entity_view_display'); $displays = $storage->loadMultiple($load_ids); $displays_by_bundle = array(); foreach ($bundles as $bundle) { // Use the selected display if any, or create a fresh runtime object. if (isset($load_ids[$bundle])) { $display = $displays[$load_ids[$bundle]]; } else { $display = $storage->create(array('targetEntityType' => $entity_type, 'bundle' => $bundle, 'mode' => $view_mode, 'status' => TRUE)); } // Let the display know which view mode was originally requested. $display->originalMode = $view_mode; // Let modules alter the display. $display_context = array('entity_type' => $entity_type, 'bundle' => $bundle, 'view_mode' => $view_mode); \Drupal::moduleHandler()->alter('entity_view_display', $display, $display_context); $displays_by_bundle[$bundle] = $display; } return $displays_by_bundle; }
/** * Retrieves enabled plugins' files, keyed by plugin ID. * * For CKEditor plugins that implement: * - CKEditorPluginButtonsInterface, not CKEditorPluginContextualInterface, * a plugin is enabled if at least one of its buttons is in the toolbar; * - CKEditorPluginContextualInterface, not CKEditorPluginButtonsInterface, * a plugin is enabled if its isEnabled() method returns TRUE * - both of these interfaces, a plugin is enabled if either is the case. * * Internal plugins (those that are part of the bundled build of CKEditor) are * excluded by default, since they are loaded implicitly. If you need to know * even implicitly loaded (i.e. internal) plugins, then set the optional * second parameter. * * @param \Drupal\editor\Entity\Editor $editor * A configured text editor object. * @param bool $include_internal_plugins * Defaults to FALSE. When set to TRUE, plugins whose isInternal() method * returns TRUE will also be included. * @return array * A list of the enabled CKEditor plugins, with the plugin IDs as keys and * the Drupal root-relative plugin files as values. * For internal plugins, the value is NULL. */ public function getEnabledPluginFiles(Editor $editor, $include_internal_plugins = FALSE) { $plugins = array_keys($this->getDefinitions()); // Flatten each row. $toolbar_rows = array(); $settings = $editor->getSettings(); foreach ($settings['toolbar']['rows'] as $row_number => $row) { $toolbar_rows[] = array_reduce($settings['toolbar']['rows'][$row_number], function (&$result, $button_group) { return array_merge($result, $button_group['items']); }, array()); } $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($toolbar_rows)); $enabled_plugins = array(); $additional_plugins = array(); foreach ($plugins as $plugin_id) { $plugin = $this->createInstance($plugin_id); if (!$include_internal_plugins && $plugin->isInternal()) { continue; } $enabled = FALSE; // Enable this plugin if it provides a button that has been enabled. if ($plugin instanceof CKEditorPluginButtonsInterface) { $plugin_buttons = array_keys($plugin->getButtons()); $enabled = count(array_intersect($toolbar_buttons, $plugin_buttons)) > 0; } // Otherwise enable this plugin if it declares itself as enabled. if (!$enabled && $plugin instanceof CKEditorPluginContextualInterface) { $enabled = $plugin->isEnabled($editor); } if ($enabled) { $enabled_plugins[$plugin_id] = $plugin->isInternal() ? NULL : $plugin->getFile(); // Check if this plugin has dependencies that also need to be enabled. $additional_plugins = array_merge($additional_plugins, array_diff($plugin->getDependencies($editor), $additional_plugins)); } } // Add the list of dependent plugins. foreach ($additional_plugins as $plugin_id) { $plugin = $this->createInstance($plugin_id); $enabled_plugins[$plugin_id] = $plugin->isInternal() ? NULL : $plugin->getFile(); } // Always return plugins in the same order. asort($enabled_plugins); return $enabled_plugins; }
/** * {@inheritdoc} */ public function alter(&$libraries, &$extension = NULL, &$context2 = NULL) { if ($extension === 'bootstrap') { // Retrieve the theme's CDN provider and assets. $provider = $this->theme->getProvider(); $assets = $provider ? $provider->getAssets() : []; // Immediately return if there is no provider or assets. if (!$provider || !$assets) { return; } // Merge the assets into the library info. $libraries['theme'] = NestedArray::mergeDeepArray([$assets, $libraries['theme']], TRUE); // Add a specific version and theme CSS overrides file. // @todo This should be retrieved by the Provider API. $version = $this->theme->getSetting('cdn_' . $provider->getPluginId() . '_version') ?: Bootstrap::FRAMEWORK_VERSION; $libraries['theme']['version'] = $version; $provider_theme = $this->theme->getSetting('cdn_' . $provider->getPluginId() . '_theme') ?: 'bootstrap'; $provider_theme = $provider_theme === 'bootstrap' || $provider_theme === 'bootstrap_theme' ? '' : "-{$provider_theme}"; foreach ($this->theme->getAncestry(TRUE) as $ancestor) { $overrides = $ancestor->getPath() . "/css/{$version}/overrides{$provider_theme}.min.css"; if (file_exists($overrides)) { // Since this uses a relative path to the ancestor from DRUPAL_ROOT, // we must prepend the entire path with forward slash (/) so it // doesn't prepend the active theme's path. $overrides = "/{$overrides}"; // The overrides file must also be stored in the "base" category so // it isn't added after any potential sub-theme's "theme" category. // There's no weight, so it will be added after the provider's assets. // @see https://www.drupal.org/node/2770613 $libraries['theme']['css']['base'][$overrides] = []; break; } } } elseif ($extension === 'core') { // Replace core dialog/jQuery UI implementations with Bootstrap Modals. if ($this->theme->getSetting('modal_enabled')) { $libraries['drupal.dialog']['override'] = 'bootstrap/drupal.dialog'; $libraries['drupal.dialog.ajax']['override'] = 'bootstrap/drupal.dialog.ajax'; } } }
/** * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize() */ public function normalize($field, $format = NULL, array $context = array()) { $normalized_field_items = array(); // Get the field definition. $entity = $field->getEntity(); $field_name = $field->getName(); $field_definition = $field->getFieldDefinition(); // If this field is not translatable, it can simply be normalized without // separating it into different translations. if (!$field_definition->isTranslatable()) { $normalized_field_items = $this->normalizeFieldItems($field, $format, $context); } else { foreach ($entity->getTranslationLanguages() as $language) { $context['langcode'] = $language->id; $translation = $entity->getTranslation($language->id); $translated_field = $translation->get($field_name); $normalized_field_items = array_merge($normalized_field_items, $this->normalizeFieldItems($translated_field, $format, $context)); } } // Merge deep so that links set in entity reference normalizers are merged // into the links property. $normalized = NestedArray::mergeDeepArray($normalized_field_items); return $normalized; }
/** * {@inheritdoc} */ public function alter(&$libraries, &$extension = NULL, &$context2 = NULL) { if ($extension === 'bootstrap') { // Retrieve the theme's CDN provider and assets. $provider = $this->theme->getProvider(); $assets = $provider ? $provider->getAssets() : []; // Immediately return if there is no provider or assets. if (!$provider || !$assets) { return; } // Merge the assets into the library info. $libraries['base-theme'] = NestedArray::mergeDeepArray([$assets, $libraries['base-theme']], TRUE); // Add a specific version and theme CSS overrides file. // @todo This should be retrieved by the Provider API. $version = $this->theme->getSetting('cdn_' . $provider->getPluginId() . '_version') ?: Bootstrap::FRAMEWORK_VERSION; $libraries['base-theme']['version'] = $version; $provider_theme = $this->theme->getSetting('cdn_' . $provider->getPluginId() . '_theme') ?: 'bootstrap'; $provider_theme = $provider_theme === 'bootstrap' || $provider_theme === 'bootstrap_theme' ? '' : "-{$provider_theme}"; foreach ($this->theme->getAncestry(TRUE) as $ancestor) { $overrides = $ancestor->getPath() . "/css/{$version}/overrides{$provider_theme}.min.css"; if (file_exists($overrides)) { // Since this uses a relative path to the ancestor from DRUPAL_ROOT, // we must prefix the entire path with / so it doesn't append the // active theme's path (which would duplicate the prefix). $libraries['base-theme']['css']['theme']["/{$overrides}"] = []; break; } } } elseif ($extension === 'core') { // Replace core dialog/jQuery UI implementations with Bootstrap Modals. if ($this->theme->getSetting('modal_enabled')) { $libraries['drupal.dialog']['override'] = 'bootstrap/drupal.dialog'; $libraries['drupal.dialog.ajax']['override'] = 'bootstrap/drupal.dialog.ajax'; } } }
/** * Sets a configuration override for the given name. * * @param string $name * The configuration object name to override. * @param array $values * The values in the configuration object to override. * * @return $this */ public function setOverride($name, array $values) { if (in_array($name, $this->names)) { if (isset($this->overrides[$name])) { // Existing overrides take precedence since these will have been added // by events with a higher priority. $this->overrides[$name] = NestedArray::mergeDeepArray(array($values, $this->overrides[$name]), TRUE); } else { $this->overrides[$name] = $values; } } return $this; }
/** * {@inheritdoc} */ public function mergeProcessOfProperty($property, array $process_of_property) { // If we already have a process value then merge the incoming process array //otherwise simply set it. $current_process = $this->getProcess(); if (isset($current_process[$property])) { $this->process = NestedArray::mergeDeepArray([$current_process, $this->getProcessNormalized([$property => $process_of_property])], TRUE); } else { $this->setProcessOfProperty($property, $process_of_property); } return $this; }
/** * Gets original data from this configuration object. * * Original data is the data as it is immediately after loading from * configuration storage before any changes. If this is a new configuration * object it will be an empty array. * * @see \Drupal\Core\Config\Config::get() * * @param string $key * A string that maps to a key within the configuration data. * @param bool $apply_overrides * Apply any overrides to the original data. Defaults to TRUE. * * @return mixed * The data that was requested. */ public function getOriginal($key = '', $apply_overrides = TRUE) { $original_data = $this->originalData; if ($apply_overrides) { // Apply overrides. if (isset($this->moduleOverrides) && is_array($this->moduleOverrides)) { $original_data = NestedArray::mergeDeepArray(array($original_data, $this->moduleOverrides), TRUE); } if (isset($this->settingsOverrides) && is_array($this->settingsOverrides)) { $original_data = NestedArray::mergeDeepArray(array($original_data, $this->settingsOverrides), TRUE); } } if (empty($key)) { return $original_data; } else { $parts = explode('.', $key); if (count($parts) == 1) { return isset($original_data[$key]) ? $original_data[$key] : NULL; } else { $value = NestedArray::getValue($original_data, $parts, $key_exists); return $key_exists ? $value : NULL; } } }
/** * Gets the enabled toolbar buttons in the given text editor instance. * * @param \Drupal\editor\Entity\Editor $editor * A configured text editor object. * * @return string[] * A list of the toolbar buttons enabled in the given text editor instance. */ public static function getEnabledButtons(Editor $editor) { $toolbar_rows = []; $settings = $editor->getSettings(); foreach ($settings['toolbar']['rows'] as $row_number => $row) { $toolbar_rows[] = array_reduce($settings['toolbar']['rows'][$row_number], function (&$result, $button_group) { return array_merge($result, $button_group['items']); }, []); } return array_unique(NestedArray::mergeDeepArray($toolbar_rows)); }
/** * {@inheritdoc} */ public function mergeAttachments(array $a, array $b) { // If both #attached arrays contain drupalSettings, then merge them // correctly; adding the same settings multiple times needs to behave // idempotently. if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) { $drupalSettings = NestedArray::mergeDeepArray(array($a['drupalSettings'], $b['drupalSettings']), TRUE); // No need for re-merging them. unset($a['drupalSettings']); unset($b['drupalSettings']); } // Apply the normal merge. $a = array_merge_recursive($a, $b); if (isset($drupalSettings)) { // Save the custom merge for the drupalSettings. $a['drupalSettings'] = $drupalSettings; } return $a; }
/** * Pre-render callback: Renders a processed text element into #markup. * * Runs all the enabled filters on a piece of text. * * Note: Because filters can inject JavaScript or execute PHP code, security * is vital here. When a user supplies a text format, you should validate it * using $format->access() before accepting/using it. This is normally done in * the validation stage of the Form API. You should for example never make a * preview of content in a disallowed format. * * @param array $element * A structured array with the following key-value pairs: * - #text: containing the text to be filtered * - #format: containing the machine name of the filter format to be used to * filter the text. Defaults to the fallback format. * - #langcode: the language code of the text to be filtered, e.g. 'en' for * English. This allows filters to be language-aware so language-specific * text replacement can be implemented. Defaults to an empty string. * - #filter_types_to_skip: an array of filter types to skip, or an empty * array (default) to skip no filter types. All of the format's filters * will be applied, except for filters of the types that are marked to be * skipped. FilterInterface::TYPE_HTML_RESTRICTOR is the only type that * cannot be skipped. * * @return array * The passed-in element with the filtered text in '#markup'. * * @ingroup sanitization */ public static function preRenderText($element) { $format_id = $element['#format']; $filter_types_to_skip = $element['#filter_types_to_skip']; $text = $element['#text']; $langcode = $element['#langcode']; if (!isset($format_id)) { $format_id = static::configFactory()->get('filter.settings')->get('fallback_format'); } // If the requested text format does not exist, the text cannot be filtered. /** @var \Drupal\filter\Entity\FilterFormat $format **/ if (!($format = FilterFormat::load($format_id))) { static::logger('filter')->alert('Missing text format: %format.', array('%format' => $format_id)); $element['#markup'] = ''; return $element; } $filter_must_be_applied = function (FilterInterface $filter) use($filter_types_to_skip) { $enabled = $filter->status === TRUE; $type = $filter->getType(); // Prevent FilterInterface::TYPE_HTML_RESTRICTOR from being skipped. $filter_type_must_be_applied = $type == FilterInterface::TYPE_HTML_RESTRICTOR || !in_array($type, $filter_types_to_skip); return $enabled && $filter_type_must_be_applied; }; // Convert all Windows and Mac newlines to a single newline, so filters only // need to deal with one possibility. $text = str_replace(array("\r\n", "\r"), "\n", $text); // Get a complete list of filters, ordered properly. /** @var \Drupal\filter\Plugin\FilterInterface[] $filters **/ $filters = $format->filters(); // Give filters a chance to escape HTML-like data such as code or formulas. foreach ($filters as $filter) { if ($filter_must_be_applied($filter)) { $text = $filter->prepare($text, $langcode); } } // Perform filtering. $cache_tags = array(); $all_assets = array(); $all_post_render_cache_callbacks = array(); foreach ($filters as $filter) { if ($filter_must_be_applied($filter)) { $result = $filter->process($text, $langcode); $all_assets[] = $result->getAssets(); $cache_tags = Cache::mergeTags($cache_tags, $result->getCacheTags()); $all_post_render_cache_callbacks[] = $result->getPostRenderCacheCallbacks(); $text = $result->getProcessedText(); } } // Filtering done, store in #markup. $element['#markup'] = $text; // Collect all cache tags. if (isset($element['#cache']) && isset($element['#cache']['tags'])) { // Merge the original cache tags array. $cache_tags = Cache::mergeTags($cache_tags, $element['#cache']['tags']); } // Prepend the text format's cache tags array. $cache_tags = Cache::mergeTags($cache_tags, $format->getCacheTag()); $element['#cache']['tags'] = $cache_tags; // Collect all attached assets. if (isset($element['#attached'])) { // Prepend the original attached assets array. array_unshift($all_assets, $element['#attached']); } $element['#attached'] = NestedArray::mergeDeepArray($all_assets); // Collect all #post_render_cache callbacks. if (isset($element['#post_render_cache'])) { // Prepend the original attached #post_render_cache array. array_unshift($all_assets, $element['#post_render_cache']); } $element['#post_render_cache'] = NestedArray::mergeDeepArray($all_post_render_cache_callbacks); return $element; }
/** * Returns the JavaScript settings assets for this response's libraries. * * Gathers all drupalSettings from all libraries in the attached assets * collection and merges them, then it merges individual attached settings, * and finally invokes hook_js_settings_alter() to allow alterations of * JavaScript settings by modules and themes. * * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets * The assets attached to the current response. * @return array * A (possibly optimized) collection of JavaScript assets. */ protected function getJsSettingsAssets(AttachedAssetsInterface $assets) { $settings = []; foreach ($this->getLibrariesToLoad($assets) as $library) { list($extension, $name) = explode('/', $library, 2); $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); if (isset($definition['drupalSettings'])) { $settings = NestedArray::mergeDeepArray([$settings, $definition['drupalSettings']], TRUE); } } // Attached settings win over settings in libraries. $settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE); return $settings; }
/** * Merges multiple values into the array. * * @param array $values * An associative key/value array. * @param bool $recursive * Flag determining whether or not to recursively merge key/value pairs. */ public function merge(array $values, $recursive = TRUE) { if ($recursive) { $this->array = NestedArray::mergeDeepArray([$this->array, $values], TRUE); } else { $this->array += $values; } }
/** * Returns the JavaScript settings assets for this response's libraries. * * Gathers all drupalSettings from all libraries in the attached assets * collection and merges them, then it merges individual attached settings, * and finally invokes hook_js_settings_alter() to allow alterations of * JavaScript settings by modules and themes. * * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets * The assets attached to the current response. * @return array * A (possibly optimized) collection of JavaScript assets. */ protected function getJsSettingsAssets(AttachedAssetsInterface $assets) { $settings = []; foreach ($this->getLibrariesToLoad($assets) as $library) { list($extension, $name) = explode('/', $library, 2); $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); if (isset($definition['drupalSettings'])) { $settings = NestedArray::mergeDeepArray([$settings, $definition['drupalSettings']], TRUE); } } // Attached settings win over settings in libraries. $settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE); // Allow modules and themes to alter the JavaScript settings. $this->moduleHandler->alter('js_settings', $settings, $assets); $this->themeManager->alter('js_settings', $settings, $assets); return $settings; }
/** * {@inheritdoc} */ public function getJsAssets(AttachedAssetsInterface $assets, $optimize) { $theme_info = $this->themeManager->getActiveTheme(); // Add the theme name to the cache key since themes may implement // hook_library_info_alter(). Additionally add the current language to // support translation of JavaScript files via hook_js_alter(). $libraries_to_load = $this->getLibrariesToLoad($assets); $cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize; if ($cached = $this->cache->get($cid)) { list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data; } else { $javascript = []; $default_options = ['type' => 'file', 'group' => JS_DEFAULT, 'weight' => 0, 'cache' => TRUE, 'preprocess' => TRUE, 'attributes' => [], 'version' => NULL, 'browsers' => []]; // Collect all libraries that contain JS assets and are in the header. $header_js_libraries = []; foreach ($libraries_to_load as $library) { list($extension, $name) = explode('/', $library, 2); $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); if (isset($definition['js']) && !empty($definition['header'])) { $header_js_libraries[] = $library; } } // The current list of header JS libraries are only those libraries that // are in the header, but their dependencies must also be loaded for them // to function correctly, so update the list with those. $header_js_libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($header_js_libraries); foreach ($libraries_to_load as $library) { list($extension, $name) = explode('/', $library, 2); $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); if (isset($definition['js'])) { foreach ($definition['js'] as $options) { $options += $default_options; // 'scope' is a calculated option, based on which libraries are // marked to be loaded from the header (see above). $options['scope'] = in_array($library, $header_js_libraries) ? 'header' : 'footer'; // Preprocess can only be set if caching is enabled and no // attributes are set. $options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE; // Always add a tiny value to the weight, to conserve the insertion // order. $options['weight'] += count($javascript) / 1000; // Local and external files must keep their name as the associative // key so the same JavaScript file is not added twice. $javascript[$options['data']] = $options; } } } // Allow modules and themes to alter the JavaScript assets. $this->moduleHandler->alter('js', $javascript, $assets); $this->themeManager->alter('js', $javascript, $assets); // Sort JavaScript assets, so that they appear in the correct order. uasort($javascript, 'static::sort'); // Prepare the return value: filter JavaScript assets per scope. $js_assets_header = []; $js_assets_footer = []; foreach ($javascript as $key => $item) { if ($item['scope'] == 'header') { $js_assets_header[$key] = $item; } elseif ($item['scope'] == 'footer') { $js_assets_footer[$key] = $item; } } if ($optimize) { $collection_optimizer = \Drupal::service('asset.js.collection_optimizer'); $js_assets_header = $collection_optimizer->optimize($js_assets_header); $js_assets_footer = $collection_optimizer->optimize($js_assets_footer); } // If the core/drupalSettings library is being loaded or is already // loaded, get the JavaScript settings assets, and convert them into a // single "regular" JavaScript asset. $libraries_to_load = $this->getLibrariesToLoad($assets); $settings_required = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries())); $settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0; // Initialize settings to FALSE since they are not needed by default. This // distinguishes between an empty array which must still allow // hook_js_settings_alter() to be run. $settings = FALSE; if ($settings_required && $settings_have_changed) { $settings = $this->getJsSettingsAssets($assets); // Allow modules to add cached JavaScript settings. foreach ($this->moduleHandler->getImplementations('js_settings_build') as $module) { $function = $module . '_' . 'js_settings_build'; $function($settings, $assets); } } $settings_in_header = in_array('core/drupalSettings', $header_js_libraries); $this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']); } if ($settings !== FALSE) { // Attached settings override both library definitions and // hook_js_settings_build(). $settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE); // Allow modules and themes to alter the JavaScript settings. $this->moduleHandler->alter('js_settings', $settings, $assets); $this->themeManager->alter('js_settings', $settings, $assets); // Update the $assets object accordingly, so that it reflects the final // settings. $assets->setSettings($settings); $settings_as_inline_javascript = ['type' => 'setting', 'group' => JS_SETTING, 'weight' => 0, 'browsers' => [], 'data' => $settings]; $settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript]; // Prepend to the list of JS assets, to render it first. Preferably in // the footer, but in the header if necessary. if ($settings_in_header) { $js_assets_header = $settings_js_asset + $js_assets_header; } else { $js_assets_footer = $settings_js_asset + $js_assets_footer; } } return [$js_assets_header, $js_assets_footer]; }
/** * {@inheritdoc} */ public function viewElements(FieldItemListInterface $items) { $elements = array(); // Check if the formatter involves a link. if ($this->getSetting('image_link') == 'content') { $uri = $items->getEntity()->urlInfo(); // @todo Remove when theme_responsive_image_formatter() has support for route name. $uri['path'] = $items->getEntity()->getSystemPath(); } elseif ($this->getSetting('image_link') == 'file') { $link_file = TRUE; } $breakpoint_styles = array(); $fallback_image_style = ''; $responsive_image_mapping = entity_load('responsive_image_mapping', $this->getSetting('responsive_image_mapping')); if ($responsive_image_mapping) { foreach ($responsive_image_mapping->getMappings() as $breakpoint_name => $multipliers) { // Make sure there are multipliers. if (!empty($multipliers)) { // Make sure that the breakpoint exists and is enabled. // @todo add the following when breakpoint->status is added again: // $responsive_image_mapping->breakpointGroup->breakpoints[$breakpoint_name]->status $breakpoint = $responsive_image_mapping->getBreakpointGroup()->getBreakpointById($breakpoint_name); if ($breakpoint) { // Determine the enabled multipliers. $multipliers = array_intersect_key($multipliers, $breakpoint->multipliers); foreach ($multipliers as $multiplier => $image_style) { // Make sure the multiplier still exists. if (!empty($image_style)) { // First mapping found is used as fallback. if (empty($fallback_image_style)) { $fallback_image_style = $image_style; } if (!isset($breakpoint_styles[$breakpoint_name])) { $breakpoint_styles[$breakpoint_name] = array(); } $breakpoint_styles[$breakpoint_name][$multiplier] = $image_style; } } } } } } // Check if the user defined a custom fallback image style. if ($this->getSetting('fallback_image_style')) { $fallback_image_style = $this->getSetting('fallback_image_style'); } // Collect cache tags to be added for each item in the field. $all_cache_tags = array(); if ($responsive_image_mapping) { $all_cache_tags[] = $responsive_image_mapping->getCacheTag(); foreach ($breakpoint_styles as $breakpoint_name => $style_per_multiplier) { foreach ($style_per_multiplier as $multiplier => $image_style_name) { $image_style = entity_load('image_style', $image_style_name); $all_cache_tags[] = $image_style->getCacheTag(); } } } if ($fallback_image_style) { $image_style = entity_load('image_style', $fallback_image_style); $all_cache_tags[] = $image_style->getCacheTag(); } $cache_tags = NestedArray::mergeDeepArray($all_cache_tags); foreach ($items as $delta => $item) { if (isset($link_file)) { $uri = array('path' => file_create_url($item->entity->getFileUri()), 'options' => array()); } $elements[$delta] = array('#theme' => 'responsive_image_formatter', '#attached' => array('library' => array('core/picturefill')), '#item' => $item, '#image_style' => $fallback_image_style, '#breakpoints' => $breakpoint_styles, '#path' => isset($uri) ? $uri : '', '#cache' => array('tags' => $cache_tags)); } return $elements; }
/** * {@inheritdoc} */ public function mergeOverrideArrayDefinition(array $other_definition) { $this->arrayDefinition = NestedArray::mergeDeepArray([$this->arrayDefinition, $other_definition], TRUE); return $this; }
/** * Updates all configuration translations for the names / languages provided. * * To be used when interface translation changes result in the need to update * configuration translations to keep them in sync. * * @param array $names * Array of names of configuration objects to update. * @param array $langcodes * (optional) Array of language codes to update. Defaults to all * configurable languages. * * @return int * Total number of configuration override and active configuration objects * updated (saved or removed). */ public function updateConfigTranslations(array $names, array $langcodes = array()) { $langcodes = $langcodes ? $langcodes : array_keys($this->languageManager->getLanguages()); $count = 0; foreach ($names as $name) { $translatable = $this->getTranslatableDefaultConfig($name); if (empty($translatable)) { // If there is nothing translatable in this configuration or not // supported, skip it. continue; } $active_langcode = $this->getActiveConfigLangcode($name); $active = $this->configStorage->read($name); foreach ($langcodes as $langcode) { $processed = $this->processTranslatableData($name, $active, $translatable, $langcode); if ($langcode != $active_langcode) { // If the language code is not the same as the active storage // language, we should update a configuration override. if (!empty($processed)) { // Update translation data in configuration override. $this->saveTranslationOverride($name, $langcode, $processed); $count++; } else { $override = $this->languageManager->getLanguageConfigOverride($langcode, $name); if (!$override->isNew()) { $data = $this->filterOverride($override->get(), $translatable); if (empty($data)) { // Delete language override if there is no data left at all. // This means all prior translations in the override were locale // managed. $this->deleteTranslationOverride($name, $langcode); $count++; } else { // If there were translatable elements besides locale managed // items, save with only those, and remove the ones managed // by locale only. $this->saveTranslationOverride($name, $langcode, $data); $count++; } } } } elseif (locale_is_translatable($langcode)) { // If the language code is the active storage language, we should // update. If it is English, we should only update if English is also // translatable. $active = NestedArray::mergeDeepArray(array($active, $processed), TRUE); $this->saveTranslationActive($name, $active); $count++; } } } return $count; }
/** * Get arbitrary overrides for the named configuration objects from modules. * * @param array $names * The names of the configuration objects to get overrides for. * * @return array * An array of overrides keyed by the configuration object name. */ protected function loadOverrides(array $names) { $overrides = array(); foreach ($this->configFactoryOverrides as $override) { // Existing overrides take precedence since these will have been added // by events with a higher priority. $overrides = NestedArray::mergeDeepArray(array($override->loadOverrides($names), $overrides), TRUE); } return $overrides; }
/** * Preprocess theme hook variables. * * @param array $variables * The variables array, passed by reference. * @param string $hook * The name of the theme hook. * @param array $info * The theme hook info. */ public static function preprocess(array &$variables, $hook, array $info) { static $theme; if (!isset($theme)) { $theme = self::getTheme(); } static $preprocess_manager; if (!isset($preprocess_manager)) { $preprocess_manager = new PreprocessManager($theme); } // Ensure that any default theme hook variables exist. Due to how theme // hook suggestion alters work, the variables provided are from the // original theme hook, not the suggestion. if (isset($info['variables'])) { $variables = NestedArray::mergeDeepArray([$info['variables'], $variables], TRUE); } // Add extra variables to all theme hooks. foreach (Bootstrap::extraVariables() as $key => $value) { if (!isset($variables[$key])) { $variables[$key] = $value; } } // Add active theme context. // @see https://www.drupal.org/node/2630870 if (!isset($variables['theme'])) { $variables['theme'] = $theme->getInfo(); $variables['theme']['name'] = $theme->getName(); $variables['theme']['path'] = $theme->getPath(); $variables['theme']['title'] = $theme->getTitle(); $variables['theme']['settings'] = $theme->settings()->get(); $variables['theme']['has_glyphicons'] = $theme->hasGlyphicons(); $variables['theme']['query_string'] = \Drupal::getContainer()->get('state')->get('system.css_js_query_string') ?: '0'; } // Invoke necessary preprocess plugin. if (isset($info['bootstrap preprocess'])) { if ($preprocess_manager->hasDefinition($info['bootstrap preprocess'])) { $class = $preprocess_manager->createInstance($info['bootstrap preprocess'], ['theme' => $theme]); /** @var \Drupal\bootstrap\Plugin\Preprocess\PreprocessInterface $class */ $class->preprocess($variables, $hook, $info); } } }
/** * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getConfig(). */ public function getConfig(Editor $editor) { // Reasonable defaults that provide expected basic behavior. $config = array('customConfig' => '', 'pasteFromWordPromptCleanup' => TRUE, 'resize_dir' => 'vertical', 'justifyClasses' => array('text-align-left', 'text-align-center', 'text-align-right', 'text-align-justify'), 'entities' => FALSE); // Add the allowedContent setting, which ensures CKEditor only allows tags // and attributes that are allowed by the text format for this text editor. list($config['allowedContent'], $config['disallowedContent']) = $this->generateACFSettings($editor); // Add the format_tags setting, if its button is enabled. $toolbar_rows = array(); $settings = $editor->getSettings(); foreach ($settings['toolbar']['rows'] as $row_number => $row) { $toolbar_rows[] = array_reduce($settings['toolbar']['rows'][$row_number], function (&$result, $button_group) { return array_merge($result, $button_group['items']); }, array()); } $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($toolbar_rows)); if (in_array('Format', $toolbar_buttons)) { $config['format_tags'] = $this->generateFormatTagsSetting($editor); } return $config; }
/** * {@inheritdoc} */ public function getDefinition($base_plugin_id, $exception_on_invalid = TRUE) { $definitions = $this->getDefinitions(); if (isset($definitions[$base_plugin_id])) { $type = $base_plugin_id; } elseif (strpos($base_plugin_id, '.') && ($name = $this->getFallbackName($base_plugin_id))) { // Found a generic name, replacing the last element by '*'. $type = $name; } else { // If we don't have definition, return the 'undefined' element. $type = 'undefined'; } $definition = $definitions[$type]; // Check whether this type is an extension of another one and compile it. if (isset($definition['type'])) { $merge = $this->getDefinition($definition['type'], $exception_on_invalid); // Preserve integer keys on merge, so sequence item types can override // parent settings as opposed to adding unused second, third, etc. items. $definition = NestedArray::mergeDeepArray(array($merge, $definition), TRUE); // Unset type so we try the merge only once per type. unset($definition['type']); $this->definitions[$type] = $definition; } // Add type and default definition class. $definition += array('definition_class' => '\\Drupal\\Core\\TypedData\\DataDefinition', 'type' => $type); return $definition; }
/** * Merges data into a configuration object. * * @param array $data_to_merge * An array containing data to merge. * * @return $this * The configuration object. */ public function merge(array $data_to_merge) { // Preserve integer keys so that configuration keys are not changed. $this->setData(NestedArray::mergeDeepArray(array($this->data, $data_to_merge), TRUE)); return $this; }
/** * Tests that array keys values on the first array are ignored when merging. * * Even if the initial ordering would place the data from the second array * before those in the first one, they are still appended, and the keys on * the first array are deleted and regenerated. * * @covers ::mergeDeepArray */ public function testMergeOutOfSequenceKeys() { $a = array('subkey' => array(10 => 'A', 30 => 'B')); $b = array('subkey' => array(20 => 'C', 0 => 'D')); // Drupal core behavior. $expected = array('subkey' => array(0 => 'A', 1 => 'B', 2 => 'C', 3 => 'D')); $actual = NestedArray::mergeDeepArray(array($a, $b)); $this->assertSame($expected, $actual, 'drupal_array_merge_deep() ignores numeric key order when merging.'); }
/** * Processes an AJAX response into current content. * * This processes the AJAX response as ajax.js does. It uses the response's * JSON data, an array of commands, to update $this->content using equivalent * DOM manipulation as is used by ajax.js. * It does not apply custom AJAX commands though, because emulation is only * implemented for the AJAX commands that ship with Drupal core. * * @param string $content * The current HTML content. * @param array $ajax_response * An array of AJAX commands. * @param array $ajax_settings * An array of AJAX settings which will be used to process the response. * @param array $drupal_settings * An array of settings to update the value of drupalSettings for the * currently-loaded page. * * @see drupalPostAjaxForm() * @see ajax.js */ protected function drupalProcessAjaxResponse($content, array $ajax_response, array $ajax_settings, array $drupal_settings) { // ajax.js applies some defaults to the settings object, so do the same // for what's used by this function. $ajax_settings += array('method' => 'replaceWith'); // DOM can load HTML soup. But, HTML soup can throw warnings, suppress // them. $dom = new \DOMDocument(); @$dom->loadHTML($content); // XPath allows for finding wrapper nodes better than DOM does. $xpath = new \DOMXPath($dom); foreach ($ajax_response as $command) { // Error messages might be not commands. if (!is_array($command)) { continue; } switch ($command['command']) { case 'settings': $drupal_settings = NestedArray::mergeDeepArray([$drupal_settings, $command['settings']], TRUE); break; case 'insert': $wrapperNode = NULL; // When a command doesn't specify a selector, use the // #ajax['wrapper'] which is always an HTML ID. if (!isset($command['selector'])) { $wrapperNode = $xpath->query('//*[@id="' . $ajax_settings['wrapper'] . '"]')->item(0); } elseif (in_array($command['selector'], array('head', 'body'))) { $wrapperNode = $xpath->query('//' . $command['selector'])->item(0); } if ($wrapperNode) { // ajax.js adds an enclosing DIV to work around a Safari bug. $newDom = new \DOMDocument(); // DOM can load HTML soup. But, HTML soup can throw warnings, // suppress them. @$newDom->loadHTML('<div>' . $command['data'] . '</div>'); // Suppress warnings thrown when duplicate HTML IDs are encountered. // This probably means we are replacing an element with the same ID. $newNode = @$dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE); $method = isset($command['method']) ? $command['method'] : $ajax_settings['method']; // The "method" is a jQuery DOM manipulation function. Emulate // each one using PHP's DOMNode API. switch ($method) { case 'replaceWith': $wrapperNode->parentNode->replaceChild($newNode, $wrapperNode); break; case 'append': $wrapperNode->appendChild($newNode); break; case 'prepend': // If no firstChild, insertBefore() falls back to // appendChild(). $wrapperNode->insertBefore($newNode, $wrapperNode->firstChild); break; case 'before': $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode); break; case 'after': // If no nextSibling, insertBefore() falls back to // appendChild(). $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode->nextSibling); break; case 'html': foreach ($wrapperNode->childNodes as $childNode) { $wrapperNode->removeChild($childNode); } $wrapperNode->appendChild($newNode); break; } } break; // @todo Add suitable implementations for these commands in order to // have full test coverage of what ajax.js can do. // @todo Add suitable implementations for these commands in order to // have full test coverage of what ajax.js can do. case 'remove': break; case 'changed': break; case 'css': break; case 'data': break; case 'restripe': break; case 'add_css': break; case 'update_build_id': $buildId = $xpath->query('//input[@name="form_build_id" and @value="' . $command['old'] . '"]')->item(0); if ($buildId) { $buildId->setAttribute('value', $command['new']); } break; } } $content = $dom->saveHTML(); $this->setRawContent($content); $this->setDrupalSettings($drupal_settings); }
/** * Updates all configuration translations for the names / languages provided. * * To be used when interface translation changes result in the need to update * configuration translations to keep them in sync. * * @param array $names * Array of names of configuration objects to update. * @param array $langcodes * (optional) Array of language codes to update. Defaults to all * configurable languages. * * @return int * Total number of configuration override and active configuration objects * updated (saved or removed). */ public function updateConfigTranslations(array $names, array $langcodes = array()) { $langcodes = $langcodes ? $langcodes : array_keys($this->languageManager->getLanguages()); $count = 0; foreach ($names as $name) { $translatable = $this->getTranslatableDefaultConfig($name); if (empty($translatable)) { // If there is nothing translatable in this configuration or not // supported, skip it. continue; } $active_langcode = $this->getActiveConfigLangcode($name); $active = $this->configStorage->read($name); foreach ($langcodes as $langcode) { $processed = $this->processTranslatableData($name, $active, $translatable, $langcode); // If the language code is not the same as the active storage // language, we should update the configuration override. if ($langcode != $active_langcode) { $override = $this->languageManager->getLanguageConfigOverride($langcode, $name); // Filter out locale managed configuration keys so that translations // removed from Locale will be reflected in the config override. $data = $this->filterOverride($override->get(), $translatable); if (!empty($processed)) { // Merge in the Locale managed translations with existing data. $data = NestedArray::mergeDeepArray(array($data, $processed), TRUE); } if (empty($data) && !$override->isNew()) { // The configuration override contains Locale overrides that no // longer exist. $this->deleteTranslationOverride($name, $langcode); $count++; } elseif (!empty($data)) { // Update translation data in configuration override. $this->saveTranslationOverride($name, $langcode, $data); $count++; } } elseif (locale_is_translatable($langcode)) { // If the language code is the active storage language, we should // update. If it is English, we should only update if English is also // translatable. $active = NestedArray::mergeDeepArray(array($active, $processed), TRUE); $this->saveTranslationActive($name, $active); $count++; } } } return $count; }