Exemple #1
0
 /**
  * {@inheritdoc}
  */
 public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match)
 {
     $response = new AjaxResponse();
     if (isset($main_content['#type']) && $main_content['#type'] == 'ajax') {
         // Complex Ajax callbacks can return a result that contains an error
         // message or a specific set of commands to send to the browser.
         $main_content += $this->elementInfoManager->getInfo('ajax');
         $error = $main_content['#error'];
         if (!empty($error)) {
             // Fall back to some default message otherwise use the specific one.
             if (!is_string($error)) {
                 $error = 'An error occurred while handling the request: The server received invalid input.';
             }
             $response->addCommand(new AlertCommand($error));
         }
     }
     $html = $this->drupalRenderRoot($main_content);
     $response->setAttachments($main_content['#attached']);
     // The selector for the insert command is NULL as the new content will
     // replace the element making the Ajax call. The default 'replaceWith'
     // behavior can be changed with #ajax['method'].
     $response->addCommand(new InsertCommand(NULL, $html));
     $status_messages = array('#type' => 'status_messages');
     $output = $this->drupalRenderRoot($status_messages);
     if (!empty($output)) {
         $response->addCommand(new PrependCommand(NULL, $output));
     }
     return $response;
 }
 /**
  * {@inheritdoc}
  */
 public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state)
 {
     $field_settings = $this->getFieldSettings();
     // The field settings include defaults for the field type. However, this
     // widget is a base class for other widgets (e.g., ImageWidget) that may act
     // on field types without these expected settings.
     $field_settings += array('display_default' => NULL, 'display_field' => NULL, 'description_field' => NULL);
     $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
     $defaults = array('fids' => array(), 'display' => (bool) $field_settings['display_default'], 'description' => '');
     // Essentially we use the managed_file type, extended with some
     // enhancements.
     $element_info = $this->elementInfo->getInfo('embridge_asset');
     $element += array('#type' => 'embridge_asset', '#upload_location' => $items[$delta]->getUploadLocation(), '#upload_validators' => $items[$delta]->getUploadValidators(), '#value_callback' => array(get_class($this), 'value'), '#process' => array_merge($element_info['#process'], array(array(get_class($this), 'process'))), '#progress_indicator' => $this->getSetting('progress_indicator'), '#extended' => TRUE, '#entity_type' => $items->getEntity()->getEntityTypeId(), '#field_name' => $this->fieldDefinition->getName(), '#field_config' => $this->fieldDefinition->id(), '#allow_search' => $field_settings['allow_search'], '#display_field' => (bool) $field_settings['display_field'], '#display_default' => $field_settings['display_default'], '#description_field' => $field_settings['description_field'], '#cardinality' => $cardinality, '#catalog_id' => $field_settings['catalog_id'], '#library_id' => $field_settings['library_id']);
     $element['#weight'] = $delta;
     // Field stores FID value in a single mode, so we need to transform it for
     // form element to recognize it correctly.
     if (!isset($items[$delta]->fids) && isset($items[$delta]->target_id)) {
         $items[$delta]->fids = array($items[$delta]->target_id);
     }
     $element['#default_value'] = $items[$delta]->getValue() + $defaults;
     $default_fids = $element['#extended'] ? $element['#default_value']['fids'] : $element['#default_value'];
     if (empty($default_fids)) {
         $file_upload_help = array('#theme' => 'file_upload_help', '#description' => $element['#description'], '#upload_validators' => $element['#upload_validators'], '#cardinality' => $cardinality);
         $this->alterFileUploadHelpParameters($file_upload_help);
         $element['#description'] = \Drupal::service('renderer')->renderPlain($file_upload_help);
         $element['#multiple'] = $cardinality != 1 ? TRUE : FALSE;
         if ($cardinality != 1 && $cardinality != -1) {
             $element['#element_validate'] = array(array(get_class($this), 'validateMultipleCount'));
         }
     }
     return $element;
 }
 /**
  * Tests the getInfo method.
  *
  * @covers ::getInfo
  * @covers ::buildInfo
  *
  * @dataProvider providerTestGetInfo
  */
 public function testGetInfo($type, $expected_info, $element_info, callable $alter_callback = NULL)
 {
     $this->moduleHandler->expects($this->once())->method('invokeAll')->with('element_info')->will($this->returnValue($element_info));
     $this->moduleHandler->expects($this->once())->method('alter')->with('element_info', $this->anything())->will($this->returnCallback($alter_callback ?: function ($info) {
         return $info;
     }));
     $this->assertEquals($expected_info, $this->elementInfo->getInfo($type));
 }
Exemple #4
0
 /**
  * Render the top of the display so it can be updated during ajax operations.
  */
 public function renderDisplayTop(ViewUI $view)
 {
     $display_id = $this->displayID;
     $element['#theme_wrappers'][] = 'views_ui_container';
     $element['#attributes']['class'] = array('views-display-top', 'clearfix');
     $element['#attributes']['id'] = array('views-display-top');
     // Extra actions for the display
     $element['extra_actions'] = array('#type' => 'dropbutton', '#attributes' => array('id' => 'views-display-extra-actions'), '#links' => array('edit-details' => array('title' => $this->t('Edit view name/description'), 'url' => Url::fromRoute('views_ui.form_edit_details', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display_id]), 'attributes' => array('class' => array('views-ajax-link'))), 'analyze' => array('title' => $this->t('Analyze view'), 'url' => Url::fromRoute('views_ui.form_analyze', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display_id]), 'attributes' => array('class' => array('views-ajax-link'))), 'duplicate' => array('title' => $this->t('Duplicate view'), 'url' => $view->urlInfo('duplicate-form')), 'reorder' => array('title' => $this->t('Reorder displays'), 'url' => Url::fromRoute('views_ui.form_reorder_displays', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display_id]), 'attributes' => array('class' => array('views-ajax-link')))));
     if ($view->access('delete')) {
         $element['extra_actions']['#links']['delete'] = array('title' => $this->t('Delete view'), 'url' => $view->urlInfo('delete-form'));
     }
     // Let other modules add additional links here.
     \Drupal::moduleHandler()->alter('views_ui_display_top_links', $element['extra_actions']['#links'], $view, $display_id);
     if (isset($view->type) && $view->type != $this->t('Default')) {
         if ($view->type == $this->t('Overridden')) {
             $element['extra_actions']['#links']['revert'] = array('title' => $this->t('Revert view'), 'href' => "admin/structure/views/view/{$view->id()}/revert", 'query' => array('destination' => $view->url('edit-form')));
         } else {
             $element['extra_actions']['#links']['delete'] = array('title' => $this->t('Delete view'), 'url' => $view->urlInfo('delete-form'));
         }
     }
     // Determine the displays available for editing.
     if ($tabs = $this->getDisplayTabs($view)) {
         if ($display_id) {
             $tabs[$display_id]['#active'] = TRUE;
         }
         $tabs['#prefix'] = '<h2 class="visually-hidden">' . $this->t('Secondary tabs') . '</h2><ul id = "views-display-menu-tabs" class="tabs secondary">';
         $tabs['#suffix'] = '</ul>';
         $element['tabs'] = $tabs;
     }
     // Buttons for adding a new display.
     foreach (Views::fetchPluginNames('display', NULL, array($view->get('base_table'))) as $type => $label) {
         $element['add_display'][$type] = array('#type' => 'submit', '#value' => $this->t('Add @display', array('@display' => $label)), '#limit_validation_errors' => array(), '#submit' => array('::submitDisplayAdd', '::submitDelayDestination'), '#attributes' => array('class' => array('add-display')), '#process' => array_merge(array('views_ui_form_button_was_clicked'), $this->elementInfo->getInfoProperty('submit', '#process', array())), '#values' => array($this->t('Add @display', array('@display' => $label)), $label));
     }
     return $element;
 }
 /**
  * Tests the getInfo method.
  *
  * @covers ::getInfo
  * @covers ::buildInfo
  *
  * @dataProvider providerTestGetInfo
  */
 public function testGetInfo($type, $expected_info, $element_info, callable $alter_callback = NULL)
 {
     $this->moduleHandler->expects($this->once())->method('invokeAll')->with('element_info')->will($this->returnValue($element_info));
     $this->moduleHandler->expects($this->once())->method('alter')->with('element_info', $this->anything())->will($this->returnCallback($alter_callback ?: function ($info) {
         return $info;
     }));
     $this->themeManager->expects($this->once())->method('getActiveTheme')->willReturn(new ActiveTheme(['name' => 'test']));
     $this->themeManager->expects($this->once())->method('alter')->with('element_info', $this->anything())->will($this->returnCallback($alter_callback ?: function ($info) {
         return $info;
     }));
     $this->cache->expects($this->at(0))->method('get')->with('element_info_build:test')->will($this->returnValue(FALSE));
     $this->cache->expects($this->at(1))->method('get')->with('element_info')->will($this->returnValue(FALSE));
     $this->cache->expects($this->at(2))->method('set')->with('element_info');
     $this->cache->expects($this->at(3))->method('set')->with('element_info_build:test');
     $this->assertEquals($expected_info, $this->elementInfo->getInfo($type));
 }
Exemple #6
0
 /**
  * {@inheritdoc}
  */
 protected function setUp()
 {
     parent::setUp();
     $this->controllerResolver = $this->getMock('Drupal\\Core\\Controller\\ControllerResolverInterface');
     $this->themeManager = $this->getMock('Drupal\\Core\\Theme\\ThemeManagerInterface');
     $this->elementInfo = $this->getMock('Drupal\\Core\\Render\\ElementInfoManagerInterface');
     $this->elementInfo->expects($this->any())->method('getInfo')->willReturnCallback(function ($type) {
         switch ($type) {
             case 'details':
                 $info = ['#theme_wrappers' => ['details']];
                 break;
             case 'link':
                 $info = ['#theme' => 'link'];
                 break;
             case 'markup':
                 $info = ['#pre_render' => [[Markup::class, 'ensureMarkupIsSafe']]];
                 break;
             default:
                 $info = [];
         }
         $info['#defaults_loaded'] = TRUE;
         return $info;
     });
     $this->requestStack = new RequestStack();
     $request = new Request();
     $request->server->set('REQUEST_TIME', $_SERVER['REQUEST_TIME']);
     $this->requestStack->push($request);
     $this->cacheFactory = $this->getMock('Drupal\\Core\\Cache\\CacheFactoryInterface');
     $this->cacheContextsManager = $this->getMockBuilder('Drupal\\Core\\Cache\\Context\\CacheContextsManager')->disableOriginalConstructor()->getMock();
     $current_user_role =& $this->currentUserRole;
     $this->cacheContextsManager->expects($this->any())->method('convertTokensToKeys')->willReturnCallback(function ($context_tokens) use(&$current_user_role) {
         $keys = [];
         foreach ($context_tokens as $context_id) {
             switch ($context_id) {
                 case 'user.roles':
                     $keys[] = 'r.' . $current_user_role;
                     break;
                 case 'languages:language_interface':
                     $keys[] = 'en';
                     break;
                 case 'theme':
                     $keys[] = 'stark';
                     break;
                 default:
                     $keys[] = $context_id;
             }
         }
         return new ContextCacheKeys($keys, new CacheableMetadata());
     });
     $this->renderCache = new RenderCache($this->requestStack, $this->cacheFactory, $this->cacheContextsManager);
     $this->renderer = new Renderer($this->controllerResolver, $this->themeManager, $this->elementInfo, $this->renderCache, $this->requestStack, $this->rendererConfig);
     $container = new ContainerBuilder();
     $container->set('cache_contexts_manager', $this->cacheContextsManager);
     $container->set('render_cache', $this->renderCache);
     $container->set('renderer', $this->renderer);
     \Drupal::setContainer($container);
 }
 /**
  * Controller method for Ajax content.
  *
  * @param \Symfony\Component\HttpFoundation\Request $request
  *   The request object.
  * @param callable $_content
  *   The callable that returns the content of the Ajax response.
  *
  * @return \Drupal\Core\Ajax\AjaxResponse
  *   A response object.
  */
 public function content(Request $request, $_content)
 {
     $content = $this->getContentResult($request, $_content);
     // If there is already a Response object, return it without manipulation.
     if ($content instanceof Response && $content->isOk()) {
         return $content;
     }
     // Allow controllers to return an HtmlFragment directly.
     if ($content instanceof HtmlFragment) {
         $content = $content->getContent();
     }
     // Most controllers return a render array, but some return a string.
     if (!is_array($content)) {
         $content = array('#markup' => $content);
     }
     $response = new AjaxResponse();
     if (isset($content['#type']) && $content['#type'] == 'ajax') {
         // Complex Ajax callbacks can return a result that contains an error
         // message or a specific set of commands to send to the browser.
         $content += $this->elementInfoManager->getInfo('ajax');
         $error = $content['#error'];
         if (!empty($error)) {
             // Fall back to some default message otherwise use the specific one.
             if (!is_string($error)) {
                 $error = 'An error occurred while handling the request: The server received invalid input.';
             }
             $response->addCommand(new AlertCommand($error));
         }
     }
     $html = $this->drupalRenderRoot($content);
     // The selector for the insert command is NULL as the new content will
     // replace the element making the Ajax call. The default 'replaceWith'
     // behavior can be changed with #ajax['method'].
     $response->addCommand(new InsertCommand(NULL, $html));
     $status_messages = array('#theme' => 'status_messages');
     $output = $this->drupalRenderRoot($status_messages);
     if (!empty($output)) {
         $response->addCommand(new PrependCommand(NULL, $output));
     }
     return $response;
 }
Exemple #8
0
 /**
  * {@inheritdoc}
  *
  * The entire HTML: takes a #type 'page' and wraps it in a #type 'html'.
  */
 public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match)
 {
     list($page, $title) = $this->prepare($main_content, $request, $route_match);
     if (!isset($page['#type']) || $page['#type'] !== 'page') {
         throw new \LogicException('Must be #type page');
     }
     $page['#title'] = $title;
     // Now render the rendered page.html.twig template inside the html.html.twig
     // template, and use the bubbled #attached metadata from $page to ensure we
     // load all attached assets.
     $html = ['#type' => 'html', 'page' => $page];
     $html += $this->elementInfoManager->getInfo('html');
     // The special page regions will appear directly in html.html.twig, not in
     // page.html.twig, hence add them here, just before rendering html.html.twig.
     $this->buildPageTopAndBottom($html);
     // The three parts of rendered markup in html.html.twig (page_top, page and
     // page_bottom) must be rendered with drupal_render_root(), so that their
     // #post_render_cache callbacks are executed (which may attach additional
     // assets).
     // html.html.twig must be able to render the final list of attached assets,
     // and hence may not execute any #post_render_cache_callbacks (because they
     // might add yet more assets to be attached), and therefore it must be
     // rendered with drupal_render(), not drupal_render_root().
     $this->renderer->render($html['page'], TRUE);
     if (isset($html['page_top'])) {
         $this->renderer->render($html['page_top'], TRUE);
     }
     if (isset($html['page_bottom'])) {
         $this->renderer->render($html['page_bottom'], TRUE);
     }
     $content = $this->renderer->render($html);
     // Set the generator in the HTTP header.
     list($version) = explode('.', \Drupal::VERSION, 2);
     $response = new CacheableResponse($content, 200, ['Content-Type' => 'text/html; charset=UTF-8', 'X-Generator' => 'Drupal ' . $version . ' (https://www.drupal.org)']);
     // Bubble the cacheability metadata associated with the rendered render
     // arrays to the response.
     foreach (['page_top', 'page', 'page_bottom'] as $region) {
         if (isset($html[$region])) {
             $response->addCacheableDependency(CacheableMetadata::createFromRenderArray($html[$region]));
         }
     }
     // Also associate the "rendered" cache tag. This allows us to invalidate the
     // entire render cache, regardless of the cache bin.
     $default = new CacheableMetadata();
     $default->setCacheTags(['rendered']);
     $response->addCacheableDependency($default);
     return $response;
 }
Exemple #9
0
 /**
  * {@inheritdoc}
  */
 protected function setUp()
 {
     parent::setUp();
     $this->moduleHandler = $this->getMock('Drupal\\Core\\Extension\\ModuleHandlerInterface');
     $this->formCache = $this->getMock('Drupal\\Core\\Form\\FormCacheInterface');
     $this->cache = $this->getMock('Drupal\\Core\\Cache\\CacheBackendInterface');
     $this->urlGenerator = $this->getMock('Drupal\\Core\\Routing\\UrlGeneratorInterface');
     $this->classResolver = $this->getClassResolverStub();
     $this->elementInfo = $this->getMockBuilder('\\Drupal\\Core\\Render\\ElementInfoManagerInterface')->disableOriginalConstructor()->getMock();
     $this->elementInfo->expects($this->any())->method('getInfo')->will($this->returnCallback(array($this, 'getInfo')));
     $this->csrfToken = $this->getMockBuilder('Drupal\\Core\\Access\\CsrfTokenGenerator')->disableOriginalConstructor()->getMock();
     $this->kernel = $this->getMockBuilder('\\Drupal\\Core\\DrupalKernel')->disableOriginalConstructor()->getMock();
     $this->account = $this->getMock('Drupal\\Core\\Session\\AccountInterface');
     $this->themeManager = $this->getMock('Drupal\\Core\\Theme\\ThemeManagerInterface');
     $this->request = new Request();
     $this->eventDispatcher = $this->getMock('Symfony\\Component\\EventDispatcher\\EventDispatcherInterface');
     $this->requestStack = new RequestStack();
     $this->requestStack->push($this->request);
     $this->logger = $this->getMock('Drupal\\Core\\Logger\\LoggerChannelInterface');
     $form_error_handler = $this->getMock('Drupal\\Core\\Form\\FormErrorHandlerInterface');
     $this->formValidator = $this->getMockBuilder('Drupal\\Core\\Form\\FormValidator')->setConstructorArgs([$this->requestStack, $this->getStringTranslationStub(), $this->csrfToken, $this->logger, $form_error_handler])->setMethods(NULL)->getMock();
     $this->formSubmitter = $this->getMockBuilder('Drupal\\Core\\Form\\FormSubmitter')->setConstructorArgs(array($this->requestStack, $this->urlGenerator))->setMethods(array('batchGet', 'drupalInstallationAttempted'))->getMock();
     $this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))));
     $this->formBuilder = new FormBuilder($this->formValidator, $this->formSubmitter, $this->formCache, $this->moduleHandler, $this->eventDispatcher, $this->requestStack, $this->classResolver, $this->elementInfo, $this->themeManager, $this->csrfToken);
 }
Exemple #10
0
 /**
  * {@inheritdoc}
  */
 public function doBuildForm($form_id, &$element, FormStateInterface &$form_state)
 {
     // Initialize as unprocessed.
     $element['#processed'] = FALSE;
     // Use element defaults.
     if (isset($element['#type']) && empty($element['#defaults_loaded']) && ($info = $this->elementInfo->getInfo($element['#type']))) {
         // Overlay $info onto $element, retaining preexisting keys in $element.
         $element += $info;
         $element['#defaults_loaded'] = TRUE;
     }
     // Assign basic defaults common for all form elements.
     $element += array('#required' => FALSE, '#attributes' => array(), '#title_display' => 'before', '#description_display' => 'after', '#errors' => NULL);
     // Special handling if we're on the top level form element.
     if (isset($element['#type']) && $element['#type'] == 'form') {
         if (!empty($element['#https']) && !UrlHelper::isExternal($element['#action'])) {
             global $base_root;
             // Not an external URL so ensure that it is secure.
             $element['#action'] = str_replace('http://', 'https://', $base_root) . $element['#action'];
         }
         // Store a reference to the complete form in $form_state prior to building
         // the form. This allows advanced #process and #after_build callbacks to
         // perform changes elsewhere in the form.
         $form_state->setCompleteForm($element);
         // Set a flag if we have a correct form submission. This is always TRUE
         // for programmed forms coming from self::submitForm(), or if the form_id
         // coming from the POST data is set and matches the current form_id.
         $input = $form_state->getUserInput();
         if ($form_state->isProgrammed() || !empty($input) && (isset($input['form_id']) && $input['form_id'] == $form_id)) {
             $form_state->setProcessInput();
             if (isset($element['#token'])) {
                 $input = $form_state->getUserInput();
                 if (empty($input['form_token']) || !$this->csrfToken->validate($input['form_token'], $element['#token'])) {
                     // Set an early form error to block certain input processing since
                     // that opens the door for CSRF vulnerabilities.
                     $this->setInvalidTokenError($form_state);
                     // This value is checked in self::handleInputElement().
                     $form_state->setInvalidToken(TRUE);
                     // Make sure file uploads do not get processed.
                     $this->requestStack->getCurrentRequest()->files = new FileBag();
                 }
             }
         } else {
             $form_state->setProcessInput(FALSE);
         }
         // All form elements should have an #array_parents property.
         $element['#array_parents'] = array();
     }
     if (!isset($element['#id'])) {
         $unprocessed_id = 'edit-' . implode('-', $element['#parents']);
         $element['#id'] = Html::getUniqueId($unprocessed_id);
         // Provide a selector usable by JavaScript. As the ID is unique, its not
         // possible to rely on it in JavaScript.
         $element['#attributes']['data-drupal-selector'] = Html::getId($unprocessed_id);
     } else {
         // Provide a selector usable by JavaScript. As the ID is unique, its not
         // possible to rely on it in JavaScript.
         $element['#attributes']['data-drupal-selector'] = Html::getId($element['#id']);
     }
     // Add the aria-describedby attribute to associate the form control with its
     // description.
     if (!empty($element['#description'])) {
         $element['#attributes']['aria-describedby'] = $element['#id'] . '--description';
     }
     // Handle input elements.
     if (!empty($element['#input'])) {
         $this->handleInputElement($form_id, $element, $form_state);
     }
     // Allow for elements to expand to multiple elements, e.g., radios,
     // checkboxes and files.
     if (isset($element['#process']) && !$element['#processed']) {
         foreach ($element['#process'] as $callback) {
             $complete_form =& $form_state->getCompleteForm();
             $element = call_user_func_array($form_state->prepareCallback($callback), array(&$element, &$form_state, &$complete_form));
         }
         $element['#processed'] = TRUE;
     }
     // We start off assuming all form elements are in the correct order.
     $element['#sorted'] = TRUE;
     // Recurse through all child elements.
     $count = 0;
     if (isset($element['#access'])) {
         $access = $element['#access'];
         $inherited_access = NULL;
         if ($access instanceof AccessResultInterface && !$access->isAllowed() || $access === FALSE) {
             $inherited_access = $access;
         }
     }
     foreach (Element::children($element) as $key) {
         // Prior to checking properties of child elements, their default
         // properties need to be loaded.
         if (isset($element[$key]['#type']) && empty($element[$key]['#defaults_loaded']) && ($info = $this->elementInfo->getInfo($element[$key]['#type']))) {
             $element[$key] += $info;
             $element[$key]['#defaults_loaded'] = TRUE;
         }
         // Don't squash an existing tree value.
         if (!isset($element[$key]['#tree'])) {
             $element[$key]['#tree'] = $element['#tree'];
         }
         // Children inherit #access from parent.
         if (isset($inherited_access)) {
             $element[$key]['#access'] = $inherited_access;
         }
         // Make child elements inherit their parent's #disabled and #allow_focus
         // values unless they specify their own.
         foreach (array('#disabled', '#allow_focus') as $property) {
             if (isset($element[$property]) && !isset($element[$key][$property])) {
                 $element[$key][$property] = $element[$property];
             }
         }
         // Don't squash existing parents value.
         if (!isset($element[$key]['#parents'])) {
             // Check to see if a tree of child elements is present. If so,
             // continue down the tree if required.
             $element[$key]['#parents'] = $element[$key]['#tree'] && $element['#tree'] ? array_merge($element['#parents'], array($key)) : array($key);
         }
         // Ensure #array_parents follows the actual form structure.
         $array_parents = $element['#array_parents'];
         $array_parents[] = $key;
         $element[$key]['#array_parents'] = $array_parents;
         // Assign a decimal placeholder weight to preserve original array order.
         if (!isset($element[$key]['#weight'])) {
             $element[$key]['#weight'] = $count / 1000;
         } else {
             // If one of the child elements has a weight then we will need to sort
             // later.
             unset($element['#sorted']);
         }
         $element[$key] = $this->doBuildForm($form_id, $element[$key], $form_state);
         $count++;
     }
     // The #after_build flag allows any piece of a form to be altered
     // after normal input parsing has been completed.
     if (isset($element['#after_build']) && !isset($element['#after_build_done'])) {
         foreach ($element['#after_build'] as $callback) {
             $element = call_user_func_array($form_state->prepareCallback($callback), array($element, &$form_state));
         }
         $element['#after_build_done'] = TRUE;
     }
     // If there is a file element, we need to flip a flag so later the
     // form encoding can be set.
     if (isset($element['#type']) && $element['#type'] == 'file') {
         $form_state->setHasFileElement();
     }
     // Final tasks for the form element after self::doBuildForm() has run for
     // all other elements.
     if (isset($element['#type']) && $element['#type'] == 'form') {
         // If there is a file element, we set the form encoding.
         if ($form_state->hasFileElement()) {
             $element['#attributes']['enctype'] = 'multipart/form-data';
         }
         // Allow Ajax submissions to the form action to bypass verification. This
         // is especially useful for multipart forms, which cannot be verified via
         // a response header.
         $element['#attached']['drupalSettings']['ajaxTrustedUrl'][$element['#action']] = TRUE;
         // If a form contains a single textfield, and the ENTER key is pressed
         // within it, Internet Explorer submits the form with no POST data
         // identifying any submit button. Other browsers submit POST data as
         // though the user clicked the first button. Therefore, to be as
         // consistent as we can be across browsers, if no 'triggering_element' has
         // been identified yet, default it to the first button.
         $buttons = $form_state->getButtons();
         if (!$form_state->isProgrammed() && !$form_state->getTriggeringElement() && !empty($buttons)) {
             $form_state->setTriggeringElement($buttons[0]);
         }
         $triggering_element = $form_state->getTriggeringElement();
         // If the triggering element specifies "button-level" validation and
         // submit handlers to run instead of the default form-level ones, then add
         // those to the form state.
         if (isset($triggering_element['#validate'])) {
             $form_state->setValidateHandlers($triggering_element['#validate']);
         }
         if (isset($triggering_element['#submit'])) {
             $form_state->setSubmitHandlers($triggering_element['#submit']);
         }
         // If the triggering element executes submit handlers, then set the form
         // state key that's needed for those handlers to run.
         if (!empty($triggering_element['#executes_submit_callback'])) {
             $form_state->setSubmitted();
         }
         // Special processing if the triggering element is a button.
         if (!empty($triggering_element['#is_button'])) {
             // Because there are several ways in which the triggering element could
             // have been determined (including from input variables set by
             // JavaScript or fallback behavior implemented for IE), and because
             // buttons often have their #name property not derived from their
             // #parents property, we can't assume that input processing that's
             // happened up until here has resulted in
             // $form_state->getValue(BUTTON_NAME) being set. But it's common for
             // forms to have several buttons named 'op' and switch on
             // $form_state->getValue('op') during submit handler execution.
             $form_state->setValue($triggering_element['#name'], $triggering_element['#value']);
         }
     }
     return $element;
 }
Exemple #11
0
 /**
  * See the docs for ::render().
  */
 protected function doRender(&$elements, $is_root_call = FALSE)
 {
     if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
         if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
             $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
         }
         $elements['#access'] = call_user_func($elements['#access_callback'], $elements);
     }
     // Early-return nothing if user does not have access.
     if (empty($elements) || isset($elements['#access']) && !$elements['#access']) {
         return '';
     }
     // Do not print elements twice.
     if (!empty($elements['#printed'])) {
         return '';
     }
     if (!isset(static::$stack)) {
         static::$stack = new \SplStack();
     }
     static::$stack->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
     //   has these configurable defaults, even when no subtree is render cached.
     // - this is a render cacheable subtree, to ensure that the cached data has
     //   the configurable defaults (which may affect the ID and invalidation).
     if ($is_root_call || isset($elements['#cache']['keys'])) {
         $required_cache_contexts = $this->rendererConfig['required_cache_contexts'];
         if (isset($elements['#cache']['contexts'])) {
             $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts);
         } else {
             $elements['#cache']['contexts'] = $required_cache_contexts;
         }
     }
     // Try to fetch the prerendered element from cache, run any
     // #post_render_cache callbacks and return the final markup.
     if (isset($elements['#cache']['keys'])) {
         $cached_element = $this->renderCache->get($elements);
         if ($cached_element !== FALSE) {
             $elements = $cached_element;
             // Only when we're not in a root (non-recursive) drupal_render() call,
             // #post_render_cache callbacks must be executed, to prevent breaking
             // the render cache in case of nested elements with #cache set.
             if ($is_root_call) {
                 $this->processPostRenderCache($elements);
             }
             // Mark the element markup as safe. If we have cached children, we need
             // to mark them as safe too. The parent markup contains the child
             // markup, so if the parent markup is safe, then the markup of the
             // individual children must be safe as well.
             $elements['#markup'] = SafeMarkup::set($elements['#markup']);
             if (!empty($elements['#cache_properties'])) {
                 foreach (Element::children($cached_element) as $key) {
                     SafeMarkup::set($cached_element[$key]['#markup']);
                 }
             }
             // The render cache item contains all the bubbleable rendering metadata
             // for the subtree.
             $this->updateStack($elements);
             // Render cache hit, so rendering is finished, all necessary info
             // collected!
             $this->bubbleStack();
             return $elements['#markup'];
         }
     }
     // Two-tier caching: track pre-bubbling elements' #cache for later
     // comparison.
     // @see \Drupal\Core\Render\RenderCacheInterface::get()
     // @see \Drupal\Core\Render\RenderCacheInterface::set()
     $pre_bubbling_elements = [];
     $pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : [];
     // If the default values for this element have not been loaded yet, populate
     // them.
     if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
         $elements += $this->elementInfo->getInfo($elements['#type']);
     }
     // Make any final changes to the element before it is rendered. This means
     // that the $element or the children can be altered or corrected before the
     // element is rendered into the final text.
     if (isset($elements['#pre_render'])) {
         foreach ($elements['#pre_render'] as $callable) {
             if (is_string($callable) && strpos($callable, '::') === FALSE) {
                 $callable = $this->controllerResolver->getControllerFromDefinition($callable);
             }
             $elements = call_user_func($callable, $elements);
         }
     }
     // Defaults for bubbleable rendering metadata.
     $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array();
     $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT;
     $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array();
     $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array();
     // Allow #pre_render to abort rendering.
     if (!empty($elements['#printed'])) {
         // The #printed element contains all the bubbleable rendering metadata for
         // the subtree.
         $this->updateStack($elements);
         // #printed, so rendering is finished, all necessary info collected!
         $this->bubbleStack();
         return '';
     }
     // Add any JavaScript state information associated with the element.
     if (!empty($elements['#states'])) {
         drupal_process_states($elements);
     }
     // Get the children of the element, sorted by weight.
     $children = Element::children($elements, TRUE);
     // Initialize this element's #children, unless a #pre_render callback
     // already preset #children.
     if (!isset($elements['#children'])) {
         $elements['#children'] = '';
     }
     // @todo Simplify after https://www.drupal.org/node/2273925.
     if (isset($elements['#markup'])) {
         $elements['#markup'] = SafeMarkup::set($elements['#markup']);
     }
     // Assume that if #theme is set it represents an implemented hook.
     $theme_is_implemented = isset($elements['#theme']);
     // Check the elements for insecure HTML and pass through sanitization.
     if (isset($elements)) {
         $markup_keys = array('#description', '#field_prefix', '#field_suffix');
         foreach ($markup_keys as $key) {
             if (!empty($elements[$key]) && is_scalar($elements[$key])) {
                 $elements[$key] = SafeMarkup::checkAdminXss($elements[$key]);
             }
         }
     }
     // Call the element's #theme function if it is set. Then any children of the
     // element have to be rendered there. If the internal #render_children
     // property is set, do not call the #theme function to prevent infinite
     // recursion.
     if ($theme_is_implemented && !isset($elements['#render_children'])) {
         $elements['#children'] = $this->theme->render($elements['#theme'], $elements);
         // If ThemeManagerInterface::render() returns FALSE this means that the
         // hook in #theme was not found in the registry and so we need to update
         // our flag accordingly. This is common for theme suggestions.
         $theme_is_implemented = $elements['#children'] !== FALSE;
     }
     // If #theme is not implemented or #render_children is set and the element
     // has an empty #children attribute, render the children now. This is the
     // same process as Renderer::render() but is inlined for speed.
     if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) {
         foreach ($children as $key) {
             $elements['#children'] .= $this->doRender($elements[$key]);
         }
         $elements['#children'] = SafeMarkup::set($elements['#children']);
     }
     // If #theme is not implemented and the element has raw #markup as a
     // fallback, prepend the content in #markup to #children. In this case
     // #children will contain whatever is provided by #pre_render prepended to
     // what is rendered recursively above. If #theme is implemented then it is
     // the responsibility of that theme implementation to render #markup if
     // required. Eventually #theme_wrappers will expect both #markup and
     // #children to be a single string as #children.
     if (!$theme_is_implemented && isset($elements['#markup'])) {
         $elements['#children'] = SafeMarkup::set($elements['#markup'] . $elements['#children']);
     }
     // Let the theme functions in #theme_wrappers add markup around the rendered
     // children.
     // #states and #attached have to be processed before #theme_wrappers,
     // because the #type 'page' render array from drupal_prepare_page() would
     // render the $page and wrap it into the html.html.twig template without the
     // attached assets otherwise.
     // If the internal #render_children property is set, do not call the
     // #theme_wrappers function(s) to prevent infinite recursion.
     if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) {
         foreach ($elements['#theme_wrappers'] as $key => $value) {
             // If the value of a #theme_wrappers item is an array then the theme
             // hook is found in the key of the item and the value contains attribute
             // overrides. Attribute overrides replace key/value pairs in $elements
             // for only this ThemeManagerInterface::render() call. This allows
             // #theme hooks and #theme_wrappers hooks to share variable names
             // without conflict or ambiguity.
             $wrapper_elements = $elements;
             if (is_string($key)) {
                 $wrapper_hook = $key;
                 foreach ($value as $attribute => $override) {
                     $wrapper_elements[$attribute] = $override;
                 }
             } else {
                 $wrapper_hook = $value;
             }
             $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements);
         }
     }
     // Filter the outputted content and make any last changes before the content
     // is sent to the browser. The changes are made on $content which allows the
     // outputted text to be filtered.
     if (isset($elements['#post_render'])) {
         foreach ($elements['#post_render'] as $callable) {
             if (is_string($callable) && strpos($callable, '::') === FALSE) {
                 $callable = $this->controllerResolver->getControllerFromDefinition($callable);
             }
             $elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
         }
     }
     // We store the resulting output in $elements['#markup'], to be consistent
     // with how render cached output gets stored. This ensures that
     // #post_render_cache callbacks get the same data to work with, no matter if
     // #cache is disabled, #cache is enabled, there is a cache hit or miss.
     $prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : '';
     $suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : '';
     $elements['#markup'] = $prefix . $elements['#children'] . $suffix;
     // We've rendered this element (and its subtree!), now update the stack.
     $this->updateStack($elements);
     // Cache the processed element if both $pre_bubbling_elements and $elements
     // have the metadata necessary to generate a cache ID.
     if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) {
         if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) {
             throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.');
         }
         $this->renderCache->set($elements, $pre_bubbling_elements);
     }
     // Only when we're in a root (non-recursive) drupal_render() call,
     // #post_render_cache callbacks must be executed, to prevent breaking the
     // render cache in case of nested elements with #cache set.
     //
     // By running them here, we ensure that:
     // - they run when #cache is disabled,
     // - they run when #cache is enabled and there is a cache miss.
     // Only the case of a cache hit when #cache is enabled, is not handled here,
     // that is handled earlier in Renderer::render().
     if ($is_root_call) {
         // We've already called ::updateStack() earlier, which updated both the
         // element and current stack frame. However,
         // Renderer::processPostRenderCache() can both change the element
         // further and create and render new child elements, so provide a fresh
         // stack frame to collect those additions, merge them back to the element,
         // and then update the current frame to match the modified element state.
         do {
             static::$stack->push(new BubbleableMetadata());
             $this->processPostRenderCache($elements);
             $post_render_additions = static::$stack->pop();
             $elements['#post_render_cache'] = NULL;
             BubbleableMetadata::createFromRenderArray($elements)->merge($post_render_additions)->applyTo($elements);
         } while (!empty($elements['#post_render_cache']));
         if (static::$stack->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();
     $elements['#printed'] = TRUE;
     $elements['#markup'] = SafeMarkup::set($elements['#markup']);
     return $elements['#markup'];
 }
Exemple #12
0
 /**
  * See the docs for ::render().
  */
 protected function doRender(&$elements, $is_root_call = FALSE)
 {
     if (empty($elements)) {
         return '';
     }
     if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
         if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
             $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
         }
         $elements['#access'] = call_user_func($elements['#access_callback'], $elements);
     }
     // Early-return nothing if user does not have access.
     if (isset($elements['#access'])) {
         // If #access is an AccessResultInterface object, we must apply it's
         // cacheability metadata to the render array.
         if ($elements['#access'] instanceof AccessResultInterface) {
             $this->addCacheableDependency($elements, $elements['#access']);
             if (!$elements['#access']->isAllowed()) {
                 return '';
             }
         } elseif ($elements['#access'] === FALSE) {
             return '';
         }
     }
     // Do not print elements twice.
     if (!empty($elements['#printed'])) {
         return '';
     }
     $context = $this->getCurrentRenderContext();
     if (!isset($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.");
     }
     $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
     //   has these configurable defaults, even when no subtree is render cached.
     // - this is a render cacheable subtree, to ensure that the cached data has
     //   the configurable defaults (which may affect the ID and invalidation).
     if ($is_root_call || isset($elements['#cache']['keys'])) {
         $required_cache_contexts = $this->rendererConfig['required_cache_contexts'];
         if (isset($elements['#cache']['contexts'])) {
             $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts);
         } else {
             $elements['#cache']['contexts'] = $required_cache_contexts;
         }
     }
     // Try to fetch the prerendered element from cache, replace any placeholders
     // and return the final markup.
     if (isset($elements['#cache']['keys'])) {
         $cached_element = $this->renderCache->get($elements);
         if ($cached_element !== FALSE) {
             $elements = $cached_element;
             // Only when we're in a root (non-recursive) Renderer::render() call,
             // placeholders must be processed, to prevent breaking the render cache
             // in case of nested elements with #cache set.
             if ($is_root_call) {
                 $this->replacePlaceholders($elements);
             }
             // Mark the element markup as safe if is it a string.
             if (is_string($elements['#markup'])) {
                 $elements['#markup'] = SafeString::create($elements['#markup']);
             }
             // The render cache item contains all the bubbleable rendering metadata
             // for the subtree.
             $context->update($elements);
             // Render cache hit, so rendering is finished, all necessary info
             // collected!
             $context->bubble();
             return $elements['#markup'];
         }
     }
     // Two-tier caching: track pre-bubbling elements' #cache for later
     // comparison.
     // @see \Drupal\Core\Render\RenderCacheInterface::get()
     // @see \Drupal\Core\Render\RenderCacheInterface::set()
     $pre_bubbling_elements = [];
     $pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : [];
     // If the default values for this element have not been loaded yet, populate
     // them.
     if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
         $elements += $this->elementInfo->getInfo($elements['#type']);
     }
     // First validate the usage of #lazy_builder; both of the next if-statements
     // use it if available.
     if (isset($elements['#lazy_builder'])) {
         // @todo Convert to assertions once https://www.drupal.org/node/2408013
         //   lands.
         if (!is_array($elements['#lazy_builder'])) {
             throw new \DomainException('The #lazy_builder property must have an array as a value.');
         }
         if (count($elements['#lazy_builder']) !== 2) {
             throw new \DomainException('The #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback.');
         }
         if (count($elements['#lazy_builder'][1]) !== count(array_filter($elements['#lazy_builder'][1], function ($v) {
             return is_null($v) || is_scalar($v);
         }))) {
             throw new \DomainException("A #lazy_builder callback's context may only contain scalar values or NULL.");
         }
         $children = Element::children($elements);
         if ($children) {
             throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no children can exist; all children must be generated by the #lazy_builder callback. You specified the following children: %s.', implode(', ', $children)));
         }
         $supported_keys = ['#lazy_builder', '#cache', '#create_placeholder', '#weight', '#printed'];
         $unsupported_keys = array_diff(array_keys($elements), $supported_keys);
         if (count($unsupported_keys)) {
             throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no properties can exist; all properties must be generated by the #lazy_builder callback. You specified the following properties: %s.', implode(', ', $unsupported_keys)));
         }
     }
     // If instructed to create a placeholder, and a #lazy_builder callback is
     // present (without such a callback, it would be impossible to replace the
     // placeholder), replace the current element with a placeholder.
     if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE) {
         if (!isset($elements['#lazy_builder'])) {
             throw new \LogicException('When #create_placeholder is set, a #lazy_builder callback must be present as well.');
         }
         $elements = $this->createPlaceholder($elements);
     }
     // Build the element if it is still empty.
     if (isset($elements['#lazy_builder'])) {
         $callable = $elements['#lazy_builder'][0];
         $args = $elements['#lazy_builder'][1];
         if (is_string($callable) && strpos($callable, '::') === FALSE) {
             $callable = $this->controllerResolver->getControllerFromDefinition($callable);
         }
         $new_elements = call_user_func_array($callable, $args);
         // Retain the original cacheability metadata, plus cache keys.
         CacheableMetadata::createFromRenderArray($elements)->merge(CacheableMetadata::createFromRenderArray($new_elements))->applyTo($new_elements);
         if (isset($elements['#cache']['keys'])) {
             $new_elements['#cache']['keys'] = $elements['#cache']['keys'];
         }
         $elements = $new_elements;
         $elements['#lazy_builder_built'] = TRUE;
     }
     // Make any final changes to the element before it is rendered. This means
     // that the $element or the children can be altered or corrected before the
     // element is rendered into the final text.
     if (isset($elements['#pre_render'])) {
         foreach ($elements['#pre_render'] as $callable) {
             if (is_string($callable) && strpos($callable, '::') === FALSE) {
                 $callable = $this->controllerResolver->getControllerFromDefinition($callable);
             }
             $elements = call_user_func($callable, $elements);
         }
     }
     // Defaults for bubbleable rendering metadata.
     $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array();
     $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT;
     $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array();
     // Allow #pre_render to abort rendering.
     if (!empty($elements['#printed'])) {
         // The #printed element contains all the bubbleable rendering metadata for
         // the subtree.
         $context->update($elements);
         // #printed, so rendering is finished, all necessary info collected!
         $context->bubble();
         return '';
     }
     // Add any JavaScript state information associated with the element.
     if (!empty($elements['#states'])) {
         drupal_process_states($elements);
     }
     // Get the children of the element, sorted by weight.
     $children = Element::children($elements, TRUE);
     // Initialize this element's #children, unless a #pre_render callback
     // already preset #children.
     if (!isset($elements['#children'])) {
         $elements['#children'] = '';
     }
     if (!empty($elements['#markup'])) {
         // @todo Decide how to support non-HTML in the render API in
         //   https://www.drupal.org/node/2501313.
         $elements['#markup'] = $this->xssFilterAdminIfUnsafe($elements['#markup']);
     }
     // Assume that if #theme is set it represents an implemented hook.
     $theme_is_implemented = isset($elements['#theme']);
     // Check the elements for insecure HTML and pass through sanitization.
     if (isset($elements)) {
         $markup_keys = array('#description', '#field_prefix', '#field_suffix');
         foreach ($markup_keys as $key) {
             if (!empty($elements[$key]) && is_scalar($elements[$key])) {
                 $elements[$key] = $this->xssFilterAdminIfUnsafe($elements[$key]);
             }
         }
     }
     // Call the element's #theme function if it is set. Then any children of the
     // element have to be rendered there. If the internal #render_children
     // property is set, do not call the #theme function to prevent infinite
     // recursion.
     if ($theme_is_implemented && !isset($elements['#render_children'])) {
         $elements['#children'] = $this->theme->render($elements['#theme'], $elements);
         // If ThemeManagerInterface::render() returns FALSE this means that the
         // hook in #theme was not found in the registry and so we need to update
         // our flag accordingly. This is common for theme suggestions.
         $theme_is_implemented = $elements['#children'] !== FALSE;
     }
     // If #theme is not implemented or #render_children is set and the element
     // has an empty #children attribute, render the children now. This is the
     // same process as Renderer::render() but is inlined for speed.
     if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) {
         foreach ($children as $key) {
             $elements['#children'] .= $this->doRender($elements[$key]);
         }
         $elements['#children'] = SafeString::create($elements['#children']);
     }
     // If #theme is not implemented and the element has raw #markup as a
     // fallback, prepend the content in #markup to #children. In this case
     // #children will contain whatever is provided by #pre_render prepended to
     // what is rendered recursively above. If #theme is implemented then it is
     // the responsibility of that theme implementation to render #markup if
     // required. Eventually #theme_wrappers will expect both #markup and
     // #children to be a single string as #children.
     if (!$theme_is_implemented && isset($elements['#markup'])) {
         $elements['#children'] = SafeString::create($elements['#markup'] . $elements['#children']);
     }
     // Let the theme functions in #theme_wrappers add markup around the rendered
     // children.
     // #states and #attached have to be processed before #theme_wrappers,
     // because the #type 'page' render array from drupal_prepare_page() would
     // render the $page and wrap it into the html.html.twig template without the
     // attached assets otherwise.
     // If the internal #render_children property is set, do not call the
     // #theme_wrappers function(s) to prevent infinite recursion.
     if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) {
         foreach ($elements['#theme_wrappers'] as $key => $value) {
             // If the value of a #theme_wrappers item is an array then the theme
             // hook is found in the key of the item and the value contains attribute
             // overrides. Attribute overrides replace key/value pairs in $elements
             // for only this ThemeManagerInterface::render() call. This allows
             // #theme hooks and #theme_wrappers hooks to share variable names
             // without conflict or ambiguity.
             $wrapper_elements = $elements;
             if (is_string($key)) {
                 $wrapper_hook = $key;
                 foreach ($value as $attribute => $override) {
                     $wrapper_elements[$attribute] = $override;
                 }
             } else {
                 $wrapper_hook = $value;
             }
             $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements);
         }
     }
     // Filter the outputted content and make any last changes before the content
     // is sent to the browser. The changes are made on $content which allows the
     // outputted text to be filtered.
     if (isset($elements['#post_render'])) {
         foreach ($elements['#post_render'] as $callable) {
             if (is_string($callable) && strpos($callable, '::') === FALSE) {
                 $callable = $this->controllerResolver->getControllerFromDefinition($callable);
             }
             $elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
         }
     }
     // We store the resulting output in $elements['#markup'], to be consistent
     // with how render cached output gets stored. This ensures that placeholder
     // replacement logic gets the same data to work with, no matter if #cache is
     // disabled, #cache is enabled, there is a cache hit or miss.
     $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : '';
     $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : '';
     $elements['#markup'] = $prefix . $elements['#children'] . $suffix;
     // We've rendered this element (and its subtree!), now update the context.
     $context->update($elements);
     // Cache the processed element if both $pre_bubbling_elements and $elements
     // have the metadata necessary to generate a cache ID.
     if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) {
         if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) {
             throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.');
         }
         $this->renderCache->set($elements, $pre_bubbling_elements);
     }
     // Only when we're in a root (non-recursive) Renderer::render() call,
     // placeholders must be processed, to prevent breaking the render cache in
     // case of nested elements with #cache set.
     //
     // By running them here, we ensure that:
     // - they run when #cache is disabled,
     // - they run when #cache is enabled and there is a cache miss.
     // Only the case of a cache hit when #cache is enabled, is not handled here,
     // that is handled earlier in Renderer::render().
     if ($is_root_call) {
         $this->replacePlaceholders($elements);
         // @todo remove as part of https://www.drupal.org/node/2511330.
         if ($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!
     $context->bubble();
     $elements['#printed'] = TRUE;
     return SafeString::create($elements['#markup']);
 }