Example #1
0
 /**
  * Builds up all element information.
  *
  * @param string $theme_name
  *   The theme name.
  *
  * @return array
  */
 protected function buildInfo($theme_name)
 {
     // Get cached definitions.
     $cid = $this->getCid($theme_name);
     if ($cache = $this->cacheBackend->get($cid)) {
         return $cache->data;
     }
     // Otherwise, rebuild and cache.
     $info = [];
     foreach ($this->getDefinitions() as $element_type => $definition) {
         $element = $this->createInstance($element_type);
         $element_info = $element->getInfo();
         // If this is element is to be used exclusively in a form, denote that it
         // will receive input, and assign the value callback.
         if ($element instanceof FormElementInterface) {
             $element_info['#input'] = TRUE;
             $element_info['#value_callback'] = array($definition['class'], 'valueCallback');
         }
         $info[$element_type] = $element_info;
     }
     foreach ($info as $element_type => $element) {
         $info[$element_type]['#type'] = $element_type;
     }
     // Allow modules to alter the element type defaults.
     $this->moduleHandler->alter('element_info', $info);
     $this->themeManager->alter('element_info', $info);
     $this->cacheBackend->set($cid, $info, Cache::PERMANENT, ['element_info_build']);
     return $info;
 }
 /**
  * Parses a given library file and allows module to alter it.
  *
  * This method sets the parsed information onto the library property.
  *
  * Library information is parsed from *.libraries.yml files; see
  * editor.library.yml for an example. Every library must have at least one js
  * or css entry. Each entry starts with a machine name and defines the
  * following elements:
  * - js: A list of JavaScript files to include. Each file is keyed by the file
  *   path. An item can have several attributes (like HTML
  *   attributes). For example:
  *   @code
  *   js:
  *     path/js/file.js: { attributes: { defer: true } }
  *   @endcode
  *   If the file has no special attributes, just use an empty object:
  *   @code
  *   js:
  *     path/js/file.js: {}
  *   @endcode
  *   The path of the file is relative to the module or theme directory, unless
  *   it starts with a /, in which case it is relative to the Drupal root. If
  *   the file path starts with //, it will be treated as a protocol-free,
  *   external resource (e.g., //cdn.com/library.js). Full URLs
  *   (e.g., http://cdn.com/library.js) as well as URLs that use a valid
  *   stream wrapper (e.g., public://path/to/file.js) are also supported.
  * - css: A list of categories for which the library provides CSS files. The
  *   available categories are:
  *   - base
  *   - layout
  *   - component
  *   - state
  *   - theme
  *   Each category is itself a key for a sub-list of CSS files to include:
  *   @code
  *   css:
  *     component:
  *       css/file.css: {}
  *   @endcode
  *   Just like with JavaScript files, each CSS file is the key of an object
  *   that can define specific attributes. The format of the file path is the
  *   same as for the JavaScript files.
  * - dependencies: A list of libraries this library depends on.
  * - version: The library version. The string "VERSION" can be used to mean
  *   the current Drupal core version.
  * - header: By default, JavaScript files are included in the footer. If the
  *   script must be included in the header (along with all its dependencies),
  *   set this to true. Defaults to false.
  * - minified: If the file is already minified, set this to true to avoid
  *   minifying it again. Defaults to false.
  * - remote: If the library is a third-party script, this provides the
  *   repository URL for reference.
  * - license: If the remote property is set, the license information is
  *   required. It has 3 properties:
  *   - name: The human-readable name of the license.
  *   - url: The URL of the license file/information for the version of the
  *     library used.
  *   - gpl-compatible: A Boolean for whether this library is GPL compatible.
  *
  * See https://www.drupal.org/node/2274843#define-library for more
  * information.
  *
  * @param string $extension
  *   The name of the extension that registered a library.
  * @param string $path
  *   The relative path to the extension.
  *
  * @return array
  *   An array of parsed library data.
  *
  * @throws \Drupal\Core\Asset\Exception\InvalidLibraryFileException
  *   Thrown when a parser exception got thrown.
  */
 protected function parseLibraryInfo($extension, $path)
 {
     $libraries = [];
     $library_file = $path . '/' . $extension . '.libraries.yml';
     if (file_exists($this->root . '/' . $library_file)) {
         try {
             $libraries = Yaml::decode(file_get_contents($this->root . '/' . $library_file));
         } catch (InvalidDataTypeException $e) {
             // Rethrow a more helpful exception to provide context.
             throw new InvalidLibraryFileException(sprintf('Invalid library definition in %s: %s', $library_file, $e->getMessage()), 0, $e);
         }
     }
     // Allow modules to add dynamic library definitions.
     $hook = 'library_info_build';
     if ($this->moduleHandler->implementsHook($extension, $hook)) {
         $libraries = NestedArray::mergeDeep($libraries, $this->moduleHandler->invoke($extension, $hook));
     }
     // Allow modules to alter the module's registered libraries.
     $this->moduleHandler->alter('library_info', $libraries, $extension);
     $this->themeManager->alter('library_info', $libraries, $extension);
     return $libraries;
 }
Example #3
0
 /**
  * {@inheritdoc}
  */
 public function prepareForm($form_id, &$form, FormStateInterface &$form_state)
 {
     $user = $this->currentUser();
     $form['#type'] = 'form';
     // Only update the action if it is not already set.
     if (!isset($form['#action'])) {
         // Instead of setting an actual action URL, we set the placeholder, which
         // will be replaced at the very last moment. This ensures forms with
         // dynamically generated action URLs don't have poor cacheability.
         // Use the proper API to generate the placeholder, when we have one. See
         // https://www.drupal.org/node/2562341.
         $placeholder = 'form_action_' . hash('crc32b', __METHOD__);
         $form['#attached']['placeholders'][$placeholder] = ['#lazy_builder' => ['form_builder:renderPlaceholderFormAction', []]];
         $form['#action'] = $placeholder;
     }
     // Fix the form method, if it is 'get' in $form_state, but not in $form.
     if ($form_state->isMethodType('get') && !isset($form['#method'])) {
         $form['#method'] = 'get';
     }
     // GET forms should not use a CSRF token.
     if (isset($form['#method']) && $form['#method'] === 'get') {
         // Merges in a default, this means if you've explicitly set #token to the
         // the $form_id on a GET form, which we don't recommend, it will work.
         $form += ['#token' => FALSE];
     }
     // Generate a new #build_id for this form, if none has been set already.
     // The form_build_id is used as key to cache a particular build of the form.
     // For multi-step forms, this allows the user to go back to an earlier
     // build, make changes, and re-submit.
     // @see self::buildForm()
     // @see self::rebuildForm()
     if (!isset($form['#build_id'])) {
         $form['#build_id'] = 'form-' . Crypt::randomBytesBase64();
     }
     $form['form_build_id'] = array('#type' => 'hidden', '#value' => $form['#build_id'], '#id' => $form['#build_id'], '#name' => 'form_build_id', '#parents' => array('form_build_id'));
     // Add a token, based on either #token or form_id, to any form displayed to
     // authenticated users. This ensures that any submitted form was actually
     // requested previously by the user and protects against cross site request
     // forgeries.
     // This does not apply to programmatically submitted forms. Furthermore,
     // since tokens are session-bound and forms displayed to anonymous users are
     // very likely cached, we cannot assign a token for them.
     // During installation, there is no $user yet.
     // Form constructors may explicitly set #token to FALSE when cross site
     // request forgery is irrelevant to the form, such as search forms.
     if ($form_state->isProgrammed() || isset($form['#token']) && $form['#token'] === FALSE) {
         unset($form['#token']);
     } else {
         $form['#cache']['contexts'][] = 'user.roles:authenticated';
         if ($user && $user->isAuthenticated()) {
             // Generate a public token based on the form id.
             $form['#token'] = $form_id;
             $form['form_token'] = array('#id' => Html::getUniqueId('edit-' . $form_id . '-form-token'), '#type' => 'token', '#default_value' => $this->csrfToken->get($form['#token']), '#parents' => array('form_token'), '#cache' => ['max-age' => 0]);
         }
     }
     if (isset($form_id)) {
         $form['form_id'] = array('#type' => 'hidden', '#value' => $form_id, '#id' => Html::getUniqueId("edit-{$form_id}"), '#parents' => array('form_id'));
     }
     if (!isset($form['#id'])) {
         $form['#id'] = Html::getUniqueId($form_id);
         // Provide a selector usable by JavaScript. As the ID is unique, its not
         // possible to rely on it in JavaScript.
         $form['#attributes']['data-drupal-selector'] = Html::getId($form_id);
     }
     $form += $this->elementInfo->getInfo('form');
     $form += array('#tree' => FALSE, '#parents' => array());
     $form['#validate'][] = '::validateForm';
     $form['#submit'][] = '::submitForm';
     $build_info = $form_state->getBuildInfo();
     // If no #theme has been set, automatically apply theme suggestions.
     // The form theme hook itself, which is rendered by form.html.twig,
     // is in #theme_wrappers. Therefore, the #theme function only has to care
     // for rendering the inner form elements, not the form itself.
     if (!isset($form['#theme'])) {
         $form['#theme'] = array($form_id);
         if (isset($build_info['base_form_id'])) {
             $form['#theme'][] = $build_info['base_form_id'];
         }
     }
     // Invoke hook_form_alter(), hook_form_BASE_FORM_ID_alter(), and
     // hook_form_FORM_ID_alter() implementations.
     $hooks = array('form');
     if (isset($build_info['base_form_id'])) {
         $hooks[] = 'form_' . $build_info['base_form_id'];
     }
     $hooks[] = 'form_' . $form_id;
     $this->moduleHandler->alter($hooks, $form, $form_state, $form_id);
     $this->themeManager->alter($hooks, $form, $form_state, $form_id);
 }
 /**
  * {@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];
 }
Example #5
0
 /**
  * {@inheritdoc}
  */
 public function prepareForm($form_id, &$form, FormStateInterface &$form_state)
 {
     $user = $this->currentUser();
     $form['#type'] = 'form';
     // Only update the action if it is not already set.
     if (!isset($form['#action'])) {
         $form['#action'] = $this->buildFormAction();
     }
     // Fix the form method, if it is 'get' in $form_state, but not in $form.
     if ($form_state->isMethodType('get') && !isset($form['#method'])) {
         $form['#method'] = 'get';
     }
     // Generate a new #build_id for this form, if none has been set already.
     // The form_build_id is used as key to cache a particular build of the form.
     // For multi-step forms, this allows the user to go back to an earlier
     // build, make changes, and re-submit.
     // @see self::buildForm()
     // @see self::rebuildForm()
     if (!isset($form['#build_id'])) {
         $form['#build_id'] = 'form-' . Crypt::randomBytesBase64();
     }
     $form['form_build_id'] = array('#type' => 'hidden', '#value' => $form['#build_id'], '#id' => $form['#build_id'], '#name' => 'form_build_id', '#parents' => array('form_build_id'));
     // Add a token, based on either #token or form_id, to any form displayed to
     // authenticated users. This ensures that any submitted form was actually
     // requested previously by the user and protects against cross site request
     // forgeries.
     // This does not apply to programmatically submitted forms. Furthermore,
     // since tokens are session-bound and forms displayed to anonymous users are
     // very likely cached, we cannot assign a token for them.
     // During installation, there is no $user yet.
     if ($user && $user->isAuthenticated() && !$form_state->isProgrammed()) {
         // Form constructors may explicitly set #token to FALSE when cross site
         // request forgery is irrelevant to the form, such as search forms.
         if (isset($form['#token']) && $form['#token'] === FALSE) {
             unset($form['#token']);
         } else {
             $form['#token'] = $form_id;
             $form['form_token'] = array('#id' => Html::getUniqueId('edit-' . $form_id . '-form-token'), '#type' => 'token', '#default_value' => $this->csrfToken->get($form['#token']), '#parents' => array('form_token'));
         }
     }
     if (isset($form_id)) {
         $form['form_id'] = array('#type' => 'hidden', '#value' => $form_id, '#id' => Html::getUniqueId("edit-{$form_id}"), '#parents' => array('form_id'));
     }
     if (!isset($form['#id'])) {
         $form['#id'] = Html::getUniqueId($form_id);
         // Provide a selector usable by JavaScript. As the ID is unique, its not
         // possible to rely on it in JavaScript.
         $form['#attributes']['data-drupal-selector'] = Html::getId($form_id);
     }
     $form += $this->elementInfo->getInfo('form');
     $form += array('#tree' => FALSE, '#parents' => array());
     $form['#validate'][] = '::validateForm';
     $form['#submit'][] = '::submitForm';
     $build_info = $form_state->getBuildInfo();
     // If no #theme has been set, automatically apply theme suggestions.
     // The form theme hook itself, which is rendered by form.html.twig,
     // is in #theme_wrappers. Therefore, the #theme function only has to care
     // for rendering the inner form elements, not the form itself.
     if (!isset($form['#theme'])) {
         $form['#theme'] = array($form_id);
         if (isset($build_info['base_form_id'])) {
             $form['#theme'][] = $build_info['base_form_id'];
         }
     }
     // Invoke hook_form_alter(), hook_form_BASE_FORM_ID_alter(), and
     // hook_form_FORM_ID_alter() implementations.
     $hooks = array('form');
     if (isset($build_info['base_form_id'])) {
         $hooks[] = 'form_' . $build_info['base_form_id'];
     }
     $hooks[] = 'form_' . $form_id;
     $this->moduleHandler->alter($hooks, $form, $form_state, $form_id);
     $this->themeManager->alter($hooks, $form, $form_state, $form_id);
 }
Example #6
0
 /**
  * {@inheritdoc}
  */
 public function getJsAssets(AttachedAssetsInterface $assets, $optimize)
 {
     $javascript = [];
     $default_options = ['type' => 'file', 'group' => JS_DEFAULT, 'every_page' => FALSE, 'weight' => 0, 'cache' => TRUE, 'preprocess' => TRUE, 'attributes' => [], 'version' => NULL, 'browsers' => []];
     $libraries_to_load = $this->getLibrariesToLoad($assets);
     // 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_needed = 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;
     if ($settings_needed && $settings_have_changed) {
         $settings = $this->getJsSettingsAssets($assets);
         if (!empty($settings)) {
             $settings_as_inline_javascript = ['type' => 'setting', 'group' => JS_SETTING, 'every_page' => TRUE, '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 (in_array('core/drupalSettings', $header_js_libraries)) {
                 $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];
 }