/** * {@inheritdoc} */ public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) { if ($route_name === '<current>') { if ($current_route = $this->routeMatch->getRouteObject()) { $requirements = $current_route->getRequirements(); // Setting _method and _schema is deprecated since 2.7. Using // setMethods() and setSchemes() are now the recommended ways. unset($requirements['_method']); unset($requirements['_schema']); $route->setRequirements($requirements); $route->setPath($current_route->getPath()); $route->setSchemes($current_route->getSchemes()); $route->setMethods($current_route->getMethods()); $route->setOptions($current_route->getOptions()); $route->setDefaults($current_route->getDefaults()); $parameters = array_merge($parameters, $this->routeMatch->getRawParameters()->all()); if ($bubbleable_metadata) { $bubbleable_metadata->addCacheContexts(['route']); } } else { // If we have no current route match available, point to the frontpage. $route->setPath('/'); } } }
/** * Provides test data for createFromRenderArray(). * * @return array */ public function providerTestCreateFromRenderArray() { $data = []; $empty_metadata = new BubbleableMetadata(); $nonempty_metadata = new BubbleableMetadata(); $nonempty_metadata->setCacheContexts(['qux'])->setCacheTags(['foo:bar'])->setAssets(['settings' => ['foo' => 'bar']]); $empty_render_array = []; $nonempty_render_array = ['#cache' => ['contexts' => ['qux'], 'tags' => ['foo:bar'], 'max-age' => Cache::PERMANENT], '#attached' => ['settings' => ['foo' => 'bar']], '#post_render_cache' => []]; $data[] = [$empty_render_array, $empty_metadata]; $data[] = [$nonempty_render_array, $nonempty_metadata]; return $data; }
public function processOutbound($path, &$options = array(), Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) { if (array_key_exists('purl_context', $options) && $options['purl_context'] == false) { if (count($this->matchedModifiers->getMatched()) && $bubbleable_metadata) { $cacheContexts = $bubbleable_metadata->getCacheContexts(); $cacheContexts[] = 'purl'; $bubbleable_metadata->setCacheContexts($cacheContexts); } return $this->contextHelper->processOutbound($this->matchedModifiers->createContexts(Context::EXIT_CONTEXT), $path, $options, $request, $bubbleable_metadata); } return $this->contextHelper->processOutbound($this->matchedModifiers->createContexts(), $path, $options, $request, $bubbleable_metadata); }
/** * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound(). */ public function processOutbound($path, &$options = array(), Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) { // Rewrite user/uid to user/username. if (preg_match('!^/user/([0-9]+)(/.*)?!', $path, $matches)) { if ($account = User::load($matches[1])) { $matches += array(2 => ''); $path = '/user/' . $account->getUsername() . $matches[2]; if ($bubbleable_metadata) { $bubbleable_metadata->addCacheTags($account->getCacheTags()); } } } // Rewrite forum/ to community/. return preg_replace('@^/forum(.*)@', '/community$1', $path); }
/** * Adds linkit custom autocomplete functionality to elements. * * Instead of using the core autocomplete, we use our own. * * {@inheritdoc} * * @see \Drupal\Core\Render\Element\FormElement::processAutocomplete */ public static function processLinkitAutocomplete(&$element, FormStateInterface $form_state, &$complete_form) { $url = NULL; $access = FALSE; if (!empty($element['#autocomplete_route_name'])) { $parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : array(); $url = Url::fromRoute($element['#autocomplete_route_name'], $parameters)->toString(TRUE); /** @var \Drupal\Core\Access\AccessManagerInterface $access_manager */ $access_manager = \Drupal::service('access_manager'); $access = $access_manager->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser(), TRUE); } if ($access) { $metadata = BubbleableMetadata::createFromRenderArray($element); if ($access->isAllowed()) { $element['#attributes']['class'][] = 'form-linkit-autocomplete'; $metadata->addAttachments(['library' => ['linkit/linkit.autocomplete']]); // Provide a data attribute for the JavaScript behavior to bind to. $element['#attributes']['data-autocomplete-path'] = $url->getGeneratedUrl(); $metadata = $metadata->merge($url); } $metadata ->merge(BubbleableMetadata::createFromObject($access)) ->applyTo($element); } return $element; }
/** * Tests core token replacements generated from a view. */ function testTokenReplacement() { $token_handler = \Drupal::token(); $view = Views::getView('test_tokens'); $view->setDisplay('page_1'); $this->executeView($view); $expected = array('[view:label]' => 'Test tokens', '[view:description]' => 'Test view to token replacement tests.', '[view:id]' => 'test_tokens', '[view:title]' => 'Test token page', '[view:url]' => $view->getUrl(NULL, 'page_1')->setAbsolute(TRUE)->toString(), '[view:total-rows]' => (string) $view->total_rows, '[view:base-table]' => 'views_test_data', '[view:base-field]' => 'id', '[view:items-per-page]' => '10', '[view:current-page]' => '1', '[view:page-count]' => '1'); $base_bubbleable_metadata = BubbleableMetadata::createFromObject($view->storage); $metadata_tests = []; $metadata_tests['[view:label]'] = $base_bubbleable_metadata; $metadata_tests['[view:description]'] = $base_bubbleable_metadata; $metadata_tests['[view:id]'] = $base_bubbleable_metadata; $metadata_tests['[view:title]'] = $base_bubbleable_metadata; $metadata_tests['[view:url]'] = $base_bubbleable_metadata; $metadata_tests['[view:total-rows]'] = $base_bubbleable_metadata; $metadata_tests['[view:base-table]'] = $base_bubbleable_metadata; $metadata_tests['[view:base-field]'] = $base_bubbleable_metadata; $metadata_tests['[view:items-per-page]'] = $base_bubbleable_metadata; $metadata_tests['[view:current-page]'] = $base_bubbleable_metadata; $metadata_tests['[view:page-count]'] = $base_bubbleable_metadata; foreach ($expected as $token => $expected_output) { $bubbleable_metadata = new BubbleableMetadata(); $output = $token_handler->replace($token, array('view' => $view), [], $bubbleable_metadata); $this->assertIdentical($output, $expected_output, format_string('Token %token replaced correctly.', array('%token' => $token))); $this->assertEqual($bubbleable_metadata, $metadata_tests[$token]); } }
/** * Tests the generation of all system site information tokens. */ public function testSystemSiteTokenReplacement() { $url_options = array('absolute' => TRUE, 'language' => $this->interfaceLanguage); $slogan = '<blink>Slogan</blink>'; $safe_slogan = Xss::filterAdmin($slogan); // Set a few site variables. $config = $this->config('system.site'); $config->set('name', '<strong>Drupal<strong>')->set('slogan', $slogan)->set('mail', '*****@*****.**')->save(); // Generate and test tokens. $tests = array(); $tests['[site:name]'] = Html::escape($config->get('name')); $tests['[site:slogan]'] = $safe_slogan; $tests['[site:mail]'] = $config->get('mail'); $tests['[site:url]'] = \Drupal::url('<front>', [], $url_options); $tests['[site:url-brief]'] = preg_replace(array('!^https?://!', '!/$!'), '', \Drupal::url('<front>', [], $url_options)); $tests['[site:login-url]'] = \Drupal::url('user.page', [], $url_options); $base_bubbleable_metadata = new BubbleableMetadata(); $metadata_tests = []; $metadata_tests['[site:name]'] = BubbleableMetadata::createFromObject(\Drupal::config('system.site')); $metadata_tests['[site:slogan]'] = BubbleableMetadata::createFromObject(\Drupal::config('system.site')); $metadata_tests['[site:mail]'] = BubbleableMetadata::createFromObject(\Drupal::config('system.site')); $bubbleable_metadata = clone $base_bubbleable_metadata; $metadata_tests['[site:url]'] = $bubbleable_metadata->addCacheContexts(['url.site']); $metadata_tests['[site:url-brief]'] = $bubbleable_metadata; $metadata_tests['[site:login-url]'] = $bubbleable_metadata; // Test to make sure that we generated something for each token. $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { $bubbleable_metadata = new BubbleableMetadata(); $output = $this->tokenService->replace($input, array(), array('langcode' => $this->interfaceLanguage->getId()), $bubbleable_metadata); $this->assertEqual($output, $expected, new FormattableMarkup('System site information token %token replaced.', ['%token' => $input])); $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]); } }
/** * {@inheritdoc} */ public function filter(DataDefinitionInterface $definition, $value, array $arguments, BubbleableMetadata $bubbleable_metadata = NULL) { if ($definition->getDataType() != 'timestamp') { // Convert the date to an timestamp. $value = $this->getTypedDataManager()->create($definition, $value)->getDateTime()->getTimestamp(); } $arguments += [0 => 'medium', 1 => '', 2 => NULL, 3 => NULL]; if ($arguments[0] != 'custom' && $bubbleable_metadata) { $config = $this->dateFormatStorage->load($arguments[0]); if (!$config) { throw new \InvalidArgumentException("Unknown date format {$arguments['0']} given."); } $bubbleable_metadata->addCacheableDependency($config); } return $this->dateFormatter->format($value, $arguments[0], $arguments[1], $arguments[2], $arguments[3]); }
/** * {@inheritdoc} */ public function build() { // Grab test attachment fixtures from // Drupal\render_attached_test\Controller\RenderAttachedTestController. $controller = new RenderAttachedTestController(); $attached = BubbleableMetadata::mergeAttachments($controller->feed(), $controller->head()); $attached = BubbleableMetadata::mergeAttachments($attached, $controller->header()); $attached = BubbleableMetadata::mergeAttachments($attached, $controller->teapotHeaderStatus()); // Return some arbitrary markup so the block doesn't disappear. $attached['#markup'] = 'Markup from attached_rendering_block.'; return $attached; }
/** * {@inheritdoc} */ public function build() { // Grab test attachment fixtures from // Drupal\render_attached_test\Controller\TestController. $controller = new TestController(); $attached = BubbleableMetadata::mergeAttachments($controller->feed(), $controller->head()); $attached = BubbleableMetadata::mergeAttachments($attached, $controller->header()); $attached = BubbleableMetadata::mergeAttachments($attached, $controller->teapotHeaderStatus()); // Use drupal_process_attached() to attach all the #attached stuff. drupal_process_attached($attached); // Return some arbitrary markup so the block doesn't disappear. return ['#markup' => 'Headers handled by drupal_process_attached().']; }
/** * {@inheritdoc} */ public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) { if ($route->hasRequirement('_csrf_token')) { $path = ltrim($route->getPath(), '/'); // Replace the path parameters with values from the parameters array. foreach ($parameters as $param => $value) { $path = str_replace("{{$param}}", $value, $path); } // Adding this to the parameters means it will get merged into the query // string when the route is compiled. if (!$bubbleable_metadata) { $parameters['token'] = $this->csrfToken->get($path); } else { // Generate a placeholder and a render array to replace it. $placeholder = hash('sha1', $path); $placeholder_render_array = ['#lazy_builder' => ['route_processor_csrf:renderPlaceholderCsrfToken', [$path]]]; // Instead of setting an actual CSRF token as the query string, we set // the placeholder, which will be replaced at the very last moment. This // ensures links with CSRF tokens don't break cacheability. $parameters['token'] = $placeholder; $bubbleable_metadata->addAttachments(['placeholders' => [$placeholder => $placeholder_render_array]]); } } }
/** * Add an AJAX command to the response. * * @param \Drupal\Core\Ajax\CommandInterface $command * An AJAX command object implementing CommandInterface. * @param bool $prepend * A boolean which determines whether the new command should be executed * before previously added commands. Defaults to FALSE. * * @return AjaxResponse * The current AjaxResponse. */ public function addCommand(CommandInterface $command, $prepend = FALSE) { if ($prepend) { array_unshift($this->commands, $command->render()); } else { $this->commands[] = $command->render(); } if ($command instanceof CommandWithAttachedAssetsInterface) { $assets = $command->getAttachedAssets(); $attachments = ['library' => $assets->getLibraries(), 'drupalSettings' => $assets->getSettings()]; $attachments = BubbleableMetadata::mergeAttachments($this->getAttachments(), $attachments); $this->setAttachments($attachments); } return $this; }
/** * Provide replacement values for placeholder tokens. * * This hook is invoked when someone calls * \Drupal\Core\Utility\Token::replace(). That function first scans the text for * [type:token] patterns, and splits the needed tokens into groups by type. * Then hook_tokens() is invoked on each token-type group, allowing your module * to respond by providing replacement text for any of the tokens in the group * that your module knows how to process. * * A module implementing this hook should also implement hook_token_info() in * order to list its available tokens on editing screens. * * @param $type * The machine-readable name of the type (group) of token being replaced, such * as 'node', 'user', or another type defined by a hook_token_info() * implementation. * @param $tokens * An array of tokens to be replaced. The keys are the machine-readable token * names, and the values are the raw [type:token] strings that appeared in the * original text. * @param array $data * An associative array of data objects to be used when generating replacement * values, as supplied in the $data parameter to * \Drupal\Core\Utility\Token::replace(). * @param array $options * An associative array of options for token replacement; see * \Drupal\Core\Utility\Token::replace() for possible values. * @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata * The bubbleable metadata. Prior to invoking this hook, * \Drupal\Core\Utility\Token::generate() collects metadata for all of the * data objects in $data. For any data sources not in $data, but that are * used by the token replacement logic, such as global configuration (e.g., * 'system.site') and related objects (e.g., $node->getOwner()), * implementations of this hook must add the corresponding metadata. * For example: * @code * $bubbleable_metadata->addCacheableDependency(\Drupal::config('system.site')); * $bubbleable_metadata->addCacheableDependency($node->getOwner()); * @endcode * * Additionally, implementations of this hook, must forward * $bubbleable_metadata to the chained tokens that they invoke. * For example: * @code * if ($created_tokens = $token_service->findWithPrefix($tokens, 'created')) { * $replacements = $token_service->generate('date', $created_tokens, array('date' => $node->getCreatedTime()), $options, $bubbleable_metadata); * } * @endcode * * @return array * An associative array of replacement values, keyed by the raw [type:token] * strings from the original text. * * @see hook_token_info() * @see hook_tokens_alter() */ function hook_tokens($type, $tokens, array $data, array $options, \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata) { $token_service = \Drupal::token(); $url_options = array('absolute' => TRUE); if (isset($options['langcode'])) { $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']); $langcode = $options['langcode']; } else { $langcode = NULL; } $sanitize = !empty($options['sanitize']); $replacements = array(); if ($type == 'node' && !empty($data['node'])) { /** @var \Drupal\node\NodeInterface $node */ $node = $data['node']; foreach ($tokens as $name => $original) { switch ($name) { // Simple key values on the node. case 'nid': $replacements[$original] = $node->nid; break; case 'title': $replacements[$original] = $sanitize ? Html::escape($node->getTitle()) : $node->getTitle(); break; case 'edit-url': $replacements[$original] = $node->url('edit-form', $url_options); break; // Default values for the chained tokens handled below. // Default values for the chained tokens handled below. case 'author': $account = $node->getOwner() ? $node->getOwner() : User::load(0); $replacements[$original] = $sanitize ? Html::escape($account->label()) : $account->label(); $bubbleable_metadata->addCacheableDependency($account); break; case 'created': $replacements[$original] = format_date($node->getCreatedTime(), 'medium', '', NULL, $langcode); break; } } if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) { $replacements = $token_service->generate('user', $author_tokens, array('user' => $node->getOwner()), $options, $bubbleable_metadata); } if ($created_tokens = $token_service->findWithPrefix($tokens, 'created')) { $replacements = $token_service->generate('date', $created_tokens, array('date' => $node->getCreatedTime()), $options, $bubbleable_metadata); } } return $replacements; }
/** * Pre-render callback: Renders a link into #markup. * * Doing so during pre_render gives modules a chance to alter the link parts. * * @param array $element * A structured array whose keys form the arguments to * \Drupal\Core\Utility\LinkGeneratorInterface::generate(): * - #title: The link text. * - #url: The URL info either pointing to a route or a non routed path. * - #options: (optional) An array of options to pass to the link generator. * * @return array * The passed-in element containing a rendered link in '#markup'. */ public static function preRenderLink($element) { // By default, link options to pass to the link generator are normally set // in #options. $element += array('#options' => array()); // However, within the scope of renderable elements, #attributes is a valid // way to specify attributes, too. Take them into account, but do not override // attributes from #options. if (isset($element['#attributes'])) { $element['#options'] += array('attributes' => array()); $element['#options']['attributes'] += $element['#attributes']; } // This #pre_render callback can be invoked from inside or outside of a Form // API context, and depending on that, a HTML ID may be already set in // different locations. #options should have precedence over Form API's #id. // #attributes have been taken over into #options above already. if (isset($element['#options']['attributes']['id'])) { $element['#id'] = $element['#options']['attributes']['id']; } elseif (isset($element['#id'])) { $element['#options']['attributes']['id'] = $element['#id']; } // Conditionally invoke self::preRenderAjaxForm(), if #ajax is set. if (isset($element['#ajax']) && !isset($element['#ajax_processed'])) { // If no HTML ID was found above, automatically create one. if (!isset($element['#id'])) { $element['#id'] = $element['#options']['attributes']['id'] = HtmlUtility::getUniqueId('ajax-link'); } $element = static::preRenderAjaxForm($element); } if (!empty($element['#url']) && $element['#url'] instanceof CoreUrl) { $options = NestedArray::mergeDeep($element['#url']->getOptions(), $element['#options']); /** @var \Drupal\Core\Utility\LinkGenerator $link_generator */ $link_generator = \Drupal::service('link_generator'); $generated_link = $link_generator->generate($element['#title'], $element['#url']->setOptions($options)); $element['#markup'] = $generated_link->getGeneratedLink(); $generated_link->merge(BubbleableMetadata::createFromRenderArray($element))->applyTo($element); } return $element; }
/** * {@inheritdoc} */ public function processOutbound($path, &$options = array(), Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) { if ($request) { // The following values are not supposed to change during a single page // request processing. if (!isset($this->queryRewrite)) { if ($this->currentUser->isAnonymous()) { $languages = $this->languageManager->getLanguages(); $config = $this->config->get('language.negotiation')->get('session'); $this->queryParam = $config['parameter']; $this->queryValue = $request->query->has($this->queryParam) ? $request->query->get($this->queryParam) : NULL; $this->queryRewrite = isset($languages[$this->queryValue]); } else { $this->queryRewrite = FALSE; } } // If the user is anonymous, the user language negotiation method is // enabled, and the corresponding option has been set, we must preserve // any explicit user language preference even with cookies disabled. if ($this->queryRewrite) { if (isset($options['query']) && is_string($options['query'])) { $query = array(); parse_str($options['query'], $query); $options['query'] = $query; } if (!isset($options['query'][$this->queryParam])) { $options['query'][$this->queryParam] = $this->queryValue; } if ($bubbleable_metadata) { // Cached URLs that have been processed by this outbound path // processor must be: $bubbleable_metadata->addCacheTags($this->config->get('language.negotiation')->getCacheTags())->addCacheContexts(['url.query_args:' . $this->queryParam]); } } } return $path; }
/** * Wrapper for handling AJAX forms. * * Wrapper around \Drupal\Core\Form\FormBuilderInterface::buildForm() to * handle some AJAX stuff automatically. * This makes some assumptions about the client. * * @param \Drupal\Core\Form\FormInterface|string $form_class * The value must be one of the following: * - The name of a class that implements \Drupal\Core\Form\FormInterface. * - An instance of a class that implements \Drupal\Core\Form\FormInterface. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. * * @return \Drupal\Core\Ajax\AjaxResponse|string|array * Returns one of three possible values: * - A \Drupal\Core\Ajax\AjaxResponse object. * - The rendered form, as a string. * - A render array with the title in #title and the rendered form in the * #markup array. */ protected function ajaxFormWrapper($form_class, FormStateInterface &$form_state) { /** @var \Drupal\Core\Render\RendererInterface $renderer */ $renderer = \Drupal::service('renderer'); // This won't override settings already in. if (!$form_state->has('rerender')) { $form_state->set('rerender', FALSE); } $ajax = $form_state->get('ajax'); // Do not overwrite if the redirect has been disabled. if (!$form_state->isRedirectDisabled()) { $form_state->disableRedirect($ajax); } $form_state->disableCache(); // Builds the form in a render context in order to ensure that cacheable // metadata is bubbled up. $render_context = new RenderContext(); $callable = function () use($form_class, &$form_state) { return \Drupal::formBuilder()->buildForm($form_class, $form_state); }; $form = $renderer->executeInRenderContext($render_context, $callable); if (!$render_context->isEmpty()) { BubbleableMetadata::createFromRenderArray($form)->merge($render_context->pop())->applyTo($form); } $output = $renderer->renderRoot($form); drupal_process_attached($form); // These forms have the title built in, so set the title here: $title = $form_state->get('title') ?: ''; if ($ajax && (!$form_state->isExecuted() || $form_state->get('rerender'))) { // If the form didn't execute and we're using ajax, build up an // Ajax command list to execute. $response = new AjaxResponse(); // Attach the library necessary for using the OpenModalDialogCommand and // set the attachments for this Ajax response. $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; $response->setAttachments($form['#attached']); $display = ''; $status_messages = array('#type' => 'status_messages'); if ($messages = $renderer->renderRoot($status_messages)) { $display = '<div class="views-messages">' . $messages . '</div>'; } $display .= $output; $options = array('dialogClass' => 'views-ui-dialog', 'width' => '75%'); $response->addCommand(new OpenModalDialogCommand($title, $display, $options)); if ($section = $form_state->get('#section')) { $response->addCommand(new Ajax\HighlightCommand('.' . Html::cleanCssIdentifier($section))); } return $response; } return $title ? ['#title' => $title, '#markup' => $output] : $output; }
/** * Renders placeholders (#attached['placeholders']). * * First, the HTML response object is converted to an equivalent render array, * with #markup being set to the response's content and #attached being set to * the response's attachments. Among these attachments, there may be * placeholders that need to be rendered (replaced). * * Next, RendererInterface::renderRoot() is called, which renders the * placeholders into their final markup. * * The markup that results from RendererInterface::renderRoot() is now the * original HTML response's content, but with the placeholders rendered. We * overwrite the existing content in the original HTML response object with * this markup. The markup that was rendered for the placeholders may also * have attachments (e.g. for CSS/JS assets) itself, and cacheability metadata * that indicates what that markup depends on. That metadata is also added to * the HTML response object. * * @param \Drupal\Core\Render\HtmlResponse $response * The HTML response whose placeholders are being replaced. * * @return \Drupal\Core\Render\HtmlResponse * The updated HTML response, with replaced placeholders. * * @see \Drupal\Core\Render\Renderer::replacePlaceholders() * @see \Drupal\Core\Render\Renderer::renderPlaceholder() */ protected function renderPlaceholders(HtmlResponse $response) { $build = ['#markup' => Markup::create($response->getContent()), '#attached' => $response->getAttachments()]; // RendererInterface::renderRoot() renders the $build render array and // updates it in place. We don't care about the return value (which is just // $build['#markup']), but about the resulting render array. // @todo Simplify this when https://www.drupal.org/node/2495001 lands. $this->renderer->renderRoot($build); // Update the Response object now that the placeholders have been rendered. $placeholders_bubbleable_metadata = BubbleableMetadata::createFromRenderArray($build); $response->setContent($build['#markup'])->addCacheableDependency($placeholders_bubbleable_metadata)->setAttachments($placeholders_bubbleable_metadata->getAttachments()); return $response; }
/** * Bubbles Twig template argument's cacheability & attachment metadata. * * For example: a generated link or generated URL object is passed as a Twig * template argument, and its bubbleable metadata must be bubbled. * * @see \Drupal\Core\GeneratedLink * @see \Drupal\Core\GeneratedUrl * * @param mixed $arg * A Twig template argument that is about to be printed. * * @see \Drupal\Core\Theme\ThemeManager::render() * @see \Drupal\Core\Render\RendererInterface::render() */ protected function bubbleArgMetadata($arg) { // If it's a renderable, then it'll be up to the generated render array it // returns to contain the necessary cacheability & attachment metadata. If // it doesn't implement CacheableDependencyInterface or AttachmentsInterface // then there is nothing to do here. if ($arg instanceof RenderableInterface || !($arg instanceof CacheableDependencyInterface || $arg instanceof AttachmentsInterface)) { return; } $arg_bubbleable = []; BubbleableMetadata::createFromObject($arg)->applyTo($arg_bubbleable); $this->renderer->render($arg_bubbleable); }
/** * Loads and renders a view via AJAX. * * @param \Symfony\Component\HttpFoundation\Request $request * The current request object. * * @return \Drupal\views\Ajax\ViewAjaxResponse * The view response as ajax response. * * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException * Thrown when the view was not found. */ public function ajaxView(Request $request) { $name = $request->request->get('view_name'); $display_id = $request->request->get('view_display_id'); if (isset($name) && isset($display_id)) { $args = $request->request->get('view_args'); $args = isset($args) && $args !== '' ? explode('/', $args) : array(); // Arguments can be empty, make sure they are passed on as NULL so that // argument validation is not triggered. $args = array_map(function ($arg) { return $arg == '' ? NULL : $arg; }, $args); $path = $request->request->get('view_path'); $dom_id = $request->request->get('view_dom_id'); $dom_id = isset($dom_id) ? preg_replace('/[^a-zA-Z0-9_-]+/', '-', $dom_id) : NULL; $pager_element = $request->request->get('pager_element'); $pager_element = isset($pager_element) ? intval($pager_element) : NULL; $response = new ViewAjaxResponse(); // Remove all of this stuff from the query of the request so it doesn't // end up in pagers and tablesort URLs. foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER) as $key) { $request->query->remove($key); $request->request->remove($key); } // Load the view. if (!($entity = $this->storage->load($name))) { throw new NotFoundHttpException(); } $view = $this->executableFactory->get($entity); if ($view && $view->access($display_id)) { $response->setView($view); // Fix the current path for paging. if (!empty($path)) { $this->currentPath->setPath('/' . $path, $request); } // Add all POST data, because AJAX is always a post and many things, // such as tablesorts, exposed filters and paging assume GET. $request_all = $request->request->all(); $query_all = $request->query->all(); $request->query->replace($request_all + $query_all); // Overwrite the destination. // @see the redirect.destination service. $origin_destination = $path; // Remove some special parameters you never want to have part of the // destination query. $used_query_parameters = $request->query->all(); // @todo Remove this parsing once these are removed from the request in // https://www.drupal.org/node/2504709. unset($used_query_parameters[FormBuilderInterface::AJAX_FORM_REQUEST], $used_query_parameters[MainContentViewSubscriber::WRAPPER_FORMAT], $used_query_parameters['ajax_page_state']); $query = UrlHelper::buildQuery($used_query_parameters); if ($query != '') { $origin_destination .= '?' . $query; } $this->redirectDestination->set($origin_destination); // Override the display's pager_element with the one actually used. if (isset($pager_element)) { $response->addCommand(new ScrollTopCommand(".js-view-dom-id-{$dom_id}")); $view->displayHandlers->get($display_id)->setOption('pager_element', $pager_element); } // Reuse the same DOM id so it matches that in drupalSettings. $view->dom_id = $dom_id; $context = new RenderContext(); $preview = $this->renderer->executeInRenderContext($context, function () use($view, $display_id, $args) { return $view->preview($display_id, $args); }); if (!$context->isEmpty()) { $bubbleable_metadata = $context->pop(); BubbleableMetadata::createFromRenderArray($preview)->merge($bubbleable_metadata)->applyTo($preview); } $response->addCommand(new ReplaceCommand(".js-view-dom-id-{$dom_id}", $preview)); return $response; } else { throw new AccessDeniedHttpException(); } } else { throw new NotFoundHttpException(); } }
/** * 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'); } /** @var \Drupal\filter\Entity\FilterFormat $format **/ $format = FilterFormat::load($format_id); // If the requested text format doesn't exist or its disabled, the text // cannot be filtered. if (!$format || !$format->status()) { $message = !$format ? 'Missing text format: %format.' : 'Disabled text format: %format.'; static::logger('filter')->alert($message, 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. $metadata = BubbleableMetadata::createFromRenderArray($element); foreach ($filters as $filter) { if ($filter_must_be_applied($filter)) { $result = $filter->process($text, $langcode); $metadata = $metadata->merge($result); $text = $result->getProcessedText(); } } // Filtering and sanitizing have been done in // \Drupal\filter\Plugin\FilterInterface. $text is not guaranteed to be // safe, but it has been passed through the filter system and checked with // a text format, so it must be printed as is. (See the note about security // in the method documentation above.) $element['#markup'] = FilteredMarkup::create($text); // Set the updated bubbleable rendering metadata and the text format's // cache tag. $metadata->applyTo($element); $element['#cache']['tags'] = Cache::mergeTags($element['#cache']['tags'], $format->getCacheTags()); return $element; }
/** * @covers ::replace * @covers ::replace */ public function testReplaceWithHookTokensAlterWithBubbleableMetadata() { $this->moduleHandler->expects($this->any())->method('invokeAll')->willReturn([]); $this->moduleHandler->expects($this->any())->method('alter')->willReturnCallback(function ($hook_name, array &$replacements, array $context, BubbleableMetadata $bubbleable_metadata) { $replacements['[node:title]'] = 'hello world'; $bubbleable_metadata->addCacheContexts(['custom_context']); $bubbleable_metadata->addCacheTags(['node:1']); $bubbleable_metadata->setCacheMaxAge(10); }); $node = $this->prophesize('Drupal\\node\\NodeInterface'); $node->getCacheContexts()->willReturn([]); $node->getCacheTags()->willReturn([]); $node->getCacheMaxAge()->willReturn(14); $node = $node->reveal(); $bubbleable_metadata = new BubbleableMetadata(); $bubbleable_metadata->setCacheContexts(['current_user']); $bubbleable_metadata->setCacheMaxAge(12); $result = $this->token->replace('[node:title]', ['node' => $node], [], $bubbleable_metadata); $this->assertEquals('hello world', $result); $this->assertEquals(['node:1'], $bubbleable_metadata->getCacheTags()); $this->assertEquals(['current_user', 'custom_context'], $bubbleable_metadata->getCacheContexts()); $this->assertEquals(10, $bubbleable_metadata->getCacheMaxAge()); }
/** * Creates some terms and a node, then tests the tokens generated from them. */ function testTaxonomyTokenReplacement() { $token_service = \Drupal::token(); $language_interface = \Drupal::languageManager()->getCurrentLanguage(); // Create two taxonomy terms. $term1 = $this->createTerm($this->vocabulary); $term2 = $this->createTerm($this->vocabulary); // Edit $term2, setting $term1 as parent. $edit = array(); $edit['name[0][value]'] = '<blink>Blinking Text</blink>'; $edit['parent[]'] = array($term1->id()); $this->drupalPostForm('taxonomy/term/' . $term2->id() . '/edit', $edit, t('Save')); // Create node with term2. $edit = array(); $node = $this->drupalCreateNode(array('type' => 'article')); $edit[$this->fieldName . '[]'] = $term2->id(); $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); // Generate and test sanitized tokens for term1. $tests = array(); $tests['[term:tid]'] = $term1->id(); $tests['[term:name]'] = Html::escape($term1->getName()); $tests['[term:description]'] = $term1->description->processed; $tests['[term:url]'] = $term1->url('canonical', array('absolute' => TRUE)); $tests['[term:node-count]'] = 0; $tests['[term:parent:name]'] = '[term:parent:name]'; $tests['[term:vocabulary:name]'] = Html::escape($this->vocabulary->label()); $tests['[term:vocabulary]'] = Html::escape($this->vocabulary->label()); $base_bubbleable_metadata = BubbleableMetadata::createFromObject($term1); $metadata_tests = array(); $metadata_tests['[term:tid]'] = $base_bubbleable_metadata; $metadata_tests['[term:name]'] = $base_bubbleable_metadata; $metadata_tests['[term:description]'] = $base_bubbleable_metadata; $metadata_tests['[term:url]'] = $base_bubbleable_metadata; $metadata_tests['[term:node-count]'] = $base_bubbleable_metadata; $metadata_tests['[term:parent:name]'] = $base_bubbleable_metadata; $bubbleable_metadata = clone $base_bubbleable_metadata; $metadata_tests['[term:vocabulary:name]'] = $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags()); $metadata_tests['[term:vocabulary]'] = $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags()); foreach ($tests as $input => $expected) { $bubbleable_metadata = new BubbleableMetadata(); $output = $token_service->replace($input, array('term' => $term1), array('langcode' => $language_interface->getId()), $bubbleable_metadata); $this->assertEqual($output, $expected, format_string('Sanitized taxonomy term token %token replaced.', array('%token' => $input))); $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]); } // Generate and test sanitized tokens for term2. $tests = array(); $tests['[term:tid]'] = $term2->id(); $tests['[term:name]'] = Html::escape($term2->getName()); $tests['[term:description]'] = $term2->description->processed; $tests['[term:url]'] = $term2->url('canonical', array('absolute' => TRUE)); $tests['[term:node-count]'] = 1; $tests['[term:parent:name]'] = Html::escape($term1->getName()); $tests['[term:parent:url]'] = $term1->url('canonical', array('absolute' => TRUE)); $tests['[term:parent:parent:name]'] = '[term:parent:parent:name]'; $tests['[term:vocabulary:name]'] = Html::escape($this->vocabulary->label()); // Test to make sure that we generated something for each token. $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { $output = $token_service->replace($input, array('term' => $term2), array('langcode' => $language_interface->getId())); $this->assertEqual($output, $expected, format_string('Sanitized taxonomy term token %token replaced.', array('%token' => $input))); } // Generate and test unsanitized tokens. $tests['[term:name]'] = $term2->getName(); $tests['[term:description]'] = $term2->getDescription(); $tests['[term:parent:name]'] = $term1->getName(); $tests['[term:vocabulary:name]'] = $this->vocabulary->label(); foreach ($tests as $input => $expected) { $output = $token_service->replace($input, array('term' => $term2), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE)); $this->assertEqual($output, $expected, format_string('Unsanitized taxonomy term token %token replaced.', array('%token' => $input))); } // Generate and test sanitized tokens. $tests = array(); $tests['[vocabulary:vid]'] = $this->vocabulary->id(); $tests['[vocabulary:name]'] = Html::escape($this->vocabulary->label()); $tests['[vocabulary:description]'] = Xss::filter($this->vocabulary->getDescription()); $tests['[vocabulary:node-count]'] = 1; $tests['[vocabulary:term-count]'] = 2; // Test to make sure that we generated something for each token. $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { $output = $token_service->replace($input, array('vocabulary' => $this->vocabulary), array('langcode' => $language_interface->getId())); $this->assertEqual($output, $expected, format_string('Sanitized taxonomy vocabulary token %token replaced.', array('%token' => $input))); } // Generate and test unsanitized tokens. $tests['[vocabulary:name]'] = $this->vocabulary->label(); $tests['[vocabulary:description]'] = $this->vocabulary->getDescription(); foreach ($tests as $input => $expected) { $output = $token_service->replace($input, array('vocabulary' => $this->vocabulary), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE)); $this->assertEqual($output, $expected, format_string('Unsanitized taxonomy vocabulary token %token replaced.', array('%token' => $input))); } }
/** * Creates a comment, then tests the tokens generated from it. */ function testCommentTokenReplacement() { $token_service = \Drupal::token(); $language_interface = \Drupal::languageManager()->getCurrentLanguage(); $url_options = array('absolute' => TRUE, 'language' => $language_interface); // Change the title of the admin user. $this->adminUser->name->value = 'This is a title with some special & > " stuff.'; $this->adminUser->save(); $this->drupalLogin($this->adminUser); // Set comment variables. $this->setCommentSubject(TRUE); // Create a node and a comment. $node = $this->drupalCreateNode(['type' => 'article', 'title' => '<script>alert("123")</script>']); $parent_comment = $this->postComment($node, $this->randomMachineName(), $this->randomMachineName(), TRUE); // Post a reply to the comment. $this->drupalGet('comment/reply/node/' . $node->id() . '/comment/' . $parent_comment->id()); $child_comment = $this->postComment(NULL, $this->randomMachineName(), $this->randomMachineName()); $comment = Comment::load($child_comment->id()); $comment->setHomepage('http://example.org/'); // Add HTML to ensure that sanitation of some fields tested directly. $comment->setSubject('<blink>Blinking Comment</blink>'); // Generate and test tokens. $tests = array(); $tests['[comment:cid]'] = $comment->id(); $tests['[comment:hostname]'] = $comment->getHostname(); $tests['[comment:author]'] = Html::escape($comment->getAuthorName()); $tests['[comment:mail]'] = $this->adminUser->getEmail(); $tests['[comment:homepage]'] = UrlHelper::filterBadProtocol($comment->getHomepage()); $tests['[comment:title]'] = Html::escape($comment->getSubject()); $tests['[comment:body]'] = $comment->comment_body->processed; $tests['[comment:langcode]'] = $comment->language()->getId(); $tests['[comment:url]'] = $comment->url('canonical', $url_options + array('fragment' => 'comment-' . $comment->id())); $tests['[comment:edit-url]'] = $comment->url('edit-form', $url_options); $tests['[comment:created]'] = \Drupal::service('date.formatter')->format($comment->getCreatedTime(), 'medium', array('langcode' => $language_interface->getId())); $tests['[comment:created:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($comment->getCreatedTime(), array('langcode' => $language_interface->getId())); $tests['[comment:changed:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($comment->getChangedTimeAcrossTranslations(), array('langcode' => $language_interface->getId())); $tests['[comment:parent:cid]'] = $comment->hasParentComment() ? $comment->getParentComment()->id() : NULL; $tests['[comment:parent:title]'] = $parent_comment->getSubject(); $tests['[comment:entity]'] = Html::escape($node->getTitle()); // Test node specific tokens. $tests['[comment:entity:nid]'] = $comment->getCommentedEntityId(); $tests['[comment:entity:title]'] = Html::escape($node->getTitle()); $tests['[comment:author:uid]'] = $comment->getOwnerId(); $tests['[comment:author:name]'] = Html::escape($this->adminUser->getDisplayName()); $base_bubbleable_metadata = BubbleableMetadata::createFromObject($comment); $metadata_tests = []; $metadata_tests['[comment:cid]'] = $base_bubbleable_metadata; $metadata_tests['[comment:hostname]'] = $base_bubbleable_metadata; $bubbleable_metadata = clone $base_bubbleable_metadata; $bubbleable_metadata->addCacheableDependency($this->adminUser); $metadata_tests['[comment:author]'] = $bubbleable_metadata; $bubbleable_metadata = clone $base_bubbleable_metadata; $bubbleable_metadata->addCacheableDependency($this->adminUser); $metadata_tests['[comment:mail]'] = $bubbleable_metadata; $metadata_tests['[comment:homepage]'] = $base_bubbleable_metadata; $metadata_tests['[comment:title]'] = $base_bubbleable_metadata; $metadata_tests['[comment:body]'] = $base_bubbleable_metadata; $metadata_tests['[comment:langcode]'] = $base_bubbleable_metadata; $metadata_tests['[comment:url]'] = $base_bubbleable_metadata; $metadata_tests['[comment:edit-url]'] = $base_bubbleable_metadata; $bubbleable_metadata = clone $base_bubbleable_metadata; $metadata_tests['[comment:created]'] = $bubbleable_metadata->addCacheTags(['rendered']); $bubbleable_metadata = clone $base_bubbleable_metadata; $metadata_tests['[comment:created:since]'] = $bubbleable_metadata->setCacheMaxAge(0); $bubbleable_metadata = clone $base_bubbleable_metadata; $metadata_tests['[comment:changed:since]'] = $bubbleable_metadata->setCacheMaxAge(0); $bubbleable_metadata = clone $base_bubbleable_metadata; $metadata_tests['[comment:parent:cid]'] = $bubbleable_metadata->addCacheTags(['comment:1']); $metadata_tests['[comment:parent:title]'] = $bubbleable_metadata; $bubbleable_metadata = clone $base_bubbleable_metadata; $metadata_tests['[comment:entity]'] = $bubbleable_metadata->addCacheTags(['node:2']); // Test node specific tokens. $metadata_tests['[comment:entity:nid]'] = $bubbleable_metadata; $metadata_tests['[comment:entity:title]'] = $bubbleable_metadata; $bubbleable_metadata = clone $base_bubbleable_metadata; $metadata_tests['[comment:author:uid]'] = $bubbleable_metadata->addCacheTags(['user:2']); $metadata_tests['[comment:author:name]'] = $bubbleable_metadata; // Test to make sure that we generated something for each token. $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { $bubbleable_metadata = new BubbleableMetadata(); $output = $token_service->replace($input, array('comment' => $comment), array('langcode' => $language_interface->getId()), $bubbleable_metadata); $this->assertEqual($output, $expected, new FormattableMarkup('Comment token %token replaced.', ['%token' => $input])); $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]); } // Test anonymous comment author. $author_name = 'This is a random & " > string'; $comment->setOwnerId(0)->setAuthorName($author_name); $input = '[comment:author]'; $output = $token_service->replace($input, array('comment' => $comment), array('langcode' => $language_interface->getId())); $this->assertEqual($output, Html::escape($author_name), format_string('Comment author token %token replaced.', array('%token' => $input))); // Load node so comment_count gets computed. $node = Node::load($node->id()); // Generate comment tokens for the node (it has 2 comments, both new). $tests = array(); $tests['[entity:comment-count]'] = 2; $tests['[entity:comment-count-new]'] = 2; foreach ($tests as $input => $expected) { $output = $token_service->replace($input, array('entity' => $node, 'node' => $node), array('langcode' => $language_interface->getId())); $this->assertEqual($output, $expected, format_string('Node comment token %token replaced.', array('%token' => $input))); } }
/** * {@inheritdoc} */ public function processOutbound($path, &$options = array(), Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) { $url_scheme = 'http'; $port = 80; if ($request) { $url_scheme = $request->getScheme(); $port = $request->getPort(); } $languages = array_flip(array_keys($this->languageManager->getLanguages())); // Language can be passed as an option, or we go for current URL language. if (!isset($options['language'])) { $language_url = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL); $options['language'] = $language_url; } elseif (!is_object($options['language']) || !isset($languages[$options['language']->getId()])) { return $path; } $config = $this->config->get('language.negotiation')->get('url'); if ($config['source'] == LanguageNegotiationUrl::CONFIG_PATH_PREFIX) { if (is_object($options['language']) && !empty($config['prefixes'][$options['language']->getId()])) { $options['prefix'] = $config['prefixes'][$options['language']->getId()] . '/'; if ($bubbleable_metadata) { $bubbleable_metadata->addCacheContexts(['languages:' . LanguageInterface::TYPE_URL]); } } } elseif ($config['source'] == LanguageNegotiationUrl::CONFIG_DOMAIN) { if (is_object($options['language']) && !empty($config['domains'][$options['language']->getId()])) { // Save the original base URL. If it contains a port, we need to // retain it below. if (!empty($options['base_url'])) { // The colon in the URL scheme messes up the port checking below. $normalized_base_url = str_replace(array('https://', 'http://'), '', $options['base_url']); } // Ask for an absolute URL with our modified base URL. $options['absolute'] = TRUE; $options['base_url'] = $url_scheme . '://' . $config['domains'][$options['language']->getId()]; // In case either the original base URL or the HTTP host contains a // port, retain it. if (isset($normalized_base_url) && strpos($normalized_base_url, ':') !== FALSE) { list(, $port) = explode(':', $normalized_base_url); $options['base_url'] .= ':' . $port; } elseif ($url_scheme == 'http' && $port != 80 || $url_scheme == 'https' && $port != 443) { $options['base_url'] .= ':' . $port; } if (isset($options['https'])) { if ($options['https'] === TRUE) { $options['base_url'] = str_replace('http://', 'https://', $options['base_url']); } elseif ($options['https'] === FALSE) { $options['base_url'] = str_replace('https://', 'http://', $options['base_url']); } } // Add Drupal's subfolder from the base_path if there is one. $options['base_url'] .= rtrim(base_path(), '/'); if ($bubbleable_metadata) { $bubbleable_metadata->addCacheContexts(['languages:' . LanguageInterface::TYPE_URL, 'url.site']); } } } return $path; }
/** * Tests bubbleable metadata of menu links' outbound route/path processing. */ public function testOutboundPathAndRouteProcessing() { \Drupal::service('router.builder')->rebuild(); $request_stack = \Drupal::requestStack(); /** @var \Symfony\Component\Routing\RequestContext $request_context */ $request_context = \Drupal::service('router.request_context'); $request = Request::create('/'); $request->attributes->set(RouteObjectInterface::ROUTE_NAME, '<front>'); $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route('/')); $request_stack->push($request); $request_context->fromRequest($request); $menu_tree = \Drupal::menuTree(); $renderer = \Drupal::service('renderer'); $default_menu_cacheability = (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT)->setCacheTags(['config:system.menu.tools'])->setCacheContexts(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']); User::create(['uid' => 1, 'name' => $this->randomString()])->save(); User::create(['uid' => 2, 'name' => $this->randomString()])->save(); // Five test cases, four asserting one outbound path/route processor, and // together covering one of each: // - no cacheability metadata, // - a cache context, // - a cache tag, // - a cache max-age. // Plus an additional test case to verify that multiple links adding // cacheability metadata of the same type is working (two links with cache // tags). $test_cases = [['uri' => 'route:<current>', 'cacheability' => (new BubbleableMetadata())->setCacheContexts(['route'])], ['uri' => 'route:outbound_processing_test.route.csrf', 'cacheability' => (new BubbleableMetadata())->setCacheContexts(['session'])->setAttachments(['placeholders' => []])], ['uri' => 'internal:/', 'cacheability' => new BubbleableMetadata()], ['uri' => 'internal:/user/1', 'cacheability' => (new BubbleableMetadata())->setCacheTags(User::load(1)->getCacheTags())], ['uri' => 'internal:/user/2', 'cacheability' => (new BubbleableMetadata())->setCacheTags(User::load(2)->getCacheTags())]]; // Test each expectation individually. foreach ($test_cases as $expectation) { $menu_link_content = MenuLinkContent::create(['link' => ['uri' => $expectation['uri']], 'menu_name' => 'tools']); $menu_link_content->save(); $tree = $menu_tree->load('tools', new MenuTreeParameters()); $build = $menu_tree->build($tree); $renderer->renderRoot($build); $expected_cacheability = $default_menu_cacheability->merge($expectation['cacheability']); $this->assertEqual($expected_cacheability, BubbleableMetadata::createFromRenderArray($build)); $menu_link_content->delete(); } // Now test them all together in one menu: the rendered menu's cacheability // metadata should be the combination of the cacheability of all links, and // thus of all tested outbound path & route processors. $expected_cacheability = new BubbleableMetadata(); foreach ($test_cases as $expectation) { $menu_link_content = MenuLinkContent::create(['link' => ['uri' => $expectation['uri']], 'menu_name' => 'tools']); $menu_link_content->save(); $expected_cacheability = $expected_cacheability->merge($expectation['cacheability']); } $tree = $menu_tree->load('tools', new MenuTreeParameters()); $build = $menu_tree->build($tree); $renderer->renderRoot($build); $expected_cacheability = $expected_cacheability->merge($default_menu_cacheability); $this->assertEqual($expected_cacheability, BubbleableMetadata::createFromRenderArray($build)); }
/** * @covers ::addCacheableDependency * @dataProvider providerTestMerge * * This only tests at a high level, because it reuses existing logic. Detailed * tests exist for the existing logic: * * @see \Drupal\Tests\Core\Cache\CacheTest::testMergeTags() * @see \Drupal\Tests\Core\Cache\CacheTest::testMergeMaxAges() * @see \Drupal\Tests\Core\Cache\CacheContextsTest */ public function testAddCacheableDependency(BubbleableMetadata $a, $b, BubbleableMetadata $expected) { $cache_contexts_manager = $this->getMockBuilder('Drupal\\Core\\Cache\\Context\\CacheContextsManager')->disableOriginalConstructor()->getMock(); $cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE); $container = new ContainerBuilder(); $container->set('cache_contexts_manager', $cache_contexts_manager); \Drupal::setContainer($container); $this->assertEquals($expected, $a->addCacheableDependency($b)); }
/** * Wraps a controller execution in a render context. * * @param callable $controller * The controller to execute. * @param array $arguments * The arguments to pass to the controller. * * @return mixed * The return value of the controller. * * @throws \LogicException * When early rendering has occurred in a controller that returned a * Response or domain object that cares about attachments or cacheability. * * @see \Symfony\Component\HttpKernel\HttpKernel::handleRaw() */ protected function wrapControllerExecutionInRenderContext($controller, array $arguments) { $context = new RenderContext(); $response = $this->renderer->executeInRenderContext($context, function () use($controller, $arguments) { // Now call the actual controller, just like HttpKernel does. return call_user_func_array($controller, $arguments); }); // If early rendering happened, i.e. if code in the controller called // drupal_render() outside of a render context, then the bubbleable metadata // for that is stored in the current render context. if (!$context->isEmpty()) { /** @var \Drupal\Core\Render\BubbleableMetadata $early_rendering_bubbleable_metadata */ $early_rendering_bubbleable_metadata = $context->pop(); // If a render array or AjaxResponse is returned by the controller, merge // the "lost" bubbleable metadata. if (is_array($response)) { BubbleableMetadata::createFromRenderArray($response)->merge($early_rendering_bubbleable_metadata)->applyTo($response); } elseif ($response instanceof AjaxResponse) { $response->addAttachments($early_rendering_bubbleable_metadata->getAttachments()); // @todo Make AjaxResponse cacheable in // https://www.drupal.org/node/956186. Meanwhile, allow contrib // subclasses to be. if ($response instanceof CacheableResponseInterface) { $response->addCacheableDependency($early_rendering_bubbleable_metadata); } } elseif ($response instanceof AttachmentsInterface || $response instanceof CacheableResponseInterface || $response instanceof CacheableDependencyInterface) { throw new \LogicException(sprintf('The controller result claims to be providing relevant cache metadata, but leaked metadata was detected. Please ensure you are not rendering content too early. Returned object class: %s.', get_class($response))); } else { // A Response or domain object is returned that does not care about // attachments nor cacheability; for instance, a RedirectResponse. It is // safe to discard any early rendering metadata. } } return $response; }
/** * Creates a file, then tests the tokens generated from it. */ function testFileTokenReplacement() { $node_storage = $this->container->get('entity.manager')->getStorage('node'); $token_service = \Drupal::token(); $language_interface = \Drupal::languageManager()->getCurrentLanguage(); // Create file field. $type_name = 'article'; $field_name = 'field_' . strtolower($this->randomMachineName()); $this->createFileField($field_name, 'node', $type_name); $test_file = $this->getTestFile('text'); // Coping a file to test uploads with non-latin filenames. $filename = drupal_dirname($test_file->getFileUri()) . '/текстовый файл.txt'; $test_file = file_copy($test_file, $filename); // Create a new node with the uploaded file. $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); // Load the node and the file. $node_storage->resetCache(array($nid)); $node = $node_storage->load($nid); $file = File::load($node->{$field_name}->target_id); // Generate and test sanitized tokens. $tests = array(); $tests['[file:fid]'] = $file->id(); $tests['[file:name]'] = SafeMarkup::checkPlain($file->getFilename()); $tests['[file:path]'] = SafeMarkup::checkPlain($file->getFileUri()); $tests['[file:mime]'] = SafeMarkup::checkPlain($file->getMimeType()); $tests['[file:size]'] = format_size($file->getSize()); $tests['[file:url]'] = SafeMarkup::checkPlain(file_create_url($file->getFileUri())); $tests['[file:created]'] = format_date($file->getCreatedTime(), 'medium', '', NULL, $language_interface->getId()); $tests['[file:created:short]'] = format_date($file->getCreatedTime(), 'short', '', NULL, $language_interface->getId()); $tests['[file:changed]'] = format_date($file->getChangedTime(), 'medium', '', NULL, $language_interface->getId()); $tests['[file:changed:short]'] = format_date($file->getChangedTime(), 'short', '', NULL, $language_interface->getId()); $tests['[file:owner]'] = SafeMarkup::checkPlain(user_format_name($this->adminUser)); $tests['[file:owner:uid]'] = $file->getOwnerId(); $base_bubbleable_metadata = BubbleableMetadata::createFromObject($file); $metadata_tests = []; $metadata_tests['[file:fid]'] = $base_bubbleable_metadata; $metadata_tests['[file:name]'] = $base_bubbleable_metadata; $metadata_tests['[file:path]'] = $base_bubbleable_metadata; $metadata_tests['[file:mime]'] = $base_bubbleable_metadata; $metadata_tests['[file:size]'] = $base_bubbleable_metadata; $metadata_tests['[file:url]'] = $base_bubbleable_metadata; $bubbleable_metadata = clone $base_bubbleable_metadata; $metadata_tests['[file:created]'] = $bubbleable_metadata->addCacheTags(['rendered']); $metadata_tests['[file:created:short]'] = $bubbleable_metadata; $metadata_tests['[file:changed]'] = $bubbleable_metadata; $metadata_tests['[file:changed:short]'] = $bubbleable_metadata; $bubbleable_metadata = clone $base_bubbleable_metadata; $metadata_tests['[file:owner]'] = $bubbleable_metadata->addCacheTags(['user:2']); $metadata_tests['[file:owner:uid]'] = $bubbleable_metadata; // Test to make sure that we generated something for each token. $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { $bubbleable_metadata = new BubbleableMetadata(); $output = $token_service->replace($input, array('file' => $file), array('langcode' => $language_interface->getId()), $bubbleable_metadata); $this->assertEqual($output, $expected, format_string('Sanitized file token %token replaced.', array('%token' => $input))); $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]); } // Generate and test unsanitized tokens. $tests['[file:name]'] = $file->getFilename(); $tests['[file:path]'] = $file->getFileUri(); $tests['[file:mime]'] = $file->getMimeType(); $tests['[file:size]'] = format_size($file->getSize()); foreach ($tests as $input => $expected) { $output = $token_service->replace($input, array('file' => $file), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE)); $this->assertEqual($output, $expected, format_string('Unsanitized file token %token replaced.', array('%token' => $input))); } }
/** * Adds Ajax information about an element to communicate with JavaScript. * * If #ajax is set on an element, this additional JavaScript is added to the * page header to attach the Ajax behaviors. See ajax.js for more information. * * @param array $element * An associative array containing the properties of the element. * Properties used: * - #ajax['event'] * - #ajax['prevent'] * - #ajax['url'] * - #ajax['callback'] * - #ajax['options'] * - #ajax['wrapper'] * - #ajax['parameters'] * - #ajax['effect'] * - #ajax['accepts'] * * @return array * The processed element with the necessary JavaScript attached to it. */ public static function preRenderAjaxForm($element) { // Skip already processed elements. if (isset($element['#ajax_processed'])) { return $element; } // Initialize #ajax_processed, so we do not process this element again. $element['#ajax_processed'] = FALSE; // Nothing to do if there are no Ajax settings. if (empty($element['#ajax'])) { return $element; } // Add a reasonable default event handler if none was specified. if (isset($element['#ajax']) && !isset($element['#ajax']['event'])) { switch ($element['#type']) { case 'submit': case 'button': case 'image_button': // Pressing the ENTER key within a textfield triggers the click event of // the form's first submit button. Triggering Ajax in this situation // leads to problems, like breaking autocomplete textfields, so we bind // to mousedown instead of click. // @see https://www.drupal.org/node/216059 $element['#ajax']['event'] = 'mousedown'; // Retain keyboard accessibility by setting 'keypress'. This causes // ajax.js to trigger 'event' when SPACE or ENTER are pressed while the // button has focus. $element['#ajax']['keypress'] = TRUE; // Binding to mousedown rather than click means that it is possible to // trigger a click by pressing the mouse, holding the mouse button down // until the Ajax request is complete and the button is re-enabled, and // then releasing the mouse button. Set 'prevent' so that ajax.js binds // an additional handler to prevent such a click from triggering a // non-Ajax form submission. This also prevents a textfield's ENTER // press triggering this button's non-Ajax form submission behavior. if (!isset($element['#ajax']['prevent'])) { $element['#ajax']['prevent'] = 'click'; } break; case 'password': case 'textfield': case 'number': case 'tel': case 'textarea': $element['#ajax']['event'] = 'blur'; break; case 'radio': case 'checkbox': case 'select': $element['#ajax']['event'] = 'change'; break; case 'link': $element['#ajax']['event'] = 'click'; break; default: return $element; } } // Attach JavaScript settings to the element. if (isset($element['#ajax']['event'])) { $element['#attached']['library'][] = 'core/jquery.form'; $element['#attached']['library'][] = 'core/drupal.ajax'; $settings = $element['#ajax']; // Assign default settings. When 'url' is set to NULL, ajax.js submits the // Ajax request to the same URL as the form or link destination is for // someone with JavaScript disabled. This is generally preferred as a way to // ensure consistent server processing for js and no-js users, and Drupal's // content negotiation takes care of formatting the response appropriately. // However, 'url' and 'options' may be set when wanting server processing // to be substantially different for a JavaScript triggered submission. $settings += ['url' => NULL, 'options' => ['query' => []], 'dialogType' => 'ajax']; if (array_key_exists('callback', $settings) && !isset($settings['url'])) { $settings['url'] = Url::fromRoute('<current>'); // Add all the current query parameters in order to ensure that we build // the same form on the AJAX POST requests. For example, // \Drupal\user\AccountForm takes query parameters into account in order // to hide the password field dynamically. $settings['options']['query'] += \Drupal::request()->query->all(); $settings['options']['query'][FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE; } // @todo Legacy support. Remove in Drupal 8. if (isset($settings['method']) && $settings['method'] == 'replace') { $settings['method'] = 'replaceWith'; } // Convert \Drupal\Core\Url object to string. if (isset($settings['url']) && $settings['url'] instanceof Url) { $url = $settings['url']->setOptions($settings['options'])->toString(TRUE); BubbleableMetadata::createFromRenderArray($element)->merge($url)->applyTo($element); $settings['url'] = $url->getGeneratedUrl(); } else { $settings['url'] = NULL; } unset($settings['options']); // Add special data to $settings['submit'] so that when this element // triggers an Ajax submission, Drupal's form processing can determine which // element triggered it. // @see _form_element_triggered_scripted_submission() if (isset($settings['trigger_as'])) { // An element can add a 'trigger_as' key within #ajax to make the element // submit as though another one (for example, a non-button can use this // to submit the form as though a button were clicked). When using this, // the 'name' key is always required to identify the element to trigger // as. The 'value' key is optional, and only needed when multiple elements // share the same name, which is commonly the case for buttons. $settings['submit']['_triggering_element_name'] = $settings['trigger_as']['name']; if (isset($settings['trigger_as']['value'])) { $settings['submit']['_triggering_element_value'] = $settings['trigger_as']['value']; } unset($settings['trigger_as']); } elseif (isset($element['#name'])) { // Most of the time, elements can submit as themselves, in which case the // 'trigger_as' key isn't needed, and the element's name is used. $settings['submit']['_triggering_element_name'] = $element['#name']; // If the element is a (non-image) button, its name may not identify it // uniquely, in which case a match on value is also needed. // @see _form_button_was_clicked() if (!empty($element['#is_button']) && empty($element['#has_garbage_value'])) { $settings['submit']['_triggering_element_value'] = $element['#value']; } } // Convert a simple #ajax['progress'] string into an array. if (isset($settings['progress']) && is_string($settings['progress'])) { $settings['progress'] = array('type' => $settings['progress']); } // Change progress path to a full URL. if (isset($settings['progress']['url']) && $settings['progress']['url'] instanceof Url) { $settings['progress']['url'] = $settings['progress']['url']->toString(); } $element['#attached']['drupalSettings']['ajax'][$element['#id']] = $settings; $element['#attached']['drupalSettings']['ajaxTrustedUrl'][$settings['url']] = TRUE; // Indicate that Ajax processing was successful. $element['#ajax_processed'] = TRUE; } return $element; }
/** * {@inheritdoc} */ public function mergeBubbleableMetadata(array $a, array $b) { $meta_a = BubbleableMetadata::createFromRenderArray($a); $meta_b = BubbleableMetadata::createFromRenderArray($b); $meta_a->merge($meta_b)->applyTo($a); return $a; }