/** * 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; }
/** * {@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]; }
/** * {@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); }
/** * {@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]; }