core/core.services.yml | 5 + .../EarlyRenderingControllerWrapperSubscriber.php | 166 ++++++++++++++++++ .../Core/Render/MainContent/HtmlRenderer.php | 5 +- core/lib/Drupal/Core/Render/RenderContext.php | 62 +++++++ core/lib/Drupal/Core/Render/Renderer.php | 136 ++++++++------- core/lib/Drupal/Core/Render/RendererInterface.php | 59 +++++-- .../aggregator/src/Tests/Views/IntegrationTest.php | 19 ++- .../src/Tests/Views/CommentFieldNameTest.php | 13 +- .../src/Tests/Views/ExtensionViewsFieldTest.php | 26 ++- core/modules/filter/src/Tests/FilterUnitTest.php | 31 ++-- .../modules/node/src/Controller/NodeController.php | 7 +- .../rest/src/Plugin/views/display/RestExport.php | 51 +++++- core/modules/simpletest/src/AssertContentTrait.php | 8 +- .../Tests/Common/EarlyRenderingControllerTest.php | 106 ++++++++++++ core/modules/system/src/Tests/Common/UrlTest.php | 9 +- .../early_rendering_controller_test.info.yml | 6 + .../early_rendering_controller_test.routing.yml | 97 +++++++++++ .../early_rendering_controller_test.services.yml | 5 + .../src/AttachmentsTestDomainObject.php | 17 ++ .../src/AttachmentsTestResponse.php | 18 ++ .../src/CacheableTestDomainObject.php | 35 ++++ .../src/CacheableTestResponse.php | 18 ++ .../src/EarlyRenderingTestController.php | 129 ++++++++++++++ .../src/TestDomainObject.php | 10 ++ .../src/TestDomainObjectViewSubscriber.php | 51 ++++++ .../src/Tests/Views/TaxonomyFieldTidTest.php | 8 +- .../src/Tests/Views/HandlerFieldUserNameTest.php | 23 ++- .../views/src/Tests/Handler/FieldGroupRowsTest.php | 24 ++- .../views/src/Tests/Handler/FieldUnitTest.php | 189 ++++++++++++++++----- .../views/src/Tests/Handler/FieldWebTest.php | 139 +++++++++++---- core/modules/views/src/Tests/Plugin/CacheTest.php | 9 +- 31 files changed, 1282 insertions(+), 199 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index 0fe0a7f..a0122c5 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1392,6 +1392,11 @@ services: renderer: class: Drupal\Core\Render\Renderer arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info', '@render_cache', '%renderer.config%'] + early_rendering_controller_wrapper_subscriber: + class: Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber + arguments: ['@controller_resolver', '@renderer'] + tags: + - { name: event_subscriber } email.validator: class: Egulias\EmailValidator\EmailValidator diff --git a/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php new file mode 100644 index 0000000..9dbe488 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php @@ -0,0 +1,166 @@ +controllerResolver = $controller_resolver; + $this->renderer = $renderer; + } + + /** + * Ensures bubbleable metadata from early rendering is not lost. + * + * @param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event + * The controller event. + */ + public function onController(FilterControllerEvent $event) { + $controller = $event->getController(); + + // See \Symfony\Component\HttpKernel\HttpKernel::handleRaw(). + $arguments = $this->controllerResolver->getArguments($event->getRequest(), $controller); + + $event->setController(function() use ($controller, $arguments) { + return $this->wrapControllerExecutionInRenderContext($controller, $arguments); + }); + } + + /** + * 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()) { + // If a render array is returned by the controller, merge the "lost" + // bubbleable metadata. + if (is_array($response)) { + $early_rendering_bubbleable_metadata = $context->pop(); + BubbleableMetadata::createFromRenderArray($response) + ->merge($early_rendering_bubbleable_metadata) + ->applyTo($response); + } + // If a Response or domain object is returned, and it cares about + // attachments or cacheability, then throw an exception: early rendering + // is not permitted in that case. It is the developer's responsibility + // to not use early rendering. + 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. E.g. a RedirectResponse. It is safe to + // discard any early rendering metadata. + } + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[KernelEvents::CONTROLLER][] = ['onController']; + + return $events; + } + +} diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php index cb987f4..10d2ce9 100644 --- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -14,6 +14,7 @@ use Drupal\Core\Render\HtmlResponse; use Drupal\Core\Render\PageDisplayVariantSelectionEvent; use Drupal\Core\Render\RenderCacheInterface; +use Drupal\Core\Render\RenderContext; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Render\RenderEvents; use Drupal\Core\Routing\RouteMatchInterface; @@ -181,7 +182,9 @@ protected function prepare(array $main_content, Request $request, RouteMatchInte // ::renderResponse(). // @todo Remove this once https://www.drupal.org/node/2359901 lands. if (!empty($main_content)) { - $this->renderer->render($main_content, FALSE); + $this->renderer->executeInRenderContext(new RenderContext(), function() use (&$main_content) { + return $this->renderer->render($main_content, FALSE); + }); $main_content = $this->renderCache->getCacheableRenderArray($main_content) + [ '#title' => isset($main_content['#title']) ? $main_content['#title'] : NULL ]; diff --git a/core/lib/Drupal/Core/Render/RenderContext.php b/core/lib/Drupal/Core/Render/RenderContext.php new file mode 100644 index 0000000..e49c291 --- /dev/null +++ b/core/lib/Drupal/Core/Render/RenderContext.php @@ -0,0 +1,62 @@ +pop(); + // Update the frame, but also update the current element, to ensure it + // contains up-to-date information in case it gets render cached. + $updated_frame = BubbleableMetadata::createFromRenderArray($element)->merge($frame); + $updated_frame->applyTo($element); + $this->push($updated_frame); + } + + /** + * Bubbles the stack. + * + * Whenever another level in the render array has been rendered, the stack + * must be bubbled, to merge its rendering metadata with that of the parent + * element. + */ + public function bubble() { + // If there's only one frame on the stack, then this is the root call, and + // we can't bubble up further. ::renderRoot() will reset the stack, but we + // must not reset it here to allow users of ::executeInRenderContext() to + // access the stack directly. + if ($this->count() === 1) { + return; + } + + // Merge the current and the parent stack frame. + $current = $this->pop(); + $parent = $this->pop(); + $this->push($current->merge($parent)); + } + +} diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 858501b..d98955d 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -58,11 +58,24 @@ class Renderer implements RendererInterface { protected $rendererConfig; /** - * The stack containing bubbleable rendering metadata. + * The render context. * - * @var \SplStack|null + * This must be static as long as some controllers rebuild the container + * during a request. This causes multiple renderer instances to co-exist + * simultaneously, render state getting lost, and therefore causing pages to + * fail to render correctly. As soon as it is guaranteed that during a request + * the same container is used, it no longer needs to be static. + * + * @var \Drupal\Core\Render\RenderContext|null + */ + protected static $context; + + /** + * Whether we're currently in a ::renderRoot() call. + * + * @var bool */ - protected static $stack; + protected $isRenderingRoot = FALSE; /** * Constructs a new Renderer. @@ -90,18 +103,29 @@ public function __construct(ControllerResolverInterface $controller_resolver, Th * {@inheritdoc} */ public function renderRoot(&$elements) { - return $this->render($elements, TRUE); + // Disallow calling ::renderRoot() from within another ::renderRoot() call. + if ($this->isRenderingRoot) { + $this->isRenderingRoot = FALSE; + throw new \LogicException('A stray renderRoot() invocation is causing bubbling of attached assets to break.'); + } + + // Render in its own render context. + $this->isRenderingRoot = TRUE; + $output = $this->executeInRenderContext(new RenderContext(), function () use (&$elements) { + return $this->render($elements, TRUE); + }); + $this->isRenderingRoot = FALSE; + + return $output; } /** * {@inheritdoc} */ public function renderPlain(&$elements) { - $current_stack = static::$stack; - $this->resetStack(); - $output = $this->renderRoot($elements); - static::$stack = $current_stack; - return $output; + return $this->executeInRenderContext(new RenderContext(), function () use (&$elements) { + return $this->render($elements, TRUE); + }); } /** @@ -151,16 +175,17 @@ public function render(&$elements, $is_root_call = FALSE) { // possible that any of them throw an exception that will cause a different // page to be rendered (e.g. throwing // \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will cause - // the 404 page to be rendered). That page might also use Renderer::render() - // but if exceptions aren't caught here, the stack will be left in an - // inconsistent state. - // Hence, catch all exceptions and reset the stack and re-throw them. + // the 404 page to be rendered). That page might also use + // Renderer::renderRoot() but if exceptions aren't caught here, it will be + // impossible to call Renderer::renderRoot() again. + // Hence, catch all exceptions, reset the isRenderingRoot property and + // re-throw exceptions. try { return $this->doRender($elements, $is_root_call); } catch (\Exception $e) { - // Reset stack and re-throw exception. - $this->resetStack(); + // Mark the ::rootRender() call finished due to this exception & re-throw. + $this->isRenderingRoot = FALSE; throw $e; } } @@ -200,10 +225,10 @@ protected function doRender(&$elements, $is_root_call = FALSE) { return ''; } - if (!isset(static::$stack)) { - static::$stack = new \SplStack(); + if (!isset(static::$context)) { + throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead."); } - static::$stack->push(new BubbleableMetadata()); + static::$context->push(new BubbleableMetadata()); // Set the bubbleable rendering metadata that has configurable defaults, if: // - this is the root call, to ensure that the final render array definitely @@ -244,10 +269,10 @@ protected function doRender(&$elements, $is_root_call = FALSE) { } // The render cache item contains all the bubbleable rendering metadata // for the subtree. - $this->updateStack($elements); + static::$context->update($elements); // Render cache hit, so rendering is finished, all necessary info // collected! - $this->bubbleStack(); + static::$context->bubble(); return $elements['#markup']; } } @@ -345,9 +370,9 @@ protected function doRender(&$elements, $is_root_call = FALSE) { if (!empty($elements['#printed'])) { // The #printed element contains all the bubbleable rendering metadata for // the subtree. - $this->updateStack($elements); + static::$context->update($elements); // #printed, so rendering is finished, all necessary info collected! - $this->bubbleStack(); + static::$context->bubble(); return ''; } @@ -473,8 +498,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $elements['#markup'] = $prefix . $elements['#children'] . $suffix; - // We've rendered this element (and its subtree!), now update the stack. - $this->updateStack($elements); + // We've rendered this element (and its subtree!), now update the context. + static::$context->update($elements); // Cache the processed element if both $pre_bubbling_elements and $elements // have the metadata necessary to generate a cache ID. @@ -496,13 +521,14 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // that is handled earlier in Renderer::render(). if ($is_root_call) { $this->replacePlaceholders($elements); - if (static::$stack->count() !== 1) { + // @todo remove as part of https://www.drupal.org/node/2511330. + if (static::$context->count() !== 1) { throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); } } // Rendering is finished, all necessary info collected! - $this->bubbleStack(); + static::$context->bubble(); $elements['#printed'] = TRUE; $elements['#markup'] = SafeMarkup::set($elements['#markup']); @@ -510,52 +536,24 @@ protected function doRender(&$elements, $is_root_call = FALSE) { } /** - * Resets the renderer service's internal stack (used for bubbling metadata). - * - * Only necessary in very rare/advanced situations, such as when rendering an - * error page if an exception occurred *during* rendering. - */ - protected function resetStack() { - static::$stack = NULL; - } - - /** - * Updates the stack. - * - * @param array &$element - * The element of the render array that has just been rendered. The stack - * frame for this element will be updated with the bubbleable rendering - * metadata of this element. - */ - protected function updateStack(&$element) { - // The latest frame represents the bubbleable metadata for the subtree. - $frame = static::$stack->pop(); - // Update the frame, but also update the current element, to ensure it - // contains up-to-date information in case it gets render cached. - $updated_frame = BubbleableMetadata::createFromRenderArray($element)->merge($frame); - $updated_frame->applyTo($element); - static::$stack->push($updated_frame); - } - - /** - * Bubbles the stack. - * - * Whenever another level in the render array has been rendered, the stack - * must be bubbled, to merge its rendering metadata with that of the parent - * element. + * {@inheritdoc} */ - protected function bubbleStack() { - // If there's only one frame on the stack, then this is the root call, and - // we can't bubble up further. Reset the stack for the next root call. - if (static::$stack->count() === 1) { - $this->resetStack(); - return; + public function executeInRenderContext(RenderContext $context, callable $callable) { + // Store the current render context. + $current_context = static::$context; + + // Set the provided context and call the callable, it will use that context. + static::$context = $context; + $result = $callable(); + // @todo Convert to an assertion in https://www.drupal.org/node/2408013 + if (static::$context->count() > 1) { + throw new \LogicException('Bubbling failed.'); } - // Merge the current and the parent stack frame. - $current = static::$stack->pop(); - $parent = static::$stack->pop(); - static::$stack->push($current->merge($parent)); + // Restore the original render context. + static::$context = $current_context; + + return $result; } /** diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php index 5d59d4d..b55966f 100644 --- a/core/lib/Drupal/Core/Render/RendererInterface.php +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -22,6 +22,8 @@ * - system internals that are responsible for rendering the final HTML * - render arrays for non-HTML responses, such as feeds * + * (Cannot be executed within another render context.) + * * @param array $elements * The structured array describing the data to be rendered. * @@ -29,6 +31,9 @@ * The rendered HTML. * * @see ::render() + * + * @throws \LogicException + * When called from inside another renderRoot() call. */ public function renderRoot(&$elements); @@ -45,9 +50,11 @@ public function renderRoot(&$elements); * ::renderRoot() call, but that is generally highly problematic (and hence an * exception is thrown when a ::renderRoot() call happens within another * ::renderRoot() call). However, in this case, we only care about the output, - * not about the bubbling. Hence this uses a separate render stack, to not + * not about the bubbling. Hence this uses a separate render context, to not * affect the parent ::renderRoot() call. * + * (Can be executed within another render context: it runs in isolation.) + * * @param array $elements * The structured array describing the data to be rendered. * @@ -88,8 +95,8 @@ public function renderPlain(&$elements); * or configuration that can affect that rendering changes. * - Placeholders, with associated self-contained placeholder render arrays, * for executing code to handle dynamic requirements that cannot be cached. - * A stack of \Drupal\Core\Render\BubbleableMetadata objects can be used to - * perform this bubbling. + * A render context (\Drupal\Core\Render\RenderContext) can be used to perform + * bubbling; it is a stack of \Drupal\Core\Render\BubbleableMetadata objects. * * Additionally, whether retrieving from cache or not, it is important to * know all of the assets (CSS and JavaScript) required by the rendered HTML, @@ -103,9 +110,9 @@ public function renderPlain(&$elements); * - If this element has already been printed (#printed = TRUE) or the user * does not have access to it (#access = FALSE), then an empty string is * returned. - * - If no stack data structure has been created yet, it is done now. Next, + * - If no render context is set yet, an exception is thrown. Otherwise, * an empty \Drupal\Core\Render\BubbleableMetadata is pushed onto the - * stack. + * render context. * - If this element has #cache defined then the cached markup for this * element will be returned if it exists in Renderer::render()'s cache. To * use Renderer::render() caching, set the element's #cache property to an @@ -299,13 +306,12 @@ public function renderPlain(&$elements); * The rendered HTML. * * @throws \LogicException - * If a root call to ::render() does not result in an empty stack, this - * indicates an erroneous ::render() root call (a root call within a - * root call, which makes no sense). Therefore, a logic exception is thrown. + * When called outside of a render context. (i.e. outside of a renderRoot(), + * renderPlain() or executeInRenderContext() call.) * @throws \Exception - * If a #pre_render callback throws an exception, it is caught to reset the - * stack used for bubbling rendering metadata, and then the exception is re- - * thrown. + * If a #pre_render callback throws an exception, it is caught to mark the + * renderer as no longer being in a root render call, if any. Then the + * exception is rethrown. * * @see \Drupal\Core\Render\ElementInfoManagerInterface::getInfo() * @see \Drupal\Core\Theme\ThemeManagerInterface::render() @@ -316,6 +322,37 @@ public function renderPlain(&$elements); public function render(&$elements, $is_root_call = FALSE); /** + * Executes a callable within a render context. + * + * Only for very advanced use cases. Prefer using ::renderRoot() and + * ::renderPlain() instead. + * + * All rendering must happen within a render context. Within a render context, + * all bubbleable metadata is bubbled and hence tracked. Outside of a render + * context, it would be lost. This could lead to missing assets, incorrect + * cache variations (and thus security issues), insufficient cache + * invalidations, and so on. + * + * Any and all rendering must therefore happen within a render context, and it + * is this method that provides that. + * + * @see \Drupal\Core\Render\BubbleableMetadata + * + * @param \Drupal\Core\Render\RenderContext $context + * The render context to execute the callable within. + * @param callable $callable + * The callable to execute. + * @return mixed + * The callable's return value. + * + * @see \Drupal\Core\Render\RenderContext + * + * @throws \LogicException + * In case bubbling has failed, can only happen in case of broken code. + */ + public function executeInRenderContext(RenderContext $context, callable $callable); + + /** * Merges the bubbleable rendering metadata o/t 2nd render array with the 1st. * * @param array $a diff --git a/core/modules/aggregator/src/Tests/Views/IntegrationTest.php b/core/modules/aggregator/src/Tests/Views/IntegrationTest.php index 1ec27b9..9444808 100644 --- a/core/modules/aggregator/src/Tests/Views/IntegrationTest.php +++ b/core/modules/aggregator/src/Tests/Views/IntegrationTest.php @@ -7,6 +7,7 @@ namespace Drupal\aggregator\Tests\Views; +use Drupal\Core\Render\RenderContext; use Drupal\Core\Url; use Drupal\views\Views; use Drupal\views\Tests\ViewTestData; @@ -66,6 +67,9 @@ protected function setUp() { * Tests basic aggregator_item view. */ public function testAggregatorItemView() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $feed = $this->feedStorage->create(array( 'title' => $this->randomMachineName(), 'url' => 'https://www.drupal.org/', @@ -112,13 +116,22 @@ public function testAggregatorItemView() { foreach ($view->result as $row) { $iid = $view->field['iid']->getValue($row); $expected_link = \Drupal::l($items[$iid]->getTitle(), Url::fromUri($items[$iid]->getLink(), ['absolute' => TRUE])); - $this->assertEqual($view->field['title']->advancedRender($row), $expected_link, 'Ensure the right link is generated'); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view, $row) { + return $view->field['title']->advancedRender($row); + }); + $this->assertEqual($output, $expected_link, 'Ensure the right link is generated'); $expected_author = aggregator_filter_xss($items[$iid]->getAuthor()); - $this->assertEqual($view->field['author']->advancedRender($row), $expected_author, 'Ensure the author got filtered'); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view, $row) { + return $view->field['author']->advancedRender($row); + }); + $this->assertEqual($output, $expected_author, 'Ensure the author got filtered'); $expected_description = aggregator_filter_xss($items[$iid]->getDescription()); - $this->assertEqual($view->field['description']->advancedRender($row), $expected_description, 'Ensure the author got filtered'); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view, $row) { + return $view->field['description']->advancedRender($row); + }); + $this->assertEqual($output, $expected_description, 'Ensure the author got filtered'); } } diff --git a/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php b/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php index ab40e6f..2fcf468 100644 --- a/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php +++ b/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php @@ -8,6 +8,7 @@ namespace Drupal\comment\Tests\Views; use Drupal\comment\Entity\Comment; +use Drupal\Core\Render\RenderContext; use Drupal\Core\Session\AnonymousUserSession; use Drupal\user\RoleInterface; use Drupal\views\Views; @@ -58,6 +59,8 @@ protected function setUp() { * Test comment field name. */ public function testCommentFieldName() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); $view = Views::getView('test_comment_field_name'); $this->executeView($view); @@ -85,8 +88,14 @@ public function testCommentFieldName() { $view = Views::getView('test_comment_field_name'); $this->executeView($view); // Test that data rendered. - $this->assertIdentical($this->comment->getFieldName(), $view->field['field_name']->advancedRender($view->result[0])); - $this->assertIdentical($this->customComment->getFieldName(), $view->field['field_name']->advancedRender($view->result[1])); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['field_name']->advancedRender($view->result[0]); + }); + $this->assertIdentical($this->comment->getFieldName(), $output); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['field_name']->advancedRender($view->result[1]); + }); + $this->assertIdentical($this->customComment->getFieldName(), $output); } } diff --git a/core/modules/file/src/Tests/Views/ExtensionViewsFieldTest.php b/core/modules/file/src/Tests/Views/ExtensionViewsFieldTest.php index 75623f4..f7d2b3f 100644 --- a/core/modules/file/src/Tests/Views/ExtensionViewsFieldTest.php +++ b/core/modules/file/src/Tests/Views/ExtensionViewsFieldTest.php @@ -7,6 +7,7 @@ namespace Drupal\file\Tests\Views; +use Drupal\Core\Render\RenderContext; use Drupal\file\Entity\File; use Drupal\views\Views; use Drupal\views\Tests\ViewUnitTestBase; @@ -69,17 +70,22 @@ protected function setUp() { * Tests file extension views field handler extension_detect_tar option. */ public function testFileExtensionTarOption() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $view = Views::getView('file_extension_view'); $view->setDisplay(); $this->executeView($view); // Test without the tar option. - $this->assertEqual($view->field['extension']->advancedRender($view->result[0]), 'png'); - $this->assertEqual($view->field['extension']->advancedRender($view->result[1]), 'tar'); - $this->assertEqual($view->field['extension']->advancedRender($view->result[2]), 'gz'); - $this->assertEqual($view->field['extension']->advancedRender($view->result[3]), ''); - // Test with the tar option. + $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + $this->assertEqual($view->field['extension']->advancedRender($view->result[0]), 'png'); + $this->assertEqual($view->field['extension']->advancedRender($view->result[1]), 'tar'); + $this->assertEqual($view->field['extension']->advancedRender($view->result[2]), 'gz'); + $this->assertEqual($view->field['extension']->advancedRender($view->result[3]), ''); + }); + // Test with the tar option. $view = Views::getView('file_extension_view'); $view->setDisplay(); $view->initHandlers(); @@ -87,10 +93,12 @@ public function testFileExtensionTarOption() { $view->field['extension']->options['settings']['extension_detect_tar'] = TRUE; $this->executeView($view); - $this->assertEqual($view->field['extension']->advancedRender($view->result[0]), 'png'); - $this->assertEqual($view->field['extension']->advancedRender($view->result[1]), 'tar'); - $this->assertEqual($view->field['extension']->advancedRender($view->result[2]), 'tar.gz'); - $this->assertEqual($view->field['extension']->advancedRender($view->result[3]), ''); + $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + $this->assertEqual($view->field['extension']->advancedRender($view->result[0]), 'png'); + $this->assertEqual($view->field['extension']->advancedRender($view->result[1]), 'tar'); + $this->assertEqual($view->field['extension']->advancedRender($view->result[2]), 'tar.gz'); + $this->assertEqual($view->field['extension']->advancedRender($view->result[3]), ''); + }); } } diff --git a/core/modules/filter/src/Tests/FilterUnitTest.php b/core/modules/filter/src/Tests/FilterUnitTest.php index 7fdf8f9..8e6c5db 100644 --- a/core/modules/filter/src/Tests/FilterUnitTest.php +++ b/core/modules/filter/src/Tests/FilterUnitTest.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Render\RenderContext; use Drupal\editor\EditorXssFilter\Standard; use Drupal\filter\Entity\FilterFormat; use Drupal\filter\FilterPluginCollection; @@ -101,10 +102,14 @@ function testAlignFilter() { * Tests the caption filter. */ function testCaptionFilter() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); $filter = $this->filters['filter_caption']; - $test = function($input) use ($filter) { - return $filter->process($input, 'und'); + $test = function($input) use ($filter, $renderer) { + return $renderer->executeInRenderContext(new RenderContext(), function () use ($input, $filter) { + return $filter->process($input, 'und'); + }); }; $attached_library = array( @@ -190,12 +195,14 @@ function testCaptionFilter() { 'filter_html_nofollow' => 0, ) )); - $test_with_html_filter = function ($input) use ($filter, $html_filter) { - // 1. Apply HTML filter's processing step. - $output = $html_filter->process($input, 'und'); - // 2. Apply caption filter's processing step. - $output = $filter->process($output, 'und'); - return $output->getProcessedText(); + $test_with_html_filter = function ($input) use ($filter, $html_filter, $renderer) { + return $renderer->executeInRenderContext(new RenderContext(), function () use ($input, $filter, $html_filter) { + // 1. Apply HTML filter's processing step. + $output = $html_filter->process($input, 'und'); + // 2. Apply caption filter's processing step. + $output = $filter->process($output, 'und'); + return $output->getProcessedText(); + }); }; // Editor XSS filter. $test_editor_xss_filter = function ($input) { @@ -252,11 +259,15 @@ function testCaptionFilter() { * Tests the combination of the align and caption filters. */ function testAlignAndCaptionFilters() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); $align_filter = $this->filters['filter_align']; $caption_filter = $this->filters['filter_caption']; - $test = function($input) use ($align_filter, $caption_filter) { - return $caption_filter->process($align_filter->process($input, 'und'), 'und'); + $test = function($input) use ($align_filter, $caption_filter, $renderer) { + return $renderer->executeInRenderContext(new RenderContext(), function () use ($input, $align_filter, $caption_filter) { + return $caption_filter->process($align_filter->process($input, 'und'), 'und'); + }); }; $attached_library = array( diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php index 5889912..b8e4918 100644 --- a/core/modules/node/src/Controller/NodeController.php +++ b/core/modules/node/src/Controller/NodeController.php @@ -188,17 +188,20 @@ public function revisionOverview(NodeInterface $node) { } $row = []; - $row[] = [ + $column = [ 'data' => [ '#type' => 'inline_template', '#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}

{{ message }}

{% endif %}', '#context' => [ 'date' => $link, - 'username' => $this->renderer->render($username), + 'username' => $this->renderer->renderPlain($username), 'message' => Xss::filter($revision->revision_log->value), ], ], ]; + // @todo Simplify once https://www.drupal.org/node/2334319 lands. + $this->renderer->addCacheableDependency($column['data'], $username); + $row[] = $column; if ($vid == $node->getRevisionId()) { $row[0]['class'] = ['revision-current']; diff --git a/core/modules/rest/src/Plugin/views/display/RestExport.php b/core/modules/rest/src/Plugin/views/display/RestExport.php index 247b60c..ae5ac7e 100644 --- a/core/modules/rest/src/Plugin/views/display/RestExport.php +++ b/core/modules/rest/src/Plugin/views/display/RestExport.php @@ -10,9 +10,14 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheableResponse; +use Drupal\Core\Render\RenderContext; +use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Routing\RouteProviderInterface; +use Drupal\Core\State\StateInterface; use Drupal\views\Plugin\views\display\ResponseDisplayPluginInterface; use Drupal\views\ViewExecutable; use Drupal\views\Plugin\views\display\PathPluginBase; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Routing\RouteCollection; /** @@ -71,6 +76,48 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac protected $mimeType; /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * Constructs a RestExport object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider + * The route provider. + * @param \Drupal\Core\State\StateInterface $state + * The state key value store. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, RendererInterface $renderer) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider, $state); + + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('router.route_provider'), + $container->get('state'), + $container->get('renderer') + ); + } + /** * {@inheritdoc} */ public function initDisplay(ViewExecutable $view, array &$display, array &$options = NULL) { @@ -263,7 +310,9 @@ public function execute() { */ public function render() { $build = array(); - $build['#markup'] = $this->view->style_plugin->render(); + $build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function() { + return $this->view->style_plugin->render(); + }); $this->view->element['#content_type'] = $this->getMimeType(); $this->view->element['#cache_properties'][] = '#content_type'; diff --git a/core/modules/simpletest/src/AssertContentTrait.php b/core/modules/simpletest/src/AssertContentTrait.php index b043ea9..3cce705 100644 --- a/core/modules/simpletest/src/AssertContentTrait.php +++ b/core/modules/simpletest/src/AssertContentTrait.php @@ -10,6 +10,7 @@ use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Xss; +use Drupal\Core\Render\RenderContext; use Symfony\Component\CssSelector\CssSelector; /** @@ -808,7 +809,12 @@ protected function assertNoTitle($title, $message = '', $group = 'Other') { * TRUE on pass, FALSE on fail. */ protected function assertThemeOutput($callback, array $variables = array(), $expected = '', $message = '', $group = 'Other') { - $output = \Drupal::theme()->render($callback, $variables); + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + + $output = $renderer->executeInRenderContext(new RenderContext(), function() use ($callback, $variables) { + return \Drupal::theme()->render($callback, $variables); + }); $this->verbose( '
' . 'Result:' . '
' . SafeMarkup::checkPlain(var_export($output, TRUE)) . '
' . '
' . 'Expected:' . '
' . SafeMarkup::checkPlain(var_export($expected, TRUE)) . '
' diff --git a/core/modules/system/src/Tests/Common/EarlyRenderingControllerTest.php b/core/modules/system/src/Tests/Common/EarlyRenderingControllerTest.php new file mode 100644 index 0000000..1759880 --- /dev/null +++ b/core/modules/system/src/Tests/Common/EarlyRenderingControllerTest.php @@ -0,0 +1,106 @@ +drupalGet(Url::fromRoute('early_rendering_controller_test.render_array')); + $this->assertResponse(200); + $this->assertRaw('Hello world!'); + $this->assertCacheTag('foo'); + $this->drupalGet(Url::fromRoute('early_rendering_controller_test.render_array.early')); + $this->assertResponse(200); + $this->assertRaw('Hello world!'); + $this->assertCacheTag('foo'); + + // Basic Response object: non-early & early. + $this->drupalGet(Url::fromRoute('early_rendering_controller_test.response')); + $this->assertResponse(200); + $this->assertRaw('Hello world!'); + $this->assertNoCacheTag('foo'); + $this->drupalGet(Url::fromRoute('early_rendering_controller_test.response.early')); + $this->assertResponse(200); + $this->assertRaw('Hello world!'); + $this->assertNoCacheTag('foo'); + + // Response object with attachments: non-early & early. + $this->drupalGet(Url::fromRoute('early_rendering_controller_test.response-with-attachments')); + $this->assertResponse(200); + $this->assertRaw('Hello world!'); + $this->assertNoCacheTag('foo'); + $this->drupalGet(Url::fromRoute('early_rendering_controller_test.response-with-attachments.early')); + $this->assertResponse(500); + $this->assertRaw('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: Drupal\early_rendering_controller_test\AttachmentsTestResponse.'); + + // Cacheable Response object: non-early & early. + $this->drupalGet(Url::fromRoute('early_rendering_controller_test.cacheable-response')); + $this->assertResponse(200); + $this->assertRaw('Hello world!'); + $this->assertNoCacheTag('foo'); + $this->drupalGet(Url::fromRoute('early_rendering_controller_test.cacheable-response.early')); + $this->assertResponse(500); + $this->assertRaw('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: Drupal\early_rendering_controller_test\CacheableTestResponse.'); + + // Basic domain object: non-early & early. + $this->drupalGet(Url::fromRoute('early_rendering_controller_test.domain-object')); + $this->assertResponse(200); + $this->assertRaw('TestDomainObject'); + $this->assertNoCacheTag('foo'); + $this->drupalGet(Url::fromRoute('early_rendering_controller_test.domain-object.early')); + $this->assertResponse(200); + $this->assertRaw('TestDomainObject'); + $this->assertNoCacheTag('foo'); + + // Basic domain object with attachments: non-early & early. + $this->drupalGet(Url::fromRoute('early_rendering_controller_test.domain-object-with-attachments')); + $this->assertResponse(200); + $this->assertRaw('AttachmentsTestDomainObject'); + $this->assertNoCacheTag('foo'); + $this->drupalGet(Url::fromRoute('early_rendering_controller_test.domain-object-with-attachments.early')); + $this->assertResponse(500); + $this->assertRaw('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: Drupal\early_rendering_controller_test\AttachmentsTestDomainObject.'); + + // Cacheable Response object: non-early & early. + $this->drupalGet(Url::fromRoute('early_rendering_controller_test.cacheable-domain-object')); + $this->assertResponse(200); + $this->assertRaw('CacheableTestDomainObject'); + $this->assertNoCacheTag('foo'); + $this->drupalGet(Url::fromRoute('early_rendering_controller_test.cacheable-domain-object.early')); + $this->assertResponse(500); + $this->assertRaw('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: Drupal\early_rendering_controller_test\CacheableTestDomainObject.'); + + // The exceptions are expected. Do not interpret them as a test failure. + // Not using File API; a potential error must trigger a PHP warning. + unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log'); + } + +} diff --git a/core/modules/system/src/Tests/Common/UrlTest.php b/core/modules/system/src/Tests/Common/UrlTest.php index 74d9129..1a26232 100644 --- a/core/modules/system/src/Tests/Common/UrlTest.php +++ b/core/modules/system/src/Tests/Common/UrlTest.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Cache\Cache; use Drupal\Core\Language\Language; +use Drupal\Core\Render\RenderContext; use Drupal\Core\Url; use Drupal\simpletest\WebTestBase; @@ -168,9 +169,11 @@ function testLinkRenderArrayText() { $l = \Drupal::l('foo', Url::fromUri('https://www.drupal.org')); // Test a renderable array passed to _l(). - $renderable_text = array('#markup' => 'foo'); - $l_renderable_text = \Drupal::l($renderable_text, Url::fromUri('https://www.drupal.org')); - $this->assertEqual($l_renderable_text, $l); + $renderer->executeInRenderContext(new RenderContext(), function() use ($renderer, $l) { + $renderable_text = array('#markup' => 'foo'); + $l_renderable_text = \Drupal::l($renderable_text, Url::fromUri('https://www.drupal.org')); + $this->assertEqual($l_renderable_text, $l); + }); // Test a themed link with plain text 'text'. $type_link_plain_array = array( diff --git a/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.info.yml b/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.info.yml new file mode 100644 index 0000000..44ab452 --- /dev/null +++ b/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.info.yml @@ -0,0 +1,6 @@ +name: 'Early rendering controller test' +type: module +description: 'Support module for EarlyRenderingControllerTest.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.routing.yml b/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.routing.yml new file mode 100644 index 0000000..4e050e4 --- /dev/null +++ b/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.routing.yml @@ -0,0 +1,97 @@ +# Controller returning a render array. +early_rendering_controller_test.render_array: + path: '/early-rendering-controller-test/render-array' + defaults: + _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::renderArray' + requirements: + _access: 'TRUE' +early_rendering_controller_test.render_array.early: + path: '/early-rendering-controller-test/render-array/early' + defaults: + _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::renderArrayEarly' + requirements: + _access: 'TRUE' + +# Controller returning a basic Response object. +early_rendering_controller_test.response: + path: '/early-rendering-controller-test/response' + defaults: + _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::response' + requirements: + _access: 'TRUE' +early_rendering_controller_test.response.early: + path: '/early-rendering-controller-test/response/early' + defaults: + _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::responseEarly' + requirements: + _access: 'TRUE' + +# Controller returning a Response object with attachments. +early_rendering_controller_test.response-with-attachments: + path: '/early-rendering-controller-test/response-with-attachments' + defaults: + _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::responseWithAttachments' + requirements: + _access: 'TRUE' +early_rendering_controller_test.response-with-attachments.early: + path: '/early-rendering-controller-test/response-with-attachments/early' + defaults: + _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::responseWithAttachmentsEarly' + requirements: + _access: 'TRUE' + +# Controller returning a cacheable Response object. +early_rendering_controller_test.cacheable-response: + path: '/early-rendering-controller-test/cacheable-response' + defaults: + _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::cacheableResponse' + requirements: + _access: 'TRUE' +early_rendering_controller_test.cacheable-response.early: + path: '/early-rendering-controller-test/cacheable-response/early' + defaults: + _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::cacheableResponseEarly' + requirements: + _access: 'TRUE' + +# Controller returning a basic domain object. +early_rendering_controller_test.domain-object: + path: '/early-rendering-controller-test/domain-object' + defaults: + _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::domainObject' + requirements: + _access: 'TRUE' +early_rendering_controller_test.domain-object.early: + path: '/early-rendering-controller-test/domain-object/early' + defaults: + _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::domainObjectEarly' + requirements: + _access: 'TRUE' + +# Controller returning a domain object with attachments. +early_rendering_controller_test.domain-object-with-attachments: + path: '/early-rendering-controller-test/domain-object-with-attachments' + defaults: + _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::domainObjectWithAttachments' + requirements: + _access: 'TRUE' +early_rendering_controller_test.domain-object-with-attachments.early: + path: '/early-rendering-controller-test/domain-object-with-attachments/early' + defaults: + _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::domainObjectWithAttachmentsEarly' + requirements: + _access: 'TRUE' + +# Controller returning a cacheable domain object. +early_rendering_controller_test.cacheable-domain-object: + path: '/early-rendering-controller-test/cacheable-domain-object' + defaults: + _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::cacheableDomainObject' + requirements: + _access: 'TRUE' +early_rendering_controller_test.cacheable-domain-object.early: + path: '/early-rendering-controller-test/cacheable-domain-object/early' + defaults: + _controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::cacheableDomainObjectEarly' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.services.yml b/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.services.yml new file mode 100644 index 0000000..8c62f1a --- /dev/null +++ b/core/modules/system/tests/modules/early_rendering_controller_test/early_rendering_controller_test.services.yml @@ -0,0 +1,5 @@ +services: + test_domain_object.view_subscriber: + class: Drupal\early_rendering_controller_test\TestDomainObjectViewSubscriber + tags: + - { name: event_subscriber } diff --git a/core/modules/system/tests/modules/early_rendering_controller_test/src/AttachmentsTestDomainObject.php b/core/modules/system/tests/modules/early_rendering_controller_test/src/AttachmentsTestDomainObject.php new file mode 100644 index 0000000..e6b6a4f --- /dev/null +++ b/core/modules/system/tests/modules/early_rendering_controller_test/src/AttachmentsTestDomainObject.php @@ -0,0 +1,17 @@ +renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('renderer') + ); + } + + protected function earlyRenderContent() { + return [ + '#markup' => 'Hello world!', + '#cache' => [ + 'tags' => [ + 'foo', + ], + ], + ]; + } + + public function renderArray() { + return [ + '#pre_render' => [function () { + $elements = $this->earlyRenderContent(); + return $elements; + }], + ]; + } + + public function renderArrayEarly() { + $render_array = $this->earlyRenderContent(); + return [ + '#markup' => $this->renderer->render($render_array), + ]; + } + + public function response() { + return new Response('Hello world!'); + } + + public function responseEarly() { + $render_array = $this->earlyRenderContent(); + return new Response($this->renderer->render($render_array)); + } + + public function responseWithAttachments() { + return new AttachmentsTestResponse('Hello world!'); + } + + public function responseWithAttachmentsEarly() { + $render_array = $this->earlyRenderContent(); + return new AttachmentsTestResponse($this->renderer->render($render_array)); + } + + public function cacheableResponse() { + return new CacheableTestResponse('Hello world!'); + } + + public function cacheableResponseEarly() { + $render_array = $this->earlyRenderContent(); + return new CacheableTestResponse($this->renderer->render($render_array)); + } + + public function domainObject() { + return new TestDomainObject(); + } + + public function domainObjectEarly() { + $render_array = $this->earlyRenderContent(); + $this->renderer->render($render_array); + return new TestDomainObject(); + } + + public function domainObjectWithAttachments() { + return new AttachmentsTestDomainObject(); + } + + public function domainObjectWithAttachmentsEarly() { + $render_array = $this->earlyRenderContent(); + $this->renderer->render($render_array); + return new AttachmentsTestDomainObject(); + } + + public function cacheableDomainObject() { + return new CacheableTestDomainObject(); + } + + public function cacheableDomainObjectEarly() { + $render_array = $this->earlyRenderContent(); + $this->renderer->render($render_array); + return new CacheableTestDomainObject(); + } + +} diff --git a/core/modules/system/tests/modules/early_rendering_controller_test/src/TestDomainObject.php b/core/modules/system/tests/modules/early_rendering_controller_test/src/TestDomainObject.php new file mode 100644 index 0000000..b2f8f8b --- /dev/null +++ b/core/modules/system/tests/modules/early_rendering_controller_test/src/TestDomainObject.php @@ -0,0 +1,10 @@ +getControllerResult(); + + if ($result instanceof TestDomainObject) { + if ($result instanceof AttachmentsTestDomainObject) { + $event->setResponse(new AttachmentsTestResponse('AttachmentsTestDomainObject')); + } + elseif ($result instanceof CacheableTestDomainObject) { + $event->setResponse(new CacheableTestResponse('CacheableTestDomainObject')); + } + else { + $event->setResponse(new Response('TestDomainObject')); + } + } + } + + /** + * {@inheritdoc} + */ + static function getSubscribedEvents() { + $events[KernelEvents::VIEW][] = ['onViewTestDomainObject']; + + return $events; + } + +} diff --git a/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldTidTest.php b/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldTidTest.php index 0289049..f02968b 100644 --- a/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldTidTest.php +++ b/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldTidTest.php @@ -7,6 +7,7 @@ namespace Drupal\taxonomy\Tests\Views; +use Drupal\Core\Render\RenderContext; use Drupal\views\Views; /** @@ -24,10 +25,15 @@ class TaxonomyFieldTidTest extends TaxonomyTestBase { public static $testViews = array('test_taxonomy_tid_field'); function testViewsHandlerTidField() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $view = Views::getView('test_taxonomy_tid_field'); $this->executeView($view); - $actual = $view->field['name']->advancedRender($view->result[0]); + $actual = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $expected = \Drupal::l($this->term1->label(), $this->term1->urlInfo()); $this->assertEqual($expected, $actual); diff --git a/core/modules/user/src/Tests/Views/HandlerFieldUserNameTest.php b/core/modules/user/src/Tests/Views/HandlerFieldUserNameTest.php index c2c0c93..5db1e3e 100644 --- a/core/modules/user/src/Tests/Views/HandlerFieldUserNameTest.php +++ b/core/modules/user/src/Tests/Views/HandlerFieldUserNameTest.php @@ -7,6 +7,7 @@ namespace Drupal\user\Tests\Views; +use Drupal\Core\Render\RenderContext; use Drupal\views\Views; /** @@ -25,6 +26,9 @@ class HandlerFieldUserNameTest extends UserTestBase { public static $testViews = array('test_views_handler_field_user_name'); public function testUserName() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $this->drupalLogin($this->drupalCreateUser(array('access user profiles'))); // Set defaults. @@ -37,13 +41,17 @@ public function testUserName() { $this->executeView($view); $anon_name = $this->config('user.settings')->get('anonymous'); - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertTrue(strpos($render, $anon_name) !== FALSE, 'For user 0 it should use the default anonymous name by default.'); $username = $this->randomMachineName(); $view->result[0]->_entity->setUsername($username); $view->result[0]->_entity->uid->value = 1; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertTrue(strpos($render, $username) !== FALSE, 'If link to user is checked the username should be part of the output.'); $this->assertTrue(strpos($render, 'user/1') !== FALSE, 'If link to user is checked the link to the user should appear as well.'); @@ -52,7 +60,9 @@ public function testUserName() { $username = $this->randomMachineName(); $view->result[0]->_entity->setUsername($username); $view->result[0]->_entity->uid->value = 1; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $username, 'If the user is not linked the username should be printed out for a normal user.'); } @@ -61,13 +71,18 @@ public function testUserName() { * Tests that the field handler works when no additional fields are added. */ public function testNoAdditionalFields() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $view = Views::getView('test_views_handler_field_user_name'); $this->executeView($view); $username = $this->randomMachineName(); $view->result[0]->_entity->setUsername($username); $view->result[0]->_entity->uid->value = 1; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertTrue(strpos($render, $username) !== FALSE, 'If link to user is checked the username should be part of the output.'); } diff --git a/core/modules/views/src/Tests/Handler/FieldGroupRowsTest.php b/core/modules/views/src/Tests/Handler/FieldGroupRowsTest.php index d318a79..9cfd854 100644 --- a/core/modules/views/src/Tests/Handler/FieldGroupRowsTest.php +++ b/core/modules/views/src/Tests/Handler/FieldGroupRowsTest.php @@ -8,6 +8,7 @@ namespace Drupal\views\Tests\Handler; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Render\RenderContext; use Drupal\views\Views; /** @@ -67,6 +68,9 @@ protected function setUp() { * Testing the "Grouped rows" functionality. */ public function testGroupRows() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $edit = array( 'title' => $this->randomMachineName(), $this->fieldName => array('a', 'b', 'c'), @@ -77,7 +81,10 @@ public function testGroupRows() { // Test grouped rows. $this->executeView($view); - $this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[0]), 'a, b, c'); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field[$this->fieldName]->advancedRender($view->result[0]); + }); + $this->assertEqual($output, 'a, b, c'); // Change the group_rows checkbox to false. $view = Views::getView('test_group_rows'); @@ -88,11 +95,20 @@ public function testGroupRows() { $view->render(); $view->row_index = 0; - $this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[0]), 'a'); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field[$this->fieldName]->advancedRender($view->result[0]); + }); + $this->assertEqual($output, 'a'); $view->row_index = 1; - $this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[1]), 'b'); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field[$this->fieldName]->advancedRender($view->result[1]); + }); + $this->assertEqual($output, 'b'); $view->row_index = 2; - $this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[2]), 'c'); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field[$this->fieldName]->advancedRender($view->result[2]); + }); + $this->assertEqual($output, 'c'); } } diff --git a/core/modules/views/src/Tests/Handler/FieldUnitTest.php b/core/modules/views/src/Tests/Handler/FieldUnitTest.php index f76e1ce..5a5d73f 100644 --- a/core/modules/views/src/Tests/Handler/FieldUnitTest.php +++ b/core/modules/views/src/Tests/Handler/FieldUnitTest.php @@ -7,6 +7,7 @@ namespace Drupal\views\Tests\Handler; +use Drupal\Core\Render\RenderContext; use Drupal\views\Tests\ViewUnitTestBase; use Drupal\views\Plugin\views\field\FieldPluginBase; use Drupal\views\Views; @@ -52,12 +53,18 @@ protected function viewsData() { * Tests that the render function is called. */ public function testRender() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $view = Views::getView('test_field_tokens'); $this->executeView($view); $random_text = $this->randomMachineName(); $view->field['job']->setTestValue($random_text); - $this->assertEqual($view->field['job']->theme($view->result[0]), $random_text, 'Make sure the render method rendered the manual set value.'); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['job']->theme($view->result[0]); + }); + $this->assertEqual($output, $random_text, 'Make sure the render method rendered the manual set value.'); } /** @@ -141,6 +148,9 @@ protected function assertNotSubString($haystack, $needle, $message = '', $group * Tests general rewriting of the output. */ public function testRewrite() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $view = Views::getView('test_view'); $view->initHandlers(); $this->executeView($view); @@ -149,11 +159,15 @@ public function testRewrite() { // Don't check the rewrite checkbox, so the text shouldn't appear. $id_field->options['alter']['text'] = $random_text = $this->randomMachineName(); - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertNotSubString($output, $random_text); $id_field->options['alter']['alter_text'] = TRUE; - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString($output, $random_text); } @@ -161,6 +175,9 @@ public function testRewrite() { * Tests the field tokens, row level and field level. */ public function testFieldTokens() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $view = Views::getView('test_field_tokens'); $this->executeView($view); $name_field_0 = $view->field['name']; @@ -182,19 +199,25 @@ public function testFieldTokens() { $expected_output_1 = "$row->views_test_data_name $row->views_test_data_name"; $expected_output_2 = "$row->views_test_data_name $row->views_test_data_name $row->views_test_data_name"; - $output = $name_field_0->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field_0, $row) { + return $name_field_0->advancedRender($row); + }); $this->assertEqual($output, $expected_output_0, format_string('Test token replacement: "!token" gave "!output"', [ '!token' => $name_field_0->options['alter']['text'], '!output' => $output, ])); - $output = $name_field_1->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field_1, $row) { + return $name_field_1->advancedRender($row); + }); $this->assertEqual($output, $expected_output_1, format_string('Test token replacement: "!token" gave "!output"', [ '!token' => $name_field_1->options['alter']['text'], '!output' => $output, ])); - $output = $name_field_2->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field_2, $row) { + return $name_field_2->advancedRender($row); + }); $this->assertEqual($output, $expected_output_2, format_string('Test token replacement: "!token" gave "!output"', [ '!token' => $name_field_2->options['alter']['text'], '!output' => $output, @@ -207,7 +230,9 @@ public function testFieldTokens() { $random_text = $this->randomMachineName(); $job_field->setTestValue($random_text); - $output = $job_field->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($job_field, $row) { + return $job_field->advancedRender($row); + }); $this->assertSubString($output, $random_text, format_string('Make sure the self token (!token => !value) appears in the output (!output)', [ '!value' => $random_text, '!output' => $output, @@ -219,7 +244,9 @@ public function testFieldTokens() { $job_field->options['alter']['text'] = $old_token; $random_text = $this->randomMachineName(); $job_field->setTestValue($random_text); - $output = $job_field->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($job_field, $row) { + return $job_field->advancedRender($row); + }); $this->assertSubString($output, $old_token, format_string('Make sure the old token style (!token => !value) is not changed in the output (!output)', [ '!value' => $random_text, '!output' => $output, @@ -268,6 +295,9 @@ function testEmpty() { * This tests alters the result to get easier and less coupled results. */ function _testHideIfEmpty() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $view = Views::getView('test_view'); $view->initDisplay(); $this->executeView($view); @@ -284,22 +314,30 @@ function _testHideIfEmpty() { // Test a valid string. $view->result[0]->{$column_map_reversed['name']} = $random_name; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $random_name, 'By default, a string should not be treated as empty.'); // Test an empty string. $view->result[0]->{$column_map_reversed['name']} = ""; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "", 'By default, "" should not be treated as empty.'); // Test zero as an integer. $view->result[0]->{$column_map_reversed['name']} = 0; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, '0', 'By default, 0 should not be treated as empty.'); // Test zero as a string. $view->result[0]->{$column_map_reversed['name']} = "0"; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "0", 'By default, "0" should not be treated as empty.'); // Test when results are not rewritten and non-zero empty values are hidden. @@ -309,22 +347,30 @@ function _testHideIfEmpty() { // Test a valid string. $view->result[0]->{$column_map_reversed['name']} = $random_name; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $random_name, 'If hide_empty is checked, a string should not be treated as empty.'); // Test an empty string. $view->result[0]->{$column_map_reversed['name']} = ""; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "", 'If hide_empty is checked, "" should be treated as empty.'); // Test zero as an integer. $view->result[0]->{$column_map_reversed['name']} = 0; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, '0', 'If hide_empty is checked, but not empty_zero, 0 should not be treated as empty.'); // Test zero as a string. $view->result[0]->{$column_map_reversed['name']} = "0"; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "0", 'If hide_empty is checked, but not empty_zero, "0" should not be treated as empty.'); // Test when results are not rewritten and all empty values are hidden. @@ -334,12 +380,16 @@ function _testHideIfEmpty() { // Test zero as an integer. $view->result[0]->{$column_map_reversed['name']} = 0; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "", 'If hide_empty and empty_zero are checked, 0 should be treated as empty.'); // Test zero as a string. $view->result[0]->{$column_map_reversed['name']} = "0"; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "", 'If hide_empty and empty_zero are checked, "0" should be treated as empty.'); // Test when results are rewritten to a valid string and non-zero empty @@ -352,22 +402,30 @@ function _testHideIfEmpty() { // Test a valid string. $view->result[0]->{$column_map_reversed['name']} = $random_value; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, it should not be treated as empty.'); // Test an empty string. $view->result[0]->{$column_map_reversed['name']} = ""; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, "" should not be treated as empty.'); // Test zero as an integer. $view->result[0]->{$column_map_reversed['name']} = 0; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, 0 should not be treated as empty.'); // Test zero as a string. $view->result[0]->{$column_map_reversed['name']} = "0"; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, "0" should not be treated as empty.'); // Test when results are rewritten to an empty string and non-zero empty results are hidden. @@ -379,22 +437,30 @@ function _testHideIfEmpty() { // Test a valid string. $view->result[0]->{$column_map_reversed['name']} = $random_name; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $random_name, 'If the rewritten string is empty, it should not be treated as empty.'); // Test an empty string. $view->result[0]->{$column_map_reversed['name']} = ""; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "", 'If the rewritten string is empty, "" should be treated as empty.'); // Test zero as an integer. $view->result[0]->{$column_map_reversed['name']} = 0; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, '0', 'If the rewritten string is empty, 0 should not be treated as empty.'); // Test zero as a string. $view->result[0]->{$column_map_reversed['name']} = "0"; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "0", 'If the rewritten string is empty, "0" should not be treated as empty.'); // Test when results are rewritten to zero as a string and non-zero empty @@ -407,22 +473,30 @@ function _testHideIfEmpty() { // Test a valid string. $view->result[0]->{$column_map_reversed['name']} = $random_name; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, the string rewritten as 0 should not be treated as empty.'); // Test an empty string. $view->result[0]->{$column_map_reversed['name']} = ""; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, "" rewritten as 0 should not be treated as empty.'); // Test zero as an integer. $view->result[0]->{$column_map_reversed['name']} = 0; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, 0 should not be treated as empty.'); // Test zero as a string. $view->result[0]->{$column_map_reversed['name']} = "0"; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, "0" should not be treated as empty.'); // Test when results are rewritten to a valid string and non-zero empty @@ -435,22 +509,30 @@ function _testHideIfEmpty() { // Test a valid string. $view->result[0]->{$column_map_reversed['name']} = $random_name; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, it should not be treated as empty.'); // Test an empty string. $view->result[0]->{$column_map_reversed['name']} = ""; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "", 'If either the original or rewritten string is invalid, "" should be treated as empty.'); // Test zero as an integer. $view->result[0]->{$column_map_reversed['name']} = 0; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, 0 should not be treated as empty.'); // Test zero as a string. $view->result[0]->{$column_map_reversed['name']} = "0"; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, "0" should not be treated as empty.'); // Test when results are rewritten to zero as a string and all empty @@ -463,22 +545,30 @@ function _testHideIfEmpty() { // Test a valid string. $view->result[0]->{$column_map_reversed['name']} = $random_name; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "", 'If the rewritten string is zero, it should be treated as empty.'); // Test an empty string. $view->result[0]->{$column_map_reversed['name']} = ""; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "", 'If the rewritten string is zero, "" should be treated as empty.'); // Test zero as an integer. $view->result[0]->{$column_map_reversed['name']} = 0; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "", 'If the rewritten string is zero, 0 should not be treated as empty.'); // Test zero as a string. $view->result[0]->{$column_map_reversed['name']} = "0"; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "", 'If the rewritten string is zero, "0" should not be treated as empty.'); } @@ -486,6 +576,9 @@ function _testHideIfEmpty() { * Tests the usage of the empty text. */ function _testEmptyText() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $view = Views::getView('test_view'); $view->initDisplay(); $this->executeView($view); @@ -495,27 +588,37 @@ function _testEmptyText() { $empty_text = $view->field['name']->options['empty'] = $this->randomMachineName(); $view->result[0]->{$column_map_reversed['name']} = ""; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $empty_text, 'If a field is empty, the empty text should be used for the output.'); $view->result[0]->{$column_map_reversed['name']} = "0"; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, "0", 'If a field is 0 and empty_zero is not checked, the empty text should not be used for the output.'); $view->result[0]->{$column_map_reversed['name']} = "0"; $view->field['name']->options['empty_zero'] = TRUE; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $empty_text, 'If a field is 0 and empty_zero is checked, the empty text should be used for the output.'); $view->result[0]->{$column_map_reversed['name']} = ""; $view->field['name']->options['alter']['alter_text'] = TRUE; $alter_text = $view->field['name']->options['alter']['text'] = $this->randomMachineName(); $view->field['name']->options['hide_alter_empty'] = FALSE; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $alter_text, 'If a field is empty, some rewrite text exists, but hide_alter_empty is not checked, render the rewrite text.'); $view->field['name']->options['hide_alter_empty'] = TRUE; - $render = $view->field['name']->advancedRender($view->result[0]); + $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { + return $view->field['name']->advancedRender($view->result[0]); + }); $this->assertIdentical($render, $empty_text, 'If a field is empty, some rewrite text exists, and hide_alter_empty is checked, use the empty text.'); } diff --git a/core/modules/views/src/Tests/Handler/FieldWebTest.php b/core/modules/views/src/Tests/Handler/FieldWebTest.php index 45a1617..39e1eb2 100644 --- a/core/modules/views/src/Tests/Handler/FieldWebTest.php +++ b/core/modules/views/src/Tests/Handler/FieldWebTest.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Render\RenderContext; use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait; use Drupal\views\Views; @@ -197,6 +198,9 @@ protected function xpathContent($content, $xpath, array $arguments = array()) { * Tests rewriting the output to a link. */ public function testAlterUrl() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $view = Views::getView('test_view'); $view->setDisplay(); $view->initHandlers(); @@ -211,13 +215,17 @@ public function testAlterUrl() { // Tests that the suffix/prefix appears on the output. $id_field->options['alter']['prefix'] = $prefix = $this->randomMachineName(); $id_field->options['alter']['suffix'] = $suffix = $this->randomMachineName(); - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString($output, $prefix); $this->assertSubString($output, $suffix); unset($id_field->options['alter']['prefix']); unset($id_field->options['alter']['suffix']); - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString($output, $path, 'Make sure that the path is part of the output'); // Some generic test code adapted from the UrlTest class, which tests @@ -228,44 +236,60 @@ public function testAlterUrl() { $expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['absolute' => $absolute]); $alter['absolute'] = $absolute; - $result = $id_field->theme($row); + $result = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString($result, $expected_result); $expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['fragment' => 'foo', 'absolute' => $absolute]); $alter['path'] = 'node/123#foo'; - $result = $id_field->theme($row); + $result = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString($result, $expected_result); $expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['query' => ['foo' => NULL], 'absolute' => $absolute]); $alter['path'] = 'node/123?foo'; - $result = $id_field->theme($row); + $result = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString($result, $expected_result); $expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['query' => ['foo' => 'bar', 'bar' => 'baz'], 'absolute' => $absolute]); $alter['path'] = 'node/123?foo=bar&bar=baz'; - $result = $id_field->theme($row); + $result = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString(Html::decodeEntities($result), Html::decodeEntities($expected_result)); // @todo The route-based URL generator strips out NULL attributes. // $expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['query' => ['foo' => NULL], 'fragment' => 'bar', 'absolute' => $absolute]); $expected_result = \Drupal::urlGenerator()->generateFromPath('node/123', array('query' => array('foo' => NULL), 'fragment' => 'bar', 'absolute' => $absolute)); $alter['path'] = 'node/123?foo#bar'; - $result = $id_field->theme($row); + $result = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString(Html::decodeEntities($result), Html::decodeEntities($expected_result)); $expected_result = \Drupal::url('', [], ['absolute' => $absolute]); $alter['path'] = ''; - $result = $id_field->theme($row); + $result = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString($result, $expected_result); } // Tests the replace spaces with dashes feature. $id_field->options['alter']['replace_spaces'] = TRUE; $id_field->options['alter']['path'] = $path = $this->randomMachineName() . ' ' . $this->randomMachineName(); - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString($output, str_replace(' ', '-', $path)); $id_field->options['alter']['replace_spaces'] = FALSE; - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); // The url has a space in it, so to check we have to decode the url output. $this->assertSubString(urldecode($output), $path); @@ -273,44 +297,60 @@ public function testAlterUrl() { // Switch on the external flag should output an external url as well. $id_field->options['alter']['external'] = TRUE; $id_field->options['alter']['path'] = $path = 'www.drupal.org'; - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString($output, 'http://www.drupal.org'); // Setup a not external url, which shouldn't lead to an external url. $id_field->options['alter']['external'] = FALSE; $id_field->options['alter']['path'] = $path = 'www.drupal.org'; - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertNotSubString($output, 'http://www.drupal.org'); // Tests the transforming of the case setting. $id_field->options['alter']['path'] = $path = $this->randomMachineName(); $id_field->options['alter']['path_case'] = 'none'; - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString($output, $path); // Switch to uppercase and lowercase. $id_field->options['alter']['path_case'] = 'upper'; - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString($output, strtoupper($path)); $id_field->options['alter']['path_case'] = 'lower'; - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString($output, strtolower($path)); // Switch to ucfirst and ucwords. $id_field->options['alter']['path_case'] = 'ucfirst'; $id_field->options['alter']['path'] = 'drupal has a great community'; - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString($output, UrlHelper::encodePath('Drupal has a great community')); $id_field->options['alter']['path_case'] = 'ucwords'; - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $this->assertSubString($output, UrlHelper::encodePath('Drupal Has A Great Community')); unset($id_field->options['alter']['path_case']); // Tests the linkclass setting and see whether it actually exists in the // output. $id_field->options['alter']['link_class'] = $class = $this->randomMachineName(); - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $elements = $this->xpathContent($output, '//a[contains(@class, :class)]', array(':class' => $class)); $this->assertTrue($elements); // @fixme link_class, alt, rel cannot be unset, which should be fixed. @@ -318,21 +358,27 @@ public function testAlterUrl() { // Tests the alt setting. $id_field->options['alter']['alt'] = $rel = $this->randomMachineName(); - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $elements = $this->xpathContent($output, '//a[contains(@title, :alt)]', array(':alt' => $rel)); $this->assertTrue($elements); $id_field->options['alter']['alt'] = ''; // Tests the rel setting. $id_field->options['alter']['rel'] = $rel = $this->randomMachineName(); - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $elements = $this->xpathContent($output, '//a[contains(@rel, :rel)]', array(':rel' => $rel)); $this->assertTrue($elements); $id_field->options['alter']['rel'] = ''; // Tests the target setting. $id_field->options['alter']['target'] = $target = $this->randomMachineName(); - $output = $id_field->theme($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($id_field, $row) { + return $id_field->theme($row); + }); $elements = $this->xpathContent($output, '//a[contains(@target, :target)]', array(':target' => $target)); $this->assertTrue($elements); unset($id_field->options['alter']['target']); @@ -453,6 +499,9 @@ public function testFieldClasses() { * Tests trimming/read-more/ellipses. */ public function testTextRendering() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $view = Views::getView('test_field_output'); $view->initHandlers(); $name_field = $view->field['name']; @@ -465,18 +514,24 @@ public function testTextRendering() { $row = $view->result[0]; $name_field->options['alter']['strip_tags'] = TRUE; - $output = $name_field->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) { + return $name_field->advancedRender($row); + }); $this->assertSubString($output, $random_text, 'Find text without html if stripping of views field output is enabled.'); $this->assertNotSubString($output, $html_text, 'Find no text with the html if stripping of views field output is enabled.'); // Tests preserving of html tags. $name_field->options['alter']['preserve_tags'] = '
'; - $output = $name_field->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) { + return $name_field->advancedRender($row); + }); $this->assertSubString($output, $random_text, 'Find text without html if stripping of views field output is enabled but a div is allowed.'); $this->assertSubString($output, $html_text, 'Find text with the html if stripping of views field output is enabled but a div is allowed.'); $name_field->options['alter']['strip_tags'] = FALSE; - $output = $name_field->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) { + return $name_field->advancedRender($row); + }); $this->assertSubString($output, $random_text, 'Find text without html if stripping of views field output is disabled.'); $this->assertSubString($output, $html_text, 'Find text with the html if stripping of views field output is disabled.'); @@ -485,13 +540,17 @@ public function testTextRendering() { $views_test_data_name = $row->views_test_data_name; $row->views_test_data_name = ' ' . $views_test_data_name . ' '; $name_field->options['alter']['trim_whitespace'] = TRUE; - $output = $name_field->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) { + return $name_field->advancedRender($row); + }); $this->assertSubString($output, $views_test_data_name, 'Make sure the trimmed text can be found if trimming is enabled.'); $this->assertNotSubString($output, $row->views_test_data_name, 'Make sure the untrimmed text can be found if trimming is enabled.'); $name_field->options['alter']['trim_whitespace'] = FALSE; - $output = $name_field->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) { + return $name_field->advancedRender($row); + }); $this->assertSubString($output, $views_test_data_name, 'Make sure the trimmed text can be found if trimming is disabled.'); $this->assertSubString($output, $row->views_test_data_name, 'Make sure the untrimmed text can be found if trimming is disabled.'); @@ -504,12 +563,16 @@ public function testTextRendering() { $name_field->options['alter']['max_length'] = 5; $trimmed_name = Unicode::substr($row->views_test_data_name, 0, 5); - $output = $name_field->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) { + return $name_field->advancedRender($row); + }); $this->assertSubString($output, $trimmed_name, format_string('Make sure the trimmed output (!trimmed) appears in the rendered output (!output).', array('!trimmed' => $trimmed_name, '!output' => $output))); $this->assertNotSubString($output, $row->views_test_data_name, format_string("Make sure the untrimmed value (!untrimmed) shouldn't appear in the rendered output (!output).", array('!untrimmed' => $row->views_test_data_name, '!output' => $output))); $name_field->options['alter']['max_length'] = 9; - $output = $name_field->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) { + return $name_field->advancedRender($row); + }); $this->assertSubString($output, $trimmed_name, format_string('Make sure the untrimmed (!untrimmed) output appears in the rendered output (!output).', array('!trimmed' => $trimmed_name, '!output' => $output))); // Take word_boundary into account for the tests. @@ -549,7 +612,9 @@ public function testTextRendering() { foreach ($tuples as $tuple) { $row->views_test_data_name = $tuple['value']; - $output = $name_field->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) { + return $name_field->advancedRender($row); + }); if ($tuple['trimmed']) { $this->assertNotSubString($output, $tuple['value'], format_string('The untrimmed value (!untrimmed) should not appear in the trimmed output (!output).', array('!untrimmed' => $tuple['value'], '!output' => $output))); @@ -566,22 +631,30 @@ public function testTextRendering() { $name_field->options['alter']['more_link_text'] = $more_text = $this->randomMachineName(); $name_field->options['alter']['more_link_path'] = $more_path = $this->randomMachineName(); - $output = $name_field->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) { + return $name_field->advancedRender($row); + }); $this->assertSubString($output, $more_text, 'Make sure a read more text is displayed if the output got trimmed'); $this->assertTrue($this->xpathContent($output, '//a[contains(@href, :path)]', array(':path' => $more_path)), 'Make sure the read more link points to the right destination.'); $name_field->options['alter']['more_link'] = FALSE; - $output = $name_field->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) { + return $name_field->advancedRender($row); + }); $this->assertNotSubString($output, $more_text, 'Make sure no read more text appears.'); $this->assertFalse($this->xpathContent($output, '//a[contains(@href, :path)]', array(':path' => $more_path)), 'Make sure no read more link appears.'); // Check for the ellipses. $row->views_test_data_name = $this->randomMachineName(8); $name_field->options['alter']['max_length'] = 5; - $output = $name_field->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) { + return $name_field->advancedRender($row); + }); $this->assertSubString($output, '…', 'An ellipsis should appear if the output is trimmed'); $name_field->options['alter']['max_length'] = 10; - $output = $name_field->advancedRender($row); + $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($name_field, $row) { + return $name_field->advancedRender($row); + }); $this->assertNotSubString($output, '…', 'No ellipsis should appear if the output is not trimmed'); } diff --git a/core/modules/views/src/Tests/Plugin/CacheTest.php b/core/modules/views/src/Tests/Plugin/CacheTest.php index e628f8e..0d8c907 100644 --- a/core/modules/views/src/Tests/Plugin/CacheTest.php +++ b/core/modules/views/src/Tests/Plugin/CacheTest.php @@ -7,6 +7,7 @@ namespace Drupal\views\Tests\Plugin; +use Drupal\Core\Render\RenderContext; use Drupal\node\Entity\Node; use Drupal\views\Tests\ViewUnitTestBase; use Drupal\views\Views; @@ -282,14 +283,18 @@ function testHeaderStorage() { $output = $view->buildRenderable(); /** @var \Drupal\Core\Render\RendererInterface $renderer */ $renderer = \Drupal::service('renderer'); - $renderer->render($output); + $renderer->executeInRenderContext(new RenderContext(), function () use (&$output, $renderer) { + return $renderer->render($output); + }); unset($view->pre_render_called); $view->destroy(); $view->setDisplay(); $output = $view->buildRenderable(); - $renderer->render($output); + $renderer->executeInRenderContext(new RenderContext(), function () use (&$output, $renderer) { + return $renderer->render($output); + }); $this->assertTrue(in_array('views_test_data/test', $output['#attached']['library']), 'Make sure libraries are added for cached views.'); $this->assertEqual(['foo' => 'bar'], $output['#attached']['drupalSettings'], 'Make sure drupalSettings are added for cached views.');