/** * {@inheritdoc} */ public function execute($object = NULL) { $url = $this->configuration['url']; // Leave external URLs unchanged, and assemble others as absolute URLs // relative to the site's base URL. if (!UrlHelper::isExternal($url)) { $parts = UrlHelper::parse($url); // @todo '<front>' is valid input for BC reasons, may be removed by // https://www.drupal.org/node/2421941 if ($parts['path'] === '<front>') { $parts['path'] = ''; } $uri = 'base:' . $parts['path']; $options = ['query' => $parts['query'], 'fragment' => $parts['fragment'], 'absolute' => TRUE]; // Treat this as if it's user input of a path relative to the site's // base URL. $url = $this->unroutedUrlAssembler->assemble($uri, $options); } $response = new RedirectResponse($url); $listener = function ($event) use($response) { $event->setResponse($response); }; // Add the listener to the event dispatcher. $this->dispatcher->addListener(KernelEvents::RESPONSE, $listener); }
/** * {@inheritdoc} */ public function isValid($path) { // External URLs and the front page are always valid. if ($path == '<front>' || UrlHelper::isExternal($path)) { return TRUE; } // Check the routing system. $collection = $this->routeProvider->getRoutesByPattern('/' . $path); if ($collection->count() == 0) { return FALSE; } $request = RequestHelper::duplicate($this->requestStack->getCurrentRequest(), '/' . $path); $request->attributes->set('_system_path', $path); // We indicate that a menu administrator is running the menu access check. $request->attributes->set('_menu_admin', TRUE); // Attempt to match this path to provide a fully built request to the // access checker. try { $request->attributes->add($this->requestMatcher->matchRequest($request)); } catch (ParamNotConvertedException $e) { return FALSE; } // Consult the access manager. $routes = $collection->all(); $route = reset($routes); return $this->accessManager->check($route, $request, $this->account); }
/** * {@inheritdoc} * * This is a helper function that calls buildExternalUrl() or buildLocalUrl() * based on a check of whether the path is a valid external URL. */ public function assemble($uri, array $options = []) { // Note that UrlHelper::isExternal will return FALSE if the $uri has a // disallowed protocol. This is later made safe since we always add at // least a leading slash. if (strpos($uri, 'base://') === 0) { return $this->buildLocalUrl($uri, $options); } elseif (UrlHelper::isExternal($uri)) { // UrlHelper::isExternal() only returns true for safe protocols. return $this->buildExternalUrl($uri, $options); } throw new \InvalidArgumentException(String::format('The URI "@uri" is invalid. You must use a valid URI scheme. Use base:// for a path, e.g., to a Drupal file that needs the base path. Do not use this for internal paths controlled by Drupal.', ['@uri' => $uri])); }
/** * {@inheritdoc} * * This is a helper function that calls buildExternalUrl() or buildLocalUrl() * based on a check of whether the path is a valid external URL. */ public function assemble($uri, array $options = [], $collect_bubbleable_metadata = FALSE) { // Note that UrlHelper::isExternal will return FALSE if the $uri has a // disallowed protocol. This is later made safe since we always add at // least a leading slash. if (parse_url($uri, PHP_URL_SCHEME) === 'base') { return $this->buildLocalUrl($uri, $options, $collect_bubbleable_metadata); } elseif (UrlHelper::isExternal($uri)) { // UrlHelper::isExternal() only returns true for safe protocols. return $this->buildExternalUrl($uri, $options, $collect_bubbleable_metadata); } throw new \InvalidArgumentException("The URI '{$uri}' is invalid. You must use a valid URI scheme. Use base: for a path, e.g., to a Drupal file that needs the base path. Do not use this for internal paths controlled by Drupal."); }
/** * {@inheritdoc} */ public function get() { if (!isset($this->destination)) { $query = $this->requestStack->getCurrentRequest()->query; if (UrlHelper::isExternal($query->get('destination'))) { $this->destination = '/'; } elseif ($query->has('destination')) { $this->destination = $query->get('destination'); } else { $this->destination = $this->urlGenerator->generateFromRoute('<current>', [], ['query' => UrlHelper::buildQuery(UrlHelper::filterQueryParameters($query->all()))]); } } return $this->destination; }
/** * Allows manipulation of the response object when performing a redirect. * * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event * The Event to process. */ public function checkRedirectUrl(FilterResponseEvent $event) { $response = $event->getResponse(); if ($response instanceof RedirectResponse) { $options = array(); $destination = $event->getRequest()->query->get('destination'); // A destination from \Drupal::request()->query always overrides the // current RedirectResponse. We do not allow absolute URLs to be passed // via \Drupal::request()->query, as this can be an attack vector, with // the following exception: // - Absolute URLs that point to this site (i.e. same base URL and // base path) are allowed. if ($destination && (!UrlHelper::isExternal($destination) || UrlHelper::externalIsLocal($destination, $GLOBALS['base_url']))) { $destination = UrlHelper::parse($destination); $path = $destination['path']; $options['query'] = $destination['query']; $options['fragment'] = $destination['fragment']; // The 'Location' HTTP header must always be absolute. $options['absolute'] = TRUE; $response->setTargetUrl($this->urlGenerator->generateFromPath($path, $options)); } } }
/** * {@inheritdoc} */ public function process($text, $langcode) { $result = new FilterProcessResult($text); $dom = Html::load($text); $xpath = new \DOMXPath($dom); /** @var \DOMNode $node */ foreach ($xpath->query('//img') as $node) { // Read the data-align attribute's value, then delete it. $width = $node->getAttribute('width'); $height = $node->getAttribute('height'); $src = $node->getAttribute('src'); if (!UrlHelper::isExternal($src)) { if ($width || $height) { /** @var \DOMNode $element */ $element = $dom->createElement('a'); $element->setAttribute('href', $src); $node->parentNode->replaceChild($element, $node); $element->appendChild($node); } } } $result->setProcessedText(Html::serialize($dom)); return $result; }
/** * {@inheritdoc} */ public function massageFormValues(array $values, array $form, array &$form_state) { foreach ($values as &$value) { if (!empty($value['url'])) { try { $parsed_url = UrlHelper::parse($value['url']); // If internal links are supported, look up whether the given value is // a path alias and store the system path instead. if ($this->supportsInternalLinks() && !UrlHelper::isExternal($value['url'])) { $parsed_url['path'] = \Drupal::service('path.alias_manager')->getPathByAlias($parsed_url['path']); } $url = Url::createFromPath($parsed_url['path']); $url->setOption('query', $parsed_url['query']); $url->setOption('fragment', $parsed_url['fragment']); $url->setOption('attributes', $value['attributes']); $value += $url->toArray(); // Reset the URL value to contain only the path. $value['url'] = $parsed_url['path']; } catch (NotFoundHttpException $e) { // Nothing to do here, LinkTypeConstraintValidator emits errors. } catch (MatchingRouteNotFoundException $e) { // Nothing to do here, LinkTypeConstraintValidator emits errors. } catch (ParamNotConvertedException $e) { // Nothing to do here, LinkTypeConstraintValidator emits errors. } } } return $values; }
/** * Helper for getUrlIfValid() and getUrlIfValidWithoutAccessCheck(). */ protected function getUrl($path, $access_check) { $path = ltrim($path, '/'); $parsed_url = UrlHelper::parse($path); $options = []; if (!empty($parsed_url['query'])) { $options['query'] = $parsed_url['query']; } if (!empty($parsed_url['fragment'])) { $options['fragment'] = $parsed_url['fragment']; } if ($parsed_url['path'] == '<front>') { return new Url('<front>', [], $options); } elseif ($parsed_url['path'] == '<none>') { return new Url('<none>', [], $options); } elseif (UrlHelper::isExternal($path) && UrlHelper::isValid($path)) { if (empty($parsed_url['path'])) { return FALSE; } return Url::fromUri($path); } $request = Request::create('/' . $path); $attributes = $this->getPathAttributes($path, $request, $access_check); if (!$attributes) { return FALSE; } $route_name = $attributes[RouteObjectInterface::ROUTE_NAME]; $route_parameters = $attributes['_raw_variables']->all(); return new Url($route_name, $route_parameters, $options + ['query' => $request->query->all()]); }
/** * {@inheritdoc} */ public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { global $base_url; $values = parent::massageFormValues($values, $form, $form_state); $file_urls = []; $countable_fields = $this->getSetting('file_fields'); foreach ($countable_fields as $field) { $files_values = array_filter(array_column($form_state->getValue($field), 'fids')); foreach ($files_values as $file_value) { /** @var FileInterface $file */ $file = File::load(reset($file_value)); if ($file) { $file_urls[] = $file->url(); } } } // Remove removed files from access urls. foreach ($values as $delta => $value) { if (UrlHelper::isExternal($value['uri']) && UrlHelper::externalIsLocal($value['uri'], $base_url) && !in_array($value['uri'], $file_urls)) { unset($values[$delta]); } } // Add new or updated files to the access urls. foreach ($file_urls as $file_url) { if (!array_search($file_url, array_column($values, 'uri'))) { $values[]['uri'] = $file_url; } } return $values; }
/** * Overrides EntityForm::validate(). */ public function validate(array $form, array &$form_state) { $menu_link = $this->buildEntity($form, $form_state); $normal_path = $this->pathAliasManager->getPathByAlias($menu_link->link_path); if ($menu_link->link_path != $normal_path) { drupal_set_message(t('The menu system stores system paths only, but will use the URL alias for display. %link_path has been stored as %normal_path', array('%link_path' => $menu_link->link_path, '%normal_path' => $normal_path))); $menu_link->link_path = $normal_path; $form_state['values']['link_path'] = $normal_path; } if (!UrlHelper::isExternal($menu_link->link_path)) { $parsed_link = parse_url($menu_link->link_path); if (isset($parsed_link['query'])) { $menu_link->options['query'] = array(); parse_str($parsed_link['query'], $menu_link->options['query']); } else { // Use unset() rather than setting to empty string // to avoid redundant serialized data being stored. unset($menu_link->options['query']); } if (isset($parsed_link['fragment'])) { $menu_link->options['fragment'] = $parsed_link['fragment']; } else { unset($menu_link->options['fragment']); } if (isset($parsed_link['path']) && $menu_link->link_path != $parsed_link['path']) { $menu_link->link_path = $parsed_link['path']; } } if (!trim($menu_link->link_path) || !drupal_valid_path($menu_link->link_path, TRUE)) { $this->setFormError('link_path', $form_state, $this->t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $menu_link->link_path))); } parent::validate($form, $form_state); }
/** * Breaks up a user-entered URL or path into all the relevant parts. * * @param string $url * The user-entered URL or path. * * @return array * The extracted parts. */ protected function extractUrl($url) { $extracted = UrlHelper::parse($url); $external = UrlHelper::isExternal($url); if ($external) { $extracted['url'] = $extracted['path']; $extracted['route_name'] = NULL; $extracted['route_parameters'] = []; } else { $extracted['url'] = ''; // If the path doesn't match a Drupal path, the route should end up empty. $extracted['route_name'] = NULL; $extracted['route_parameters'] = []; try { // Find the route_name. $url_obj = \Drupal::pathValidator()->getUrlIfValid($extracted['path']); if ($url_obj) { $extracted['route_name'] = $url_obj->getRouteName(); $extracted['route_parameters'] = $url_obj->getRouteParameters(); } } catch (MatchingRouteNotFoundException $e) { // The path doesn't match a Drupal path. } catch (ParamNotConvertedException $e) { // A path like node/99 matched a route, but the route parameter was // invalid (e.g. node with ID 99 does not exist). } } return $extracted; }
/** * Tests UrlHelper::parse(). */ function testDrupalParseUrl() { // Relative, absolute, and external URLs, without/with explicit script path, // without/with Drupal path. foreach (array('', '/', 'http://drupal.org/') as $absolute) { foreach (array('', 'index.php/') as $script) { foreach (array('', 'foo/bar') as $path) { $url = $absolute . $script . $path . '?foo=bar&bar=baz&baz#foo'; $expected = array('path' => $absolute . $script . $path, 'query' => array('foo' => 'bar', 'bar' => 'baz', 'baz' => ''), 'fragment' => 'foo'); $this->assertEqual(UrlHelper::parse($url), $expected, 'URL parsed correctly.'); } } } // Relative URL that is known to confuse parse_url(). $url = 'foo/bar:1'; $result = array('path' => 'foo/bar:1', 'query' => array(), 'fragment' => ''); $this->assertEqual(UrlHelper::parse($url), $result, 'Relative URL parsed correctly.'); // Test that drupal can recognize an absolute URL. Used to prevent attack vectors. $url = 'http://drupal.org/foo/bar?foo=bar&bar=baz&baz#foo'; $this->assertTrue(UrlHelper::isExternal($url), 'Correctly identified an external URL.'); // Test that UrlHelper::parse() does not allow spoofing a URL to force a malicious redirect. $parts = UrlHelper::parse('forged:http://cwe.mitre.org/data/definitions/601.html'); $this->assertFalse(UrlHelper::isValid($parts['path'], TRUE), '\\Drupal\\Component\\Utility\\UrlHelper::isValid() correctly parsed a forged URL.'); }
/** * Breaks up a user-entered URL or path into all the relevant parts. * * @param string $url * The user-entered URL or path. * * @return array * The extracted parts. */ protected function extractUrl($url) { $extracted = UrlHelper::parse($url); $external = UrlHelper::isExternal($url); if ($external) { $extracted['url'] = $extracted['path']; $extracted['route_name'] = NULL; $extracted['route_parameters'] = array(); } else { $extracted['url'] = ''; // If the path doesn't match a Drupal path, the route should end up empty. $extracted['route_name'] = NULL; $extracted['route_parameters'] = array(); try { // Find the route_name. $normal_path = $this->pathAliasManager->getPathByAlias($extracted['path']); $url_obj = Url::createFromPath($normal_path); $extracted['route_name'] = $url_obj->getRouteName(); $extracted['route_parameters'] = $url_obj->getRouteParameters(); } catch (MatchingRouteNotFoundException $e) { // The path doesn't match a Drupal path. } catch (ParamNotConvertedException $e) { // A path like node/99 matched a route, but the route parameter was // invalid (e.g. node with ID 99 does not exist). } } return $extracted; }
/** * {@inheritdoc} */ public function renderText($alter) { // We need to preserve the safeness of the value regardless of the // alterations made by this method. Any alterations or replacements made // within this method need to ensure that at the minimum the result is // XSS admin filtered. See self::renderAltered() as an example that does. $value_is_safe = $this->last_render instanceof MarkupInterface; // Cast to a string so that empty checks and string functions work as // expected. $value = (string) $this->last_render; if (!empty($alter['alter_text']) && $alter['text'] !== '') { $tokens = $this->getRenderTokens($alter); $value = $this->renderAltered($alter, $tokens); } if (!empty($this->options['alter']['trim_whitespace'])) { $value = trim($value); } // Check if there should be no further rewrite for empty values. $no_rewrite_for_empty = $this->options['hide_alter_empty'] && $this->isValueEmpty($this->original_value, $this->options['empty_zero']); // Check whether the value is empty and return nothing, so the field isn't rendered. // First check whether the field should be hidden if the value(hide_alter_empty = TRUE) /the rewrite is empty (hide_alter_empty = FALSE). // For numeric values you can specify whether "0"/0 should be empty. if ((($this->options['hide_empty'] && empty($value)) || ($alter['phase'] != static::RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty)) && $this->isValueEmpty($value, $this->options['empty_zero'], FALSE)) { return ''; } // Only in empty phase. if ($alter['phase'] == static::RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty) { // If we got here then $alter contains the value of "No results text" // and so there is nothing left to do. return ViewsRenderPipelineMarkup::create($value); } if (!empty($alter['strip_tags'])) { $value = strip_tags($value, $alter['preserve_tags']); } $more_link = ''; if (!empty($alter['trim']) && !empty($alter['max_length'])) { $length = strlen($value); $value = $this->renderTrimText($alter, $value); if ($this->options['alter']['more_link'] && strlen($value) < $length) { $tokens = $this->getRenderTokens($alter); $more_link_text = $this->options['alter']['more_link_text'] ? $this->options['alter']['more_link_text'] : $this->t('more'); $more_link_text = strtr(Xss::filterAdmin($more_link_text), $tokens); $more_link_path = $this->options['alter']['more_link_path']; $more_link_path = strip_tags(Html::decodeEntities($this->viewsTokenReplace($more_link_path, $tokens))); // Make sure that paths which were run through URL generation work as // well. $base_path = base_path(); // Checks whether the path starts with the base_path. if (strpos($more_link_path, $base_path) === 0) { $more_link_path = Unicode::substr($more_link_path, Unicode::strlen($base_path)); } // @todo Views should expect and store a leading /. See // https://www.drupal.org/node/2423913. $options = array( 'attributes' => array( 'class' => array( 'views-more-link', ), ), ); if (UrlHelper::isExternal($more_link_path)) { $more_link_url = CoreUrl::fromUri($more_link_path, $options); } else { $more_link_url = CoreUrl::fromUserInput('/' . $more_link_path, $options); } $more_link = ' ' . $this->linkGenerator()->generate($more_link_text, $more_link_url); } } if (!empty($alter['nl2br'])) { $value = nl2br($value); } if ($value_is_safe) { $value = ViewsRenderPipelineMarkup::create($value); } $this->last_render_text = $value; if (!empty($alter['make_link']) && (!empty($alter['path']) || !empty($alter['url']))) { if (!isset($tokens)) { $tokens = $this->getRenderTokens($alter); } $value = $this->renderAsLink($alter, $value, $tokens); } // Preserve whether or not the string is safe. Since $more_link comes from // \Drupal::l(), it is safe to append. Check if the value is an instance of // \Drupal\Component\Render\MarkupInterface here because renderAsLink() // can return both safe and unsafe values. if ($value instanceof MarkupInterface) { return ViewsRenderPipelineMarkup::create($value . $more_link); } else { // If the string is not already marked safe, it is still OK to return it // because it will be sanitized by Twig. return $value . $more_link; } }
/** * {@inheritdoc} */ protected function isLocal($url) { return !UrlHelper::isExternal($url) || UrlHelper::externalIsLocal($url, $this->getRequestContext()->getCompleteBaseUrl()); }
/** * Returns the Url object matching a path. * * @param string $path * A path (e.g. 'node/1', 'http://drupal.org'). * * @return static * An Url object. * * @throws \Drupal\Core\Routing\MatchingRouteNotFoundException * Thrown when the path cannot be matched. */ public static function createFromPath($path) { if (UrlHelper::isExternal($path)) { $url = new static($path); $url->setExternal(); return $url; } // Special case the front page route. if ($path == '<front>') { $route_name = $path; $route_parameters = array(); } else { // Look up the route name and parameters used for the given path. try { $result = \Drupal::service('router')->match('/' . $path); } catch (ResourceNotFoundException $e) { throw new MatchingRouteNotFoundException(sprintf('No matching route could be found for the path "%s"', $path), 0, $e); } $route_name = $result[RouteObjectInterface::ROUTE_NAME]; $route_parameters = $result['_raw_variables']->all(); } return new static($route_name, $route_parameters); }
/** * {@inheritdoc} */ public function preSave(EntityStorageInterface $storage) { parent::preSave($storage); // This is the easiest way to handle the unique internal path '<front>', // since a path marked as external does not need to match a route. $this->external = UrlHelper::isExternal($this->link_path) || $this->link_path == '<front>' ? 1 : 0; // Try to find a parent link. If found, assign it and derive its menu. $parent = $this->findParent($storage); if ($parent) { $this->plid = $parent->id(); $this->menu_name = $parent->menu_name; } else { $this->plid = 0; } // Directly fill parents for top-level links. if ($this->plid == 0) { $this->p1 = $this->id(); for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) { $parent_property = "p{$i}"; $this->{$parent_property} = 0; } $this->depth = 1; } else { if ($this->has_children && $this->original) { $limit = MENU_MAX_DEPTH - $storage->findChildrenRelativeDepth($this->original) - 1; } else { $limit = MENU_MAX_DEPTH - 1; } if ($parent->depth > $limit) { return FALSE; } $this->depth = $parent->depth + 1; $this->setParents($parent); } // Need to check both plid and menu_name, since plid can be 0 in any menu. if (isset($this->original) && ($this->plid != $this->original->plid || $this->menu_name != $this->original->menu_name)) { $storage->moveChildren($this); } // Find the route_name. if (!$this->external && !isset($this->route_name)) { $url = Url::createFromPath($this->link_path); $this->route_name = $url->getRouteName(); $this->route_parameters = $url->getRouteParameters(); } elseif (empty($this->link_path)) { $this->link_path = \Drupal::urlGenerator()->getPathFromRoute($this->route_name, $this->route_parameters); } }
/** * Encapsulate UrlHelper::isExternal. * * @param string $url * The url to evaluate. * * @return bool * Whether or not the url points to an external location. * * @codeCoverageIgnore */ protected function isExternal($url) { return UrlHelper::isExternal($url); }
/** * {@inheritdoc} */ public function doBuildForm($form_id, &$element, &$form_state) { // Initialize as unprocessed. $element['#processed'] = FALSE; // Use element defaults. if (isset($element['#type']) && empty($element['#defaults_loaded']) && ($info = $this->getElementInfo($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', '#errors' => NULL); // Special handling if we're on the top level form element. if (isset($element['#type']) && $element['#type'] == 'form') { if (!empty($element['#https']) && Settings::get('mixed_mode_sessions', FALSE) && !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['complete_form'] =& $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. if ($form_state['programmed'] || !empty($form_state['input']) && (isset($form_state['input']['form_id']) && $form_state['input']['form_id'] == $form_id)) { $form_state['process_input'] = TRUE; } else { $form_state['process_input'] = FALSE; } // All form elements should have an #array_parents property. $element['#array_parents'] = array(); } if (!isset($element['#id'])) { $element['#id'] = $this->drupalHtmlId('edit-' . implode('-', $element['#parents'])); } // 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 $process) { $element = call_user_func_array($process, array(&$element, &$form_state, &$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; 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->getElementInfo($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']; } // Deny access to child elements if parent is denied. if (isset($element['#access']) && !$element['#access']) { $element[$key]['#access'] = FALSE; } // 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 $callable) { $element = call_user_func_array($callable, 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['has_file_element'] = TRUE; } // 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 (isset($form_state['has_file_element'])) { $element['#attributes']['enctype'] = 'multipart/form-data'; } // 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. if (!$form_state['programmed'] && !isset($form_state['triggering_element']) && !empty($form_state['buttons'])) { $form_state['triggering_element'] = $form_state['buttons'][0]; } // 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. foreach (array('validate', 'submit') as $type) { if (isset($form_state['triggering_element']['#' . $type])) { $form_state[$type . '_handlers'] = $form_state['triggering_element']['#' . $type]; } } // If the triggering element executes submit handlers, then set the form // state key that's needed for those handlers to run. if (!empty($form_state['triggering_element']['#executes_submit_callback'])) { $form_state['submitted'] = TRUE; } // Special processing if the triggering element is a button. if (!empty($form_state['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['values'][BUTTON_NAME] being set. But it's common for // forms to have several buttons named 'op' and switch on // $form_state['values']['op'] during submit handler execution. $form_state['values'][$form_state['triggering_element']['#name']] = $form_state['triggering_element']['#value']; } } return $element; }
/** * Tests external versus internal paths. * * @dataProvider providerTestIsExternal * @covers ::isExternal * * @param string $path * URL or path to test. * @param bool $expected * Expected result. */ public function testIsExternal($path, $expected) { $isExternal = UrlHelper::isExternal($path); $this->assertEquals($expected, $isExternal); }
/** * {@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; }
/** * Creates a new Url object for 'internal:' URIs. * * Important note: the URI minus the scheme can NOT simply be validated by a * \Drupal\Core\Path\PathValidatorInterface implementation. The semantics of * the 'internal:' URI scheme are different: * - PathValidatorInterface accepts paths without a leading slash (e.g. * 'node/add') as well as 2 special paths: '<front>' and '<none>', which are * mapped to the correspondingly named routes. * - 'internal:' URIs store paths with a leading slash that represents the * root — i.e. the front page — (e.g. 'internal:/node/add'), and doesn't * have any exceptions. * * To clarify, a few examples of path plus corresponding 'internal:' URI: * - 'node/add' -> 'internal:/node/add' * - 'node/add?foo=bar' -> 'internal:/node/add?foo=bar' * - 'node/add#kitten' -> 'internal:/node/add#kitten' * - '<front>' -> 'internal:/' * - '<front>foo=bar' -> 'internal:/?foo=bar' * - '<front>#kitten' -> 'internal:/#kitten' * - '<none>' -> 'internal:' * - '<none>foo=bar' -> 'internal:?foo=bar' * - '<none>#kitten' -> 'internal:#kitten' * * Therefore, when using a PathValidatorInterface to validate 'internal:' * URIs, we must map: * - 'internal:' (path component is '') to the special '<none>' path * - 'internal:/' (path component is '/') to the special '<front>' path * - 'internal:/some-path' (path component is '/some-path') to 'some-path' * * @param array $uri_parts * Parts from an URI of the form internal:{path} as from parse_url(). * @param array $options * An array of options, see static::fromUri() for details. * * @return \Drupal\Core\Url * A new Url object for a 'internal:' URI. * * @throws \InvalidArgumentException * Thrown when the URI's path component doesn't have a leading slash. */ protected static function fromInternalUri(array $uri_parts, array $options) { // Both PathValidator::getUrlIfValidWithoutAccessCheck() and 'base:' URIs // only accept/contain paths without a leading slash, unlike 'internal:' // URIs, for which the leading slash means "relative to Drupal root" and // "relative to Symfony app root" (just like in Symfony/Drupal 8 routes). if (empty($uri_parts['path'])) { $uri_parts['path'] = '<none>'; } elseif ($uri_parts['path'] === '/') { $uri_parts['path'] = '<front>'; } else { if ($uri_parts['path'][0] !== '/') { throw new \InvalidArgumentException(SafeMarkup::format('The internal path component "@path" is invalid. Its path component must have a leading slash, e.g. internal:/foo.', ['@path' => $uri_parts['path']])); } // Remove the leading slash. $uri_parts['path'] = substr($uri_parts['path'], 1); if (UrlHelper::isExternal($uri_parts['path'])) { throw new \InvalidArgumentException(SafeMarkup::format('The internal path component "@path" is external. You are not allowed to specify an external URL together with internal:/.', ['@path' => $uri_parts['path']])); } } $url = \Drupal::pathValidator()->getUrlIfValidWithoutAccessCheck($uri_parts['path']) ?: static::fromUri('base:' . $uri_parts['path'], $options); // Allow specifying additional options. $url->setOptions($options + $url->getOptions()); return $url; }
/** * Retrieves a Drupal path or an absolute path. * * @param string $path * Drupal path or URL to load into Mink controlled browser. * @param array $options * (optional) Options to be forwarded to the url generator. * * @return string * The retrieved HTML string, also available as $this->getRawContent() */ protected function drupalGet($path, array $options = array()) { $options['absolute'] = TRUE; // The URL generator service is not necessarily available yet; e.g., in // interactive installer tests. if ($this->container->has('url_generator')) { if (UrlHelper::isExternal($path)) { $url = Url::fromUri($path, $options)->toString(); } else { // This is needed for language prefixing. $options['path_processing'] = TRUE; $url = Url::fromUri('base:/' . $path, $options)->toString(); } } else { $url = $this->getAbsoluteUrl($path); } $session = $this->getSession(); $this->prepareRequest(); $session->visit($url); $out = $session->getPage()->getContent(); // Ensure that any changes to variables in the other thread are picked up. $this->refreshVariables(); return $out; }
/** * Sanitize the destination parameter to prevent open redirect attacks. * * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event * The Event to process. */ public function sanitizeDestination(GetResponseEvent $event) { $request = $event->getRequest(); // Sanitize the destination parameter (which is often used for redirects) to // prevent open redirect attacks leading to other domains. Sanitize both // $_GET['destination'] and $_REQUEST['destination'] to protect code that // relies on either, but do not sanitize $_POST to avoid interfering with // unrelated form submissions. The sanitization happens here because // url_is_external() requires the variable system to be available. $query_info = $request->query; $request_info = $request->request; if ($query_info->has('destination') || $request_info->has('destination')) { // If the destination is an external URL, remove it. if ($query_info->has('destination') && UrlHelper::isExternal($query_info->get('destination'))) { $query_info->remove('destination'); $request_info->remove('destination'); } // If there's still something in $_REQUEST['destination'] that didn't come // from $_GET, check it too. if ($request_info->has('destination') && (!$query_info->has('destination') || $request_info->get('destination') != $query_info->get('destination')) && UrlHelper::isExternal($request_info->get('destination'))) { $request_info->remove('destination'); } } }
/** * Builds an a absolute URL from a system path or a URL object. * * @param string|\Drupal\Core\Url $path * A system path or a URL. * @param array $options * Options to be passed to Url::fromUri(). * * @return string * An absolute URL stsring. */ protected function buildUrl($path, array $options = array()) { if ($path instanceof Url) { $url_options = $path->getOptions(); $options = $url_options + $options; $path->setOptions($options); return $path->setAbsolute()->toString(); } elseif ($this->container->has('url_generator')) { $force_internal = isset($options['external']) && $options['external'] == FALSE; if (!$force_internal && UrlHelper::isExternal($path)) { return Url::fromUri($path, $options)->toString(); } else { $uri = $path === '<front>' ? 'base:/' : 'base:/' . $path; // Path processing is needed for language prefixing. Skip it when a // path that may look like an external URL is being used as internal. $options['path_processing'] = !$force_internal; return Url::fromUri($uri, $options)->setAbsolute()->toString(); } } else { return $this->getAbsoluteUrl($path); } }
/** * Retrieves a Drupal path or an absolute path. * * @param string|\Drupal\Core\Url $path * Drupal path or URL to load into Mink controlled browser. * @param array $options * (optional) Options to be forwarded to the url generator. * * @return string * The retrieved HTML string, also available as $this->getRawContent() */ protected function drupalGet($path, array $options = array()) { $options['absolute'] = TRUE; if ($path instanceof Url) { $url_options = $path->getOptions(); $options = $url_options + $options; $path->setOptions($options); $url = $path->setAbsolute()->toString(); } // The URL generator service is not necessarily available yet; e.g., in // interactive installer tests. elseif ($this->container->has('url_generator')) { if (UrlHelper::isExternal($path)) { $url = Url::fromUri($path, $options)->toString(); } else { // This is needed for language prefixing. $options['path_processing'] = TRUE; $url = Url::fromUri('base:/' . $path, $options)->toString(); } } else { $url = $this->getAbsoluteUrl($path); } $session = $this->getSession(); $this->prepareRequest(); $session->visit($url); $out = $session->getPage()->getContent(); // Ensure that any changes to variables in the other thread are picked up. $this->refreshVariables(); if ($this->htmlOutputEnabled) { $html_output = 'GET request to: ' . $url . '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl(); $html_output .= '<hr />' . $out; $html_output .= $this->getHtmlOutputHeaders(); $this->htmlOutput($html_output); } return $out; }