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