/** * Handles the response for inline entity form autocompletion. * * @return \Symfony\Component\HttpFoundation\JsonResponse */ public function autocomplete($entity_type_id, $field_name, $bundle, Request $request) { $string = $request->query->get('q'); $fields = $this->entityManager->getFieldDefinitions($entity_type_id, $bundle); $widget = $this->entityManager->getStorage('entity_form_display')->load($entity_type_id . '.' . $bundle . '.default')->getComponent($field_name); // The path was passed invalid parameters, or the string is empty. // strlen() is used instead of empty() since '0' is a valid value. if (!isset($fields[$field_name]) || !$widget || !strlen($string)) { throw new AccessDeniedHttpException(); } $field = $fields[$field_name]; $results = array(); if ($field->getType() == 'entity_reference') { /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler */ $handler = $this->selectionManager->getSelectionHandler($field); $entity_labels = $handler->getReferenceableEntities($string, $widget['settings']['match_operator'], 10); foreach ($entity_labels as $bundle => $labels) { // Loop through each entity type, and autocomplete with its titles. foreach ($labels as $entity_id => $label) { // entityreference has already check_plain-ed the title. $results[] = t('!label (!entity_id)', array('!label' => $label, '!entity_id' => $entity_id)); } } } $matches = array(); foreach ($results as $result) { // Strip things like starting/trailing white spaces, line breaks and tags. $key = preg_replace('/\\s\\s+/', ' ', str_replace("\n", '', trim(Html::decodeEntities(strip_tags($result))))); $matches[] = ['value' => $key, 'label' => '<div class="reference-autocomplete">' . $result . '</div>']; } return new JsonResponse($matches); }
/** * Gets the list of links used by this field. * * @return array * The links which are used by the render function. */ protected function getLinks() { $links = array(); foreach ($this->options['fields'] as $field) { if (empty($this->view->field[$field]->last_render_text)) { continue; } $title = $this->view->field[$field]->last_render_text; $path = ''; $url = NULL; if (!empty($this->view->field[$field]->options['alter']['path'])) { $path = $this->view->field[$field]->options['alter']['path']; } elseif (!empty($this->view->field[$field]->options['alter']['url']) && $this->view->field[$field]->options['alter']['url'] instanceof UrlObject) { $url = $this->view->field[$field]->options['alter']['url']; } // Make sure that tokens are replaced for this paths as well. $tokens = $this->getRenderTokens(array()); $path = strip_tags(Html::decodeEntities($this->viewsTokenReplace($path, $tokens))); $links[$field] = array('url' => $path ? UrlObject::fromUri('internal:/' . $path) : $url, 'title' => $title); if (!empty($this->options['destination'])) { $links[$field]['query'] = \Drupal::destination()->getAsArray(); } } return $links; }
/** * {@inheritdoc} */ public function process($text, $langcode) { $result = new FilterProcessResult($text); if (stristr($text, 'data-caption') !== FALSE) { $dom = Html::load($text); $xpath = new \DOMXPath($dom); foreach ($xpath->query('//*[@data-caption]') as $node) { // Read the data-caption attribute's value, then delete it. $caption = Html::escape($node->getAttribute('data-caption')); $node->removeAttribute('data-caption'); // Sanitize caption: decode HTML encoding, limit allowed HTML tags; only // allow inline tags that are allowed by default, plus <br>. $caption = Html::decodeEntities($caption); $caption = FilteredMarkup::create(Xss::filter($caption, array('a', 'em', 'strong', 'cite', 'code', 'br'))); // The caption must be non-empty. if (Unicode::strlen($caption) === 0) { continue; } // Given the updated node and caption: re-render it with a caption, but // bubble up the value of the class attribute of the captioned element, // this allows it to collaborate with e.g. the filter_align filter. $tag = $node->tagName; $classes = $node->getAttribute('class'); $node->removeAttribute('class'); $node = $node->parentNode->tagName === 'a' ? $node->parentNode : $node; $filter_caption = array('#theme' => 'filter_caption', '#node' => FilteredMarkup::create($node->C14N()), '#tag' => $tag, '#caption' => $caption, '#classes' => $classes); $altered_html = drupal_render($filter_caption); // Load the altered HTML into a new DOMDocument and retrieve the element. $updated_nodes = Html::load($altered_html)->getElementsByTagName('body')->item(0)->childNodes; foreach ($updated_nodes as $updated_node) { // Import the updated node from the new DOMDocument into the original // one, importing also the child nodes of the updated node. $updated_node = $dom->importNode($updated_node, TRUE); $node->parentNode->insertBefore($updated_node, $node); } // Finally, remove the original data-caption node. $node->parentNode->removeChild($node); } $result->setProcessedText(Html::serialize($dom))->addAttachments(array('library' => array('filter/caption'))); } return $result; }
/** * Returns matched labels based on a given search string. * * @param string $target_type * The ID of the target entity type. * @param string $selection_handler * The plugin ID of the entity reference selection handler. * @param array $selection_settings * An array of settings that will be passed to the selection handler. * @param string $string * (optional) The label of the entity to query by. * * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException * Thrown when the current user doesn't have access to the specifies entity. * * @return array * An array of matched entity labels, in the format required by the AJAX * autocomplete API (e.g. array('value' => $value, 'label' => $label)). * * @see \Drupal\system\Controller\EntityAutocompleteController */ public function getMatches($target_type, $selection_handler, $selection_settings, $string = '') { $matches = array(); $options = array('target_type' => $target_type, 'handler' => $selection_handler, 'handler_settings' => $selection_settings); $handler = $this->selectionManager->getInstance($options); if (isset($string)) { // Get an array of matching entities. $match_operator = !empty($selection_settings['match_operator']) ? $selection_settings['match_operator'] : 'CONTAINS'; $entity_labels = $handler->getReferenceableEntities($string, $match_operator, 10); // Loop through the entities and convert them into autocomplete output. foreach ($entity_labels as $values) { foreach ($values as $entity_id => $label) { $key = "{$label} ({$entity_id})"; // Strip things like starting/trailing white spaces, line breaks and // tags. $key = preg_replace('/\\s\\s+/', ' ', str_replace("\n", '', trim(Html::decodeEntities(strip_tags($key))))); // Names containing commas or quotes must be wrapped in quotes. $key = Tags::encode($key); $matches[] = array('value' => $key, 'label' => $label); } } } return $matches; }
/** * Recursive function to add replacements for nested query string parameters. * * E.g. if you pass in the following array: * array( * 'foo' => array( * 'a' => 'value', * 'b' => 'value', * ), * 'bar' => array( * 'a' => 'value', * 'b' => array( * 'c' => value, * ), * ), * ); * * Would yield the following array of tokens: * array( * '%foo_a' => 'value' * '%foo_b' => 'value' * '%bar_a' => 'value' * '%bar_b_c' => 'value' * ); * * @param $array * An array of values. * * @param $parent_keys * An array of parent keys. This will represent the array depth. * * @return * An array of available tokens, with nested keys representative of the array structure. */ protected function getTokenValuesRecursive(array $array, array $parent_keys = array()) { $tokens = array(); foreach ($array as $param => $val) { if (is_array($val)) { // Copy parent_keys array, so we don't affect other elements of this // iteration. $child_parent_keys = $parent_keys; $child_parent_keys[] = $param; // Get the child tokens. $child_tokens = $this->getTokenValuesRecursive($val, $child_parent_keys); // Add them to the current tokens array. $tokens += $child_tokens; } else { // Create a token key based on array element structure. $token_string = !empty($parent_keys) ? implode('_', $parent_keys) . '_' . $param : $param; $tokens['%' . $token_string] = strip_tags(Html::decodeEntities($val)); } } return $tokens; }
protected function getAllEvents(DrupalStyle $io, $eventType, $eventSeverity, $userId, $asc, $offset, $limit) { $connection = $this->getDrupalService('database'); $dateFormatter = $this->getDrupalService('date.formatter'); $userStorage = $this->getDrupalService('entity_type.manager')->getStorage('user'); $severity = RfcLogLevel::getLevels(); $query = $connection->select('watchdog', 'w'); $query->fields('w', ['wid', 'uid', 'severity', 'type', 'timestamp', 'message', 'variables']); if ($eventType) { $query->condition('type', $eventType); } if ($eventSeverity) { if (!in_array($eventSeverity, $severity)) { $io->error(sprintf($this->trans('commands.database.log.debug.messages.invalid-severity'), $eventSeverity)); return false; } $query->condition('severity', array_search($eventSeverity, $severity)); } if ($userId) { $query->condition('uid', $userId); } if ($asc) { $query->orderBy('wid', 'ASC'); } else { $query->orderBy('wid', 'DESC'); } if ($limit) { $query->range($offset, $limit); } $result = $query->execute(); $tableHeader = [$this->trans('commands.database.log.debug.messages.event-id'), $this->trans('commands.database.log.debug.messages.type'), $this->trans('commands.database.log.debug.messages.date'), $this->trans('commands.database.log.debug.messages.message'), $this->trans('commands.database.log.debug.messages.user'), $this->trans('commands.database.log.debug.messages.severity')]; $tableRows = []; foreach ($result as $dblog) { $user = $userStorage->load($dblog->uid); $tableRows[] = [$dblog->wid, $dblog->type, $dateFormatter->format($dblog->timestamp, 'short'), Unicode::truncate(Html::decodeEntities(strip_tags($this->formatMessage($dblog))), 56, true, true), $user->getUsername() . ' (' . $user->id() . ')', $severity[$dblog->severity]]; } $io->table($tableHeader, $tableRows); return true; }
/** * Determines if a given button triggered the form submission. * * This detects button controls that trigger a form submission by being * clicked and having the click processed by the browser rather than being * captured by JavaScript. Essentially, it detects if the button's name and * value are part of the POST data, but with extra code to deal with the * convoluted way in which browsers submit data for image button clicks. * * This does not detect button clicks processed by Ajax (that is done in * self::elementTriggeredScriptedSubmission()) and it does not detect form * submissions from Internet Explorer in response to an ENTER key pressed in a * textfield (self::doBuildForm() has extra code for that). * * Because this function contains only part of the logic needed to determine * $form_state->getTriggeringElement(), it should not be called from anywhere * other than within the Form API. Form validation and submit handlers needing * to know which button was clicked should get that information from * $form_state->getTriggeringElement(). */ protected function buttonWasClicked($element, FormStateInterface &$form_state) { // First detect normal 'vanilla' button clicks. Traditionally, all standard // buttons on a form share the same name (usually 'op'), and the specific // return value is used to determine which was clicked. This ONLY works as // long as $form['#name'] puts the value at the top level of the tree of // \Drupal::request()->request data. $input = $form_state->getUserInput(); // The input value attribute is treated as CDATA by browsers. This means // that they replace character entities with characters. Therefore, we need // to decode the value in $element['#value']. For more details see // http://www.w3.org/TR/html401/types.html#type-cdata. if (isset($input[$element['#name']]) && $input[$element['#name']] == Html::decodeEntities($element['#value'])) { return TRUE; } elseif (!empty($element['#has_garbage_value']) && isset($element['#value']) && $element['#value'] !== '') { return TRUE; } return FALSE; }
/** * Sends BigPipe placeholders' replacements as embedded AJAX responses. * * @param array $placeholders * Associative array; the BigPipe placeholders. Keys are the BigPipe * selectors. * @param array $placeholder_order * Indexed array; the order in which the BigPipe placeholders must be sent. * Values are the BigPipe selectors. (These values correspond to keys in * $placeholders.) * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets * The cumulative assets sent so far; to be updated while rendering BigPipe * placeholders. */ protected function sendPlaceholders(array $placeholders, array $placeholder_order, AttachedAssetsInterface $cumulative_assets) { // Return early if there are no BigPipe placeholders to send. if (empty($placeholders)) { return; } // Send a container and the start signal. print "\n"; print '<script type="application/json" data-big-pipe-event="start"></script>' . "\n"; flush(); // A BigPipe response consists of a HTML response plus multiple embedded // AJAX responses. To process the attachments of those AJAX responses, we // need a fake request that is identical to the master request, but with // one change: it must have the right Accept header, otherwise the work- // around for a bug in IE9 will cause not JSON, but <textarea>-wrapped JSON // to be returned. // @see \Drupal\Core\EventSubscriber\AjaxResponseSubscriber::onResponse() $fake_request = $this->requestStack->getMasterRequest()->duplicate(); $fake_request->headers->set('Accept', 'application/json'); foreach ($placeholder_order as $placeholder) { if (!isset($placeholders[$placeholder])) { continue; } // Render the placeholder. $placeholder_render_array = $placeholders[$placeholder]; $elements = $this->renderPlaceholder($placeholder, $placeholder_render_array); // Create a new AjaxResponse. $ajax_response = new AjaxResponse(); // JavaScript's querySelector automatically decodes HTML entities in // attributes, so we must decode the entities of the current BigPipe // placeholder (which has HTML entities encoded since we use it to find // the placeholders). $big_pipe_js_selector = Html::decodeEntities($placeholder); $ajax_response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-selector="%s"]', $big_pipe_js_selector), $elements['#markup'])); $ajax_response->setAttachments($elements['#attached']); // Push a fake request with the asset libraries loaded so far and dispatch // KernelEvents::RESPONSE event. This results in the attachments for the // AJAX response being processed by AjaxResponseAttachmentsProcessor and // hence: // - the necessary AJAX commands to load the necessary missing asset // libraries and updated AJAX page state are added to the AJAX response // - the attachments associated with the response are finalized, which // allows us to track the total set of asset libraries sent in the // initial HTML response plus all embedded AJAX responses sent so far. $fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())] + $cumulative_assets->getSettings()['ajaxPageState']); $this->requestStack->push($fake_request); $event = new FilterResponseEvent($this->httpKernel, $fake_request, HttpKernelInterface::SUB_REQUEST, $ajax_response); $this->eventDispatcher->dispatch(KernelEvents::RESPONSE, $event); $ajax_response = $event->getResponse(); $this->requestStack->pop(); // Send this embedded AJAX response. $json = $ajax_response->getContent(); $output = <<<EOF <script type="application/json" data-big-pipe-placeholder="{$placeholder}" data-drupal-ajax-processor="big_pipe"> {$json} </script> EOF; print $output; flush(); // Another placeholder was rendered and sent, track the set of asset // libraries sent so far. Any new settings are already sent; we don't need // to track those. if (isset($ajax_response->getAttachments()['drupalSettings']['ajaxPageState']['libraries'])) { $cumulative_assets->setAlreadyLoadedLibraries(explode(',', $ajax_response->getAttachments()['drupalSettings']['ajaxPageState']['libraries'])); } } // Send the stop signal. print '<script type="application/json" data-big-pipe-event="stop"></script>' . "\n"; flush(); }
/** * Asserts that text transformed to lowercase with HTML entities decoded does not contain a given string. * * Otherwise fails the test with a given message, similar to all the * SimpleTest assert* functions. * * Note that this does not remove nulls, new lines, and other character that * could be used to obscure a tag or an attribute name. * * @param $haystack * Text to look in. * @param $needle * Lowercase, plain text to look for. * @param $message * (optional) Message to display if failed. Defaults to an empty string. * @param $group * (optional) The group this message belongs to. Defaults to 'Other'. * * @return bool * TRUE on pass, FALSE on fail. */ function assertNoNormalized($haystack, $needle, $message = '', $group = 'Other') { return $this->assertTrue(strpos(strtolower(Html::decodeEntities($haystack)), $needle) === FALSE, $message, $group); }
/** * Menu callback; autocomplete.js callback to return a list of files. */ public static function autocomplete(Request $request, $entity_type, $bundle_name, $field_name) { $matches = array(); $string = Unicode::strtolower($request->query->get('q')); $field_definition = entity_load('field_config', $entity_type . '.' . $bundle_name . '.' . $field_name); $handler = \Drupal::getContainer()->get('plugin.manager.entity_reference_selection')->getSelectionHandler($field_definition); if (isset($string)) { // Get an array of matching entities. $widget = entity_get_form_display($entity_type, $bundle_name, 'default')->getComponent($field_name); $autocomplete_type = $widget['third_party_settings']['filefield_sources']['filefield_sources']['source_reference']['autocomplete']; $match_operator = !empty($autocomplete_type) ? $autocomplete_type : FILEFIELD_SOURCE_REFERENCE_CONTAINS_AUTOCOMPLETE_TYPE; $entity_labels = $handler->getReferenceableEntities($string, $match_operator, 10); // Loop through the entities and convert them into autocomplete output. foreach ($entity_labels as $values) { foreach ($values as $entity_id => $label) { $key = "{$label} [fid:{$entity_id}]"; // Strip things like starting/trailing white spaces, line breaks and // tags. $key = preg_replace('/\\s\\s+/', ' ', str_replace("\n", '', trim(Html::decodeEntities(strip_tags($key))))); // Names containing commas or quotes must be wrapped in quotes. $matches[] = array('value' => $key, 'label' => $label); } } } return new JsonResponse($matches); }
/** * Confirms that a log message appears on the database log overview screen. * * This function should only be used for the admin/reports/dblog page, because * it checks for the message link text truncated to 56 characters. Other log * pages have no detail links so they contain the full message text. * * @param string $log_message * The database log message to check. * @param string $message * The message to pass to simpletest. */ protected function assertLogMessage($log_message, $message) { $message_text = Unicode::truncate(Html::decodeEntities(strip_tags($log_message)), 56, TRUE, TRUE); $this->assertLink($message_text, 0, $message); }
/** * Tests rewriting the output to a link. */ public function testAlterUrl() { /** @var \Drupal\Core\Render\RendererInterface $renderer */ $renderer = \Drupal::service('renderer'); $view = Views::getView('test_view'); $view->setDisplay(); $view->initHandlers(); $this->executeView($view); $row = $view->result[0]; $id_field = $view->field['id']; // Setup the general settings required to build a link. $id_field->options['alter']['make_link'] = TRUE; $id_field->options['alter']['path'] = $path = $this->randomMachineName(); // Tests that the suffix/prefix appears on the output. $id_field->options['alter']['prefix'] = $prefix = $this->randomMachineName(); $id_field->options['alter']['suffix'] = $suffix = $this->randomMachineName(); $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString($output, $prefix); $this->assertSubString($output, $suffix); unset($id_field->options['alter']['prefix']); unset($id_field->options['alter']['suffix']); $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString($output, $path, 'Make sure that the path is part of the output'); // Some generic test code adapted from the UrlTest class, which tests // mostly the different options for the path. foreach (array(FALSE, TRUE) as $absolute) { $alter =& $id_field->options['alter']; $alter['path'] = 'node/123'; $expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['absolute' => $absolute]); $alter['absolute'] = $absolute; $result = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString($result, $expected_result); $expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['fragment' => 'foo', 'absolute' => $absolute]); $alter['path'] = 'node/123#foo'; $result = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString($result, $expected_result); $expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['query' => ['foo' => NULL], 'absolute' => $absolute]); $alter['path'] = 'node/123?foo'; $result = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString($result, $expected_result); $expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['query' => ['foo' => 'bar', 'bar' => 'baz'], 'absolute' => $absolute]); $alter['path'] = 'node/123?foo=bar&bar=baz'; $result = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString(Html::decodeEntities($result), Html::decodeEntities($expected_result)); // @todo The route-based URL generator strips out NULL attributes. // $expected_result = \Drupal::url('entity.node.canonical', ['node' => '123'], ['query' => ['foo' => NULL], 'fragment' => 'bar', 'absolute' => $absolute]); $expected_result = \Drupal::urlGenerator()->generateFromPath('node/123', array('query' => array('foo' => NULL), 'fragment' => 'bar', 'absolute' => $absolute)); $alter['path'] = 'node/123?foo#bar'; $result = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString(Html::decodeEntities($result), Html::decodeEntities($expected_result)); $expected_result = \Drupal::url('<front>', [], ['absolute' => $absolute]); $alter['path'] = '<front>'; $result = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString($result, $expected_result); } // Tests the replace spaces with dashes feature. $id_field->options['alter']['replace_spaces'] = TRUE; $id_field->options['alter']['path'] = $path = $this->randomMachineName() . ' ' . $this->randomMachineName(); $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString($output, str_replace(' ', '-', $path)); $id_field->options['alter']['replace_spaces'] = FALSE; $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); // The url has a space in it, so to check we have to decode the url output. $this->assertSubString(urldecode($output), $path); // Tests the external flag. // Switch on the external flag should output an external url as well. $id_field->options['alter']['external'] = TRUE; $id_field->options['alter']['path'] = $path = 'www.drupal.org'; $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString($output, 'http://www.drupal.org'); // Setup a not external url, which shouldn't lead to an external url. $id_field->options['alter']['external'] = FALSE; $id_field->options['alter']['path'] = $path = 'www.drupal.org'; $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertNotSubString($output, 'http://www.drupal.org'); // Tests the transforming of the case setting. $id_field->options['alter']['path'] = $path = $this->randomMachineName(); $id_field->options['alter']['path_case'] = 'none'; $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString($output, $path); // Switch to uppercase and lowercase. $id_field->options['alter']['path_case'] = 'upper'; $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString($output, strtoupper($path)); $id_field->options['alter']['path_case'] = 'lower'; $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString($output, strtolower($path)); // Switch to ucfirst and ucwords. $id_field->options['alter']['path_case'] = 'ucfirst'; $id_field->options['alter']['path'] = 'drupal has a great community'; $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString($output, UrlHelper::encodePath('Drupal has a great community')); $id_field->options['alter']['path_case'] = 'ucwords'; $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $this->assertSubString($output, UrlHelper::encodePath('Drupal Has A Great Community')); unset($id_field->options['alter']['path_case']); // Tests the linkclass setting and see whether it actually exists in the // output. $id_field->options['alter']['link_class'] = $class = $this->randomMachineName(); $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $elements = $this->xpathContent($output, '//a[contains(@class, :class)]', array(':class' => $class)); $this->assertTrue($elements); // @fixme link_class, alt, rel cannot be unset, which should be fixed. $id_field->options['alter']['link_class'] = ''; // Tests the alt setting. $id_field->options['alter']['alt'] = $rel = $this->randomMachineName(); $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $elements = $this->xpathContent($output, '//a[contains(@title, :alt)]', array(':alt' => $rel)); $this->assertTrue($elements); $id_field->options['alter']['alt'] = ''; // Tests the rel setting. $id_field->options['alter']['rel'] = $rel = $this->randomMachineName(); $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $elements = $this->xpathContent($output, '//a[contains(@rel, :rel)]', array(':rel' => $rel)); $this->assertTrue($elements); $id_field->options['alter']['rel'] = ''; // Tests the target setting. $id_field->options['alter']['target'] = $target = $this->randomMachineName(); $output = $renderer->executeInRenderContext(new RenderContext(), function () use($id_field, $row) { return $id_field->theme($row); }); $elements = $this->xpathContent($output, '//a[contains(@target, :target)]', array(':target' => $target)); $this->assertTrue($elements); unset($id_field->options['alter']['target']); }
/** * Removes superfluous whitespace and unescapes HTML entities. * * @param string $value * The text to process. * * @return string * The text without unnecessary whitespace and HTML entities transformed * back to plain text. */ protected function normalizeText($value) { $value = Html::decodeEntities($value); $value = trim($value); $value = preg_replace('/\s+/', ' ', $value); return $value; }
/** * {@inheritdoc} */ protected function sanitizeLabel(&$label) { // Select form inputs allow unencoded HTML entities, but no HTML tags. $label = Html::decodeEntities(strip_tags($label)); }
/** * Tests Html::decodeEntities(). * * @dataProvider providerDecodeEntities * @covers ::decodeEntities */ public function testDecodeEntities($text, $expected) { $this->assertEquals($expected, Html::decodeEntities($text)); }
/** * Tests the failed search text, and various other text on the search page. */ function testSearchText() { $this->drupalLogin($this->searchingUser); $this->drupalGet('search/node'); $this->assertText(t('Enter your keywords')); $this->assertText(t('Search')); $this->assertTitle(t('Search') . ' | Drupal', 'Search page title is correct'); $edit = array(); $search_terms = 'bike shed ' . $this->randomMachineName(); $edit['keys'] = $search_terms; $this->drupalPostForm('search/node', $edit, t('Search')); $this->assertText('search yielded no results'); $this->assertText(t('Search')); $title_source = 'Search for @keywords | Drupal'; $this->assertTitle(t($title_source, array('@keywords' => Unicode::truncate($search_terms, 60, TRUE, TRUE))), 'Search page title is correct'); $this->assertNoText('Node', 'Erroneous tab and breadcrumb text is not present'); $this->assertNoText(t('Node'), 'Erroneous translated tab and breadcrumb text is not present'); $this->assertText(t('Content'), 'Tab and breadcrumb text is present'); $this->clickLink('Search help'); $this->assertText('Search help', 'Correct title is on search help page'); $this->assertText('Use upper-case OR to get more results', 'Correct text is on content search help page'); // Search for a longer text, and see that it is in the title, truncated. $edit = array(); $search_terms = 'Every word is like an unnecessary stain on silence and nothingness.'; $edit['keys'] = $search_terms; $this->drupalPostForm('search/node', $edit, t('Search')); $this->assertTitle(t($title_source, array('@keywords' => 'Every word is like an unnecessary stain on silence and…')), 'Search page title is correct'); // Search for a string with a lot of special characters. $search_terms = 'Hear nothing > "see nothing" `feel' . " '1982."; $edit['keys'] = $search_terms; $this->drupalPostForm('search/node', $edit, t('Search')); $actual_title = (string) current($this->xpath('//title')); $this->assertEqual($actual_title, Html::decodeEntities(t($title_source, array('@keywords' => Unicode::truncate($search_terms, 60, TRUE, TRUE)))), 'Search page title is correct'); $edit['keys'] = $this->searchingUser->getUsername(); $this->drupalPostForm('search/user', $edit, t('Search')); $this->assertText(t('Search')); $this->assertTitle(t($title_source, array('@keywords' => Unicode::truncate($this->searchingUser->getUsername(), 60, TRUE, TRUE)))); $this->clickLink('Search help'); $this->assertText('Search help', 'Correct title is on search help page'); $this->assertText('user names and partial user names', 'Correct text is on user search help page'); // Test that search keywords containing slashes are correctly loaded // from the GET params and displayed in the search form. $arg = $this->randomMachineName() . '/' . $this->randomMachineName(); $this->drupalGet('search/node', array('query' => array('keys' => $arg))); $input = $this->xpath("//input[@id='edit-keys' and @value='{$arg}']"); $this->assertFalse(empty($input), 'Search keys with a / are correctly set as the default value in the search box.'); // Test a search input exceeding the limit of AND/OR combinations to test // the Denial-of-Service protection. $limit = $this->config('search.settings')->get('and_or_limit'); $keys = array(); for ($i = 0; $i < $limit + 1; $i++) { // Use a key of 4 characters to ensure we never generate 'AND' or 'OR'. $keys[] = $this->randomMachineName(4); if ($i % 2 == 0) { $keys[] = 'OR'; } } $edit['keys'] = implode(' ', $keys); $this->drupalPostForm('search/node', $edit, t('Search')); $this->assertRaw(t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', array('@count' => $limit))); // Test that a search on Node or User with no keywords entered generates // the "Please enter some keywords" message. $this->drupalPostForm('search/node', array(), t('Search')); $this->assertText(t('Please enter some keywords'), 'With no keywords entered, message is displayed on node page'); $this->drupalPostForm('search/user', array(), t('Search')); $this->assertText(t('Please enter some keywords'), 'With no keywords entered, message is displayed on user page'); // Make sure the "Please enter some keywords" message is NOT displayed if // you use "or" words or phrases in Advanced Search. $this->drupalPostForm('search/node', array('or' => $this->randomMachineName() . ' ' . $this->randomMachineName()), t('Advanced search')); $this->assertNoText(t('Please enter some keywords'), 'With advanced OR keywords entered, no keywords message is not displayed on node page'); $this->drupalPostForm('search/node', array('phrase' => '"' . $this->randomMachineName() . '" "' . $this->randomMachineName() . '"'), t('Advanced search')); $this->assertNoText(t('Please enter some keywords'), 'With advanced phrase entered, no keywords message is not displayed on node page'); // Verify that if you search for a too-short keyword, you get the right // message, and that if after that you search for a longer keyword, you // do not still see the message. $this->drupalPostForm('search/node', array('keys' => $this->randomMachineName(1)), t('Search')); $this->assertText('You must include at least one positive keyword', 'Keyword message is displayed when searching for short word'); $this->assertNoText(t('Please enter some keywords'), 'With short word entered, no keywords message is not displayed'); $this->drupalPostForm(NULL, array('keys' => $this->randomMachineName()), t('Search')); $this->assertNoText('You must include at least one positive keyword', 'Keyword message is not displayed when searching for long word after short word search'); // Test that if you search for a URL with .. in it, you still end up at // the search page. See issue https://www.drupal.org/node/890058. $this->drupalPostForm('search/node', array('keys' => '../../admin'), t('Search')); $this->assertResponse(200, 'Searching for ../../admin with non-admin user does not lead to a 403 error'); $this->assertText('no results', 'Searching for ../../admin with non-admin user gives you a no search results page'); // Test that if you search for a URL starting with "./", you still end up // at the search page. See issue https://www.drupal.org/node/1421560. $this->drupalPostForm('search/node', array('keys' => '.something'), t('Search')); $this->assertResponse(200, 'Searching for .something does not lead to a 403 error'); $this->assertText('no results', 'Searching for .something gives you a no search results page'); }
/** * Overrides \Drupal\views\Plugin\views\field\FieldPluginBase::render(). * * Renders the contextual fields. * * @param \Drupal\views\ResultRow $values * The values retrieved from a single row of a view's query result. * * @see contextual_preprocess() * @see contextual_contextual_links_view_alter() */ public function render(ResultRow $values) { $links = array(); foreach ($this->options['fields'] as $field) { $rendered_field = $this->view->style_plugin->getField($values->index, $field); if (empty($rendered_field)) { continue; } $title = $this->view->field[$field]->last_render_text; $path = ''; if (!empty($this->view->field[$field]->options['alter']['path'])) { $path = $this->view->field[$field]->options['alter']['path']; } elseif (!empty($this->view->field[$field]->options['alter']['url']) && $this->view->field[$field]->options['alter']['url'] instanceof Url) { $path = $this->view->field[$field]->options['alter']['url']->toString(); } if (!empty($title) && !empty($path)) { // Make sure that tokens are replaced for this paths as well. $tokens = $this->getRenderTokens(array()); $path = strip_tags(Html::decodeEntities(strtr($path, $tokens))); $links[$field] = array('href' => $path, 'title' => $title); if (!empty($this->options['destination'])) { $links[$field]['query'] = $this->getDestinationArray(); } } } // Renders a contextual links placeholder. if (!empty($links)) { $contextual_links = array('contextual' => array('', array(), array('contextual-views-field-links' => UrlHelper::encodePath(Json::encode($links))))); $element = array('#type' => 'contextual_links_placeholder', '#id' => _contextual_links_to_id($contextual_links)); return drupal_render($element); } else { return ''; } }
/** * Transforms an HTML string into plain text, preserving its structure. * * The output will be suitable for use as 'format=flowed; delsp=yes' text * (RFC 3676) and can be passed directly to MailManagerInterface::mail() for sending. * * We deliberately use LF rather than CRLF, see MailManagerInterface::mail(). * * This function provides suitable alternatives for the following tags: * <a> <em> <i> <strong> <b> <br> <p> <blockquote> <ul> <ol> <li> <dl> <dt> * <dd> <h1> <h2> <h3> <h4> <h5> <h6> <hr> * * @param string $string * The string to be transformed. * @param array $allowed_tags * (optional) If supplied, a list of tags that will be transformed. If * omitted, all supported tags are transformed. * * @return string * The transformed string. */ public static function htmlToText($string, $allowed_tags = NULL) { // Cache list of supported tags. if (empty(static::$supportedTags)) { static::$supportedTags = array('a', 'em', 'i', 'strong', 'b', 'br', 'p', 'blockquote', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr'); } // Make sure only supported tags are kept. $allowed_tags = isset($allowed_tags) ? array_intersect(static::$supportedTags, $allowed_tags) : static::$supportedTags; // Make sure tags, entities and attributes are well-formed and properly // nested. $string = Html::normalize(Xss::filter($string, $allowed_tags)); // Apply inline styles. $string = preg_replace('!</?(em|i)((?> +)[^>]*)?>!i', '/', $string); $string = preg_replace('!</?(strong|b)((?> +)[^>]*)?>!i', '*', $string); // Replace inline <a> tags with the text of link and a footnote. // 'See <a href="https://www.drupal.org">the Drupal site</a>' becomes // 'See the Drupal site [1]' with the URL included as a footnote. static::htmlToMailUrls(NULL, TRUE); $pattern = '@(<a[^>]+?href="([^"]*)"[^>]*?>(.+?)</a>)@i'; $string = preg_replace_callback($pattern, 'static::htmlToMailUrls', $string); $urls = static::htmlToMailUrls(); $footnotes = ''; if (count($urls)) { $footnotes .= "\n"; for ($i = 0, $max = count($urls); $i < $max; $i++) { $footnotes .= '[' . ($i + 1) . '] ' . $urls[$i] . "\n"; } } // Split tags from text. $split = preg_split('/<([^>]+?)>/', $string, -1, PREG_SPLIT_DELIM_CAPTURE); // Note: PHP ensures the array consists of alternating delimiters and // literals and begins and ends with a literal (inserting $null as // required). // Odd/even counter (tag or no tag). $tag = FALSE; // Case conversion function. $casing = NULL; $output = ''; // All current indentation string chunks. $indent = array(); // Array of counters for opened lists. $lists = array(); foreach ($split as $value) { // Holds a string ready to be formatted and output. $chunk = NULL; // Process HTML tags (but don't output any literally). if ($tag) { list($tagname) = explode(' ', strtolower($value), 2); switch ($tagname) { // List counters. case 'ul': array_unshift($lists, '*'); break; case 'ol': array_unshift($lists, 1); break; case '/ul': case '/ol': array_shift($lists); // Ensure blank new-line. $chunk = ''; break; // Quotation/list markers, non-fancy headers. // Quotation/list markers, non-fancy headers. case 'blockquote': // Format=flowed indentation cannot be mixed with lists. $indent[] = count($lists) ? ' "' : '>'; break; case 'li': $indent[] = isset($lists[0]) && is_numeric($lists[0]) ? ' ' . $lists[0]++ . ') ' : ' * '; break; case 'dd': $indent[] = ' '; break; case 'h3': $indent[] = '.... '; break; case 'h4': $indent[] = '.. '; break; case '/blockquote': if (count($lists)) { // Append closing quote for inline quotes (immediately). $output = rtrim($output, "> \n") . "\"\n"; // Ensure blank new-line. $chunk = ''; } // Fall-through. // Fall-through. case '/li': case '/dd': array_pop($indent); break; case '/h3': case '/h4': array_pop($indent); case '/h5': case '/h6': // Ensure blank new-line. $chunk = ''; break; // Fancy headers. // Fancy headers. case 'h1': $indent[] = '======== '; $casing = '\\Drupal\\Component\\Utility\\Unicode::strtoupper'; break; case 'h2': $indent[] = '-------- '; $casing = '\\Drupal\\Component\\Utility\\Unicode::strtoupper'; break; case '/h1': case '/h2': $casing = NULL; // Pad the line with dashes. $output = static::htmlToTextPad($output, $tagname == '/h1' ? '=' : '-', ' '); array_pop($indent); // Ensure blank new-line. $chunk = ''; break; // Horizontal rulers. // Horizontal rulers. case 'hr': // Insert immediately. $output .= static::wrapMail('', implode('', $indent)) . "\n"; $output = static::htmlToTextPad($output, '-'); break; // Paragraphs and definition lists. // Paragraphs and definition lists. case '/p': case '/dl': // Ensure blank new-line. $chunk = ''; break; } } else { // Convert inline HTML text to plain text; not removing line-breaks or // white-space, since that breaks newlines when sanitizing plain-text. $value = trim(Html::decodeEntities($value)); if (Unicode::strlen($value)) { $chunk = $value; } } // See if there is something waiting to be output. if (isset($chunk)) { // Apply any necessary case conversion. if (isset($casing)) { $chunk = call_user_func($casing, $chunk); } $line_endings = Settings::get('mail_line_endings', PHP_EOL); // Format it and apply the current indentation. $output .= static::wrapMail($chunk, implode('', $indent)) . $line_endings; // Remove non-quotation markers from indentation. $indent = array_map('\\Drupal\\Core\\Mail\\MailFormatHelper::htmlToTextClean', $indent); } $tag = !$tag; } return $output . $footnotes; }
/** * {@inheritdoc} */ public function buildEntity(array $form, FormStateInterface $form_state) { /** @var \Drupal\comment\CommentInterface $comment */ $comment = parent::buildEntity($form, $form_state); if (!$form_state->isValueEmpty('date') && $form_state->getValue('date') instanceof DrupalDateTime) { $comment->setCreatedTime($form_state->getValue('date')->getTimestamp()); } else { $comment->setCreatedTime(REQUEST_TIME); } $author_name = $form_state->getValue('name'); if (!$this->currentUser->isAnonymous()) { // Assign the owner based on the given user name - none means anonymous. $accounts = $this->entityManager->getStorage('user')->loadByProperties(array('name' => $author_name)); $account = reset($accounts); $uid = $account ? $account->id() : 0; $comment->setOwnerId($uid); } // If the comment was posted by an anonymous user and no author name was // required, use "Anonymous" by default. if ($comment->getOwnerId() === 0 && (!isset($author_name) || $author_name === '')) { $comment->setAuthorName($this->config('user.settings')->get('anonymous')); } // Validate the comment's subject. If not specified, extract from comment // body. if (trim($comment->getSubject()) == '') { if ($comment->hasField('comment_body')) { // The body may be in any format, so: // 1) Filter it into HTML // 2) Strip out all HTML tags // 3) Convert entities back to plain-text. $comment_text = $comment->comment_body->processed; $comment->setSubject(Unicode::truncate(trim(Html::decodeEntities(strip_tags($comment_text))), 29, TRUE, TRUE)); } // Edge cases where the comment body is populated only by HTML tags will // require a default subject. if ($comment->getSubject() == '') { $comment->setSubject($this->t('(No subject)')); } } return $comment; }
/** * {@inheritdoc} */ public function getArgumentsTokens() { $tokens = array(); if (!empty($this->view->build_info['substitutions'])) { $tokens = $this->view->build_info['substitutions']; } // Add tokens for every argument (contextual filter) and path arg. $handlers = count($this->view->display_handler->getHandlers('argument')); for ($count = 1; $count <= $handlers; $count++) { if (!isset($tokens["%{$count}"])) { $tokens["%{$count}"] = ''; } // Use strip tags as there should never be HTML in the path. // However, we need to preserve special characters like " that // were encoded by \Drupal\Component\Utility\Html::escape(). $tokens["!{$count}"] = isset($this->view->args[$count - 1]) ? strip_tags(Html::decodeEntities($this->view->args[$count - 1])) : ''; } return $tokens; }
/** * Asserts expected BigPipe placeholders are present and replaced. * * @param array $expected_big_pipe_placeholders * Keys: BigPipe placeholder IDs. Values: expected AJAX response. */ protected function assertBigPipePlaceholders(array $expected_big_pipe_placeholders) { $this->pass('Verifying BigPipe placeholders & replacements…', 'Debug'); $this->assertSetsEqual(array_keys($expected_big_pipe_placeholders), explode(' ', $this->drupalGetHeader('BigPipe-Test-Placeholders'))); $placeholder_positions = []; $placeholder_replacement_positions = []; foreach ($expected_big_pipe_placeholders as $big_pipe_placeholder_id => $expected_ajax_response) { $this->pass('BigPipe placeholder: ' . $big_pipe_placeholder_id, 'Debug'); // Verify expected placeholder. $expected_placeholder_html = '<div data-big-pipe-placeholder-id="' . $big_pipe_placeholder_id . '"></div>'; $this->assertRaw($expected_placeholder_html, 'BigPipe placeholder for placeholder ID "' . $big_pipe_placeholder_id . '" found.'); $pos = strpos($this->getRawContent(), $expected_placeholder_html); $placeholder_positions[$pos] = $big_pipe_placeholder_id; // Verify expected placeholder replacement. $result = $this->xpath('//script[@data-big-pipe-replacement-for-placeholder-with-id=:id]', [':id' => Html::decodeEntities($big_pipe_placeholder_id)]); $this->assertEqual($expected_ajax_response, trim((string) $result[0])); $expected_placeholder_replacement = '<script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="' . $big_pipe_placeholder_id . '">'; $this->assertRaw($expected_placeholder_replacement); $pos = strpos($this->getRawContent(), $expected_placeholder_replacement); $placeholder_replacement_positions[$pos] = $big_pipe_placeholder_id; } ksort($placeholder_positions, SORT_NUMERIC); $this->assertEqual(array_keys($expected_big_pipe_placeholders), array_values($placeholder_positions)); $this->assertEqual(count($expected_big_pipe_placeholders), preg_match_all('/' . preg_quote('<div data-big-pipe-placeholder-id="', '/') . '/', $this->getRawContent())); $this->assertEqual(array_keys($expected_big_pipe_placeholders), array_values($placeholder_replacement_positions)); $this->assertEqual(count($expected_big_pipe_placeholders), preg_match_all('/' . preg_quote('<script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="', '/') . '/', $this->getRawContent())); $this->pass('Verifying BigPipe start/stop signals…', 'Debug'); $this->assertRaw(BigPipe::START_SIGNAL, 'BigPipe start signal present.'); $this->assertRaw(BigPipe::STOP_SIGNAL, 'BigPipe stop signal present.'); $start_signal_position = strpos($this->getRawContent(), BigPipe::START_SIGNAL); $stop_signal_position = strpos($this->getRawContent(), BigPipe::STOP_SIGNAL); $this->assertTrue($start_signal_position < $stop_signal_position, 'BigPipe start signal appears before stop signal.'); $this->pass('Verifying BigPipe placeholder replacements and start/stop signals were streamed in the correct order…', 'Debug'); $expected_stream_order = array_keys($expected_big_pipe_placeholders); array_unshift($expected_stream_order, BigPipe::START_SIGNAL); array_push($expected_stream_order, BigPipe::STOP_SIGNAL); $actual_stream_order = $placeholder_replacement_positions + [$start_signal_position => BigPipe::START_SIGNAL, $stop_signal_position => BigPipe::STOP_SIGNAL]; ksort($actual_stream_order, SORT_NUMERIC); $this->assertEqual($expected_stream_order, array_values($actual_stream_order)); }
/** * {@inheritdoc} */ public function buildEntity(array $form, FormStateInterface $form_state) { /** @var \Drupal\comment\CommentInterface $comment */ $comment = parent::buildEntity($form, $form_state); if (!$form_state->isValueEmpty('date') && $form_state->getValue('date') instanceof DrupalDateTime) { $comment->setCreatedTime($form_state->getValue('date')->getTimestamp()); } else { $comment->setCreatedTime(REQUEST_TIME); } // Empty author ID should revert to anonymous. $author_id = $form_state->getValue('uid'); if ($comment->id() && $this->currentUser->hasPermission('administer comments')) { // Admin can leave the author ID blank to revert to anonymous. $author_id = $author_id ?: 0; } if (!is_null($author_id)) { if ($author_id === 0 && $form['author']['name']['#access']) { // Use the author name value when the form has access to the element and // the author ID is anonymous. $comment->setAuthorName($form_state->getValue('name')); } else { // Ensure the author name is not set. $comment->setAuthorName(NULL); } } else { $author_id = $this->currentUser->id(); } $comment->setOwnerId($author_id); // Validate the comment's subject. If not specified, extract from comment // body. if (trim($comment->getSubject()) == '') { if ($comment->hasField('comment_body')) { // The body may be in any format, so: // 1) Filter it into HTML // 2) Strip out all HTML tags // 3) Convert entities back to plain-text. $comment_text = $comment->comment_body->processed; $comment->setSubject(Unicode::truncate(trim(Html::decodeEntities(strip_tags($comment_text))), 29, TRUE, TRUE)); } // Edge cases where the comment body is populated only by HTML tags will // require a default subject. if ($comment->getSubject() == '') { $comment->setSubject($this->t('(No subject)')); } } return $comment; }
/** * Sanitizes the HTML select element's options. * * The function is recursive to support optgroups. */ protected function prepareFilterSelectOptions(&$options) { foreach ($options as $value => $label) { // Recurse for optgroups. if (is_array($label)) { $this->prepareFilterSelectOptions($options[$value]); } elseif (is_object($label) && isset($label->option)) { $this->prepareFilterSelectOptions($options[$value]->option); } else { // Cast the label to a string since it can be an object. // @see \Drupal\Core\StringTranslation\TranslationWrapper $options[$value] = strip_tags(Html::decodeEntities((string) $label)); } } }
/** * Returns snippets from a piece of text, with certain keywords highlighted. * * Largely copied from search_excerpt(). * * @param string $text * The text to extract fragments from. * @param array $keys * The search keywords entered by the user. * * @return string|null * A string containing HTML for the excerpt. Or NULL if no excerpt could be * created. */ protected function createExcerpt($text, array $keys) { // Prepare text by stripping HTML tags and decoding HTML entities. $text = strip_tags(str_replace(array('<', '>'), array(' <', '> '), $text)); $text = Html::decodeEntities($text); $text = preg_replace('/\\s+/', ' ', $text); $text = trim($text, ' '); $text_length = strlen($text); // Try to reach the requested excerpt length with about two fragments (each // with a keyword and some context). $ranges = array(); $length = 0; $look_start = array(); $remaining_keys = $keys; // Get the set excerpt length from the configuration. If the length is too // small, only use one fragment. $excerpt_length = $this->configuration['excerpt_length']; $context_length = round($excerpt_length / 4) - 3; if ($context_length < 32) { $context_length = round($excerpt_length / 2) - 1; } while ($length < $excerpt_length && !empty($remaining_keys)) { $found_keys = array(); foreach ($remaining_keys as $key) { if ($length >= $excerpt_length) { break; } // Remember where we last found $key, in case we are coming through a // second time. if (!isset($look_start[$key])) { $look_start[$key] = 0; } // See if we can find $key after where we found it the last time. Since // we are requiring a match on a word boundary, make sure $text starts // and ends with a space. $matches = array(); if (preg_match('/' . self::$boundary . preg_quote($key, '/') . self::$boundary . '/iu', ' ' . $text . ' ', $matches, PREG_OFFSET_CAPTURE, $look_start[$key])) { $found_position = $matches[0][1]; $look_start[$key] = $found_position + 1; // Keep track of which keys we found this time, in case we need to // pass through again to find more text. $found_keys[] = $key; // Locate a space before and after this match, leaving some context on // each end. if ($found_position > $context_length) { $before = strpos($text, ' ', $found_position - $context_length); if ($before !== FALSE) { ++$before; } } else { $before = 0; } if ($before !== FALSE && $before <= $found_position) { if ($text_length > $found_position + $context_length) { $after = strrpos(substr($text, 0, $found_position + $context_length), ' ', $found_position); } else { $after = $text_length; } if ($after !== FALSE && $after > $found_position) { if ($before < $after) { // Save this range. $ranges[$before] = $after; $length += $after - $before; } } } } } // Next time through this loop, only look for keys we found this time, // if any. $remaining_keys = $found_keys; } if (!$ranges) { // We didn't find any keyword matches, return NULL. return NULL; } // Sort the text ranges by starting position. ksort($ranges); // Collapse overlapping text ranges into one. The sorting makes it O(n). $new_ranges = array(); $max_end = 0; $working_from = $working_to = NULL; foreach ($ranges as $this_from => $this_to) { $max_end = max($max_end, $this_to); if (is_null($working_from)) { // This is the first time through this loop: initialize. $working_from = $this_from; $working_to = $this_to; continue; } if ($this_from <= $working_to) { // The ranges overlap: combine them. $working_to = max($working_to, $this_to); } else { // The ranges do not overlap: save the working range and start a new // one. $new_ranges[$working_from] = $working_to; $working_from = $this_from; $working_to = $this_to; } } // Save the remaining working range. $new_ranges[$working_from] = $working_to; // Fetch text within the combined ranges we found. $out = array(); foreach ($new_ranges as $from => $to) { $out[] = Html::escape(substr($text, $from, $to - $from)); } if (!$out) { return NULL; } $ellipses = $this->getEllipses(); $excerpt = $ellipses[0] . implode($ellipses[1], $out) . $ellipses[2]; return $this->highlightField($excerpt, $keys); }
/** * Sends BigPipe placeholders' replacements as embedded AJAX responses. * * @param array $placeholders * Associative array; the BigPipe placeholders. Keys are the BigPipe * placeholder IDs. * @param array $placeholder_order * Indexed array; the order in which the BigPipe placeholders must be sent. * Values are the BigPipe placeholder IDs. (These values correspond to keys * in $placeholders.) * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets * The cumulative assets sent so far; to be updated while rendering BigPipe * placeholders. * * @throws \Exception * If an exception is thrown during the rendering of a placeholder, it is * caught to allow the other placeholders to still be replaced. But when * error logging is configured to be verbose, the exception is rethrown to * simplify debugging. */ protected function sendPlaceholders(array $placeholders, array $placeholder_order, AttachedAssetsInterface $cumulative_assets) { // Return early if there are no BigPipe placeholders to send. if (empty($placeholders)) { return; } // Send the start signal. print "\n"; print static::START_SIGNAL; print "\n"; flush(); // A BigPipe response consists of a HTML response plus multiple embedded // AJAX responses. To process the attachments of those AJAX responses, we // need a fake request that is identical to the master request, but with // one change: it must have the right Accept header, otherwise the work- // around for a bug in IE9 will cause not JSON, but <textarea>-wrapped JSON // to be returned. // @see \Drupal\Core\EventSubscriber\AjaxResponseSubscriber::onResponse() $fake_request = $this->requestStack->getMasterRequest()->duplicate(); $fake_request->headers->set('Accept', 'application/vnd.drupal-ajax'); foreach ($placeholder_order as $placeholder_id) { if (!isset($placeholders[$placeholder_id])) { continue; } // Render the placeholder. $placeholder_render_array = $placeholders[$placeholder_id]; try { $elements = $this->renderPlaceholder($placeholder_id, $placeholder_render_array); } catch (\Exception $e) { if (\Drupal::config('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) { throw $e; } else { trigger_error($e, E_USER_ERROR); continue; } } // Create a new AjaxResponse. $ajax_response = new AjaxResponse(); // JavaScript's querySelector automatically decodes HTML entities in // attributes, so we must decode the entities of the current BigPipe // placeholder ID (which has HTML entities encoded since we use it to find // the placeholders). $big_pipe_js_placeholder_id = Html::decodeEntities($placeholder_id); $ajax_response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-placeholder-id="%s"]', $big_pipe_js_placeholder_id), $elements['#markup'])); $ajax_response->setAttachments($elements['#attached']); // Push a fake request with the asset libraries loaded so far and dispatch // KernelEvents::RESPONSE event. This results in the attachments for the // AJAX response being processed by AjaxResponseAttachmentsProcessor and // hence: // - the necessary AJAX commands to load the necessary missing asset // libraries and updated AJAX page state are added to the AJAX response // - the attachments associated with the response are finalized, which // allows us to track the total set of asset libraries sent in the // initial HTML response plus all embedded AJAX responses sent so far. $fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())] + $cumulative_assets->getSettings()['ajaxPageState']); try { $ajax_response = $this->filterEmbeddedResponse($fake_request, $ajax_response); } catch (\Exception $e) { if (\Drupal::config('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) { throw $e; } else { trigger_error($e, E_USER_ERROR); continue; } } // Send this embedded AJAX response. $json = $ajax_response->getContent(); $output = <<<EOF <script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="{$placeholder_id}"> {$json} </script> EOF; print $output; flush(); // Another placeholder was rendered and sent, track the set of asset // libraries sent so far. Any new settings are already sent; we don't need // to track those. if (isset($ajax_response->getAttachments()['drupalSettings']['ajaxPageState']['libraries'])) { $cumulative_assets->setAlreadyLoadedLibraries(explode(',', $ajax_response->getAttachments()['drupalSettings']['ajaxPageState']['libraries'])); } } // Send the stop signal. print "\n"; print static::STOP_SIGNAL; print "\n"; flush(); }
/** * Renders an embedded entity. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity to be rendered. * @param array $context * (optional) Array of context values, corresponding to the attributes on * the embed HTML tag. * * @return string * The HTML of the entity rendered with the Entity Embed Display plugin. */ protected function renderEntityEmbed(EntityInterface $entity, array $context = array()) { // Support the deprecated view-mode data attribute. if (isset($context['data-view-mode']) && !isset($context['data-entity-embed-display']) && !isset($context['data-entity-embed-settings'])) { $context['data-entity-embed-display'] = 'entity_reference:entity_reference_entity_view'; $context['data-entity-embed-settings'] = ['view_mode' => &$context['data-view-mode']]; } // Merge in default attributes. $context += array('data-entity-id' => $entity->id(), 'data-entity-type' => $entity->getEntityTypeId(), 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', 'data-entity-embed-settings' => array()); // The default Entity Embed Display plugin has been deprecated by the // rendered entity field formatter. if ($context['data-entity-embed-display'] === 'default') { $context['data-entity-embed-display'] = 'entity_reference:entity_reference_entity_view'; } // The caption text is double-encoded, so decode it here. if (isset($context['data-caption'])) { $context['data-caption'] = Html::decodeEntities($context['data-caption']); } // Allow modules to alter the entity prior to embed rendering. $this->moduleHandler()->alter(array("{$context['data-entity-type']}_embed_context", 'entity_embed_context'), $context, $entity); // Build and render the Entity Embed Display plugin, allowing modules to // alter the result before rendering. $build = $this->renderEntityEmbedDisplayPlugin($entity, $context['data-entity-embed-display'], $context['data-entity-embed-settings'], $context); // Maintain data-align if it is there. if (isset($context['data-align'])) { $build['#attributes']['data-align'] = $context['data-align']; } elseif (isset($context['class'])) { $build['#attributes']['class'][] = $context['class']; } // Maintain data-caption if it is there. if (isset($context['data-caption'])) { $build['#attributes']['data-caption'] = $context['data-caption']; } // @todo Should this hook get invoked if $build is an empty array? $this->moduleHandler()->alter(array("{$context['data-entity-type']}_embed", 'entity_embed'), $build, $entity, $context); $entity_output = $this->renderer()->render($build); return $entity_output; }
/** * Build all the arguments. */ protected function _buildArguments() { // Initially, we want to build sorts and fields. This can change, though, // if we get a summary view. if (empty($this->argument)) { return TRUE; } // build arguments. $position = -1; $substitutions = array(); $status = TRUE; // Get the title. $title = $this->display_handler->getOption('title'); // Iterate through each argument and process. foreach ($this->argument as $id => $arg) { $position++; $argument = $this->argument[$id]; if ($argument->broken()) { continue; } $argument->setRelationship(); $arg = isset($this->args[$position]) ? $this->args[$position] : NULL; $argument->position = $position; if (isset($arg) || $argument->hasDefaultArgument()) { if (!isset($arg)) { $arg = $argument->getDefaultArgument(); // make sure default args get put back. if (isset($arg)) { $this->args[$position] = $arg; } // remember that this argument was computed, not passed on the URL. $argument->is_default = TRUE; } // Set the argument, which will also validate that the argument can be set. if (!$argument->setArgument($arg)) { $status = $argument->validateFail($arg); break; } if ($argument->isException()) { $arg_title = $argument->exceptionTitle(); } else { $arg_title = $argument->getTitle(); $argument->query($this->display_handler->useGroupBy()); } // Add this argument's substitution $substitutions['%' . ($position + 1)] = $arg_title; $substitutions['!' . ($position + 1)] = strip_tags(Html::decodeEntities($arg)); // Test to see if we should use this argument's title if (!empty($argument->options['title_enable']) && !empty($argument->options['title'])) { $title = $argument->options['title']; } } else { // determine default condition and handle. $status = $argument->defaultAction(); break; } // Be safe with references and loops: unset($argument); } // set the title in the build info. if (!empty($title)) { $this->build_info['title'] = $title; } // Store the arguments for later use. $this->build_info['substitutions'] = $substitutions; return $status; }
/** * {@inheritdoc} */ public function cleanString($string, array $options = array()) { if (empty($this->cleanStringCache)) { // Generate and cache variables used in this method. $config = $this->configFactory->get('pathauto.settings'); $this->cleanStringCache = array('separator' => $config->get('separator'), 'strings' => array(), 'transliterate' => $config->get('transliterate'), 'punctuation' => array(), 'reduce_ascii' => (bool) $config->get('reduce_ascii'), 'ignore_words_regex' => FALSE, 'lowercase' => (bool) $config->get('case'), 'maxlength' => min($config->get('max_component_length'), $this->aliasStorageHelper->getAliasSchemaMaxLength())); // Generate and cache the punctuation replacements for strtr(). $punctuation = $this->getPunctuationCharacters(); foreach ($punctuation as $name => $details) { $action = $config->get('punctuation.' . $name); switch ($action) { case PathautoManagerInterface::PUNCTUATION_REMOVE: $cache['punctuation'][$details['value']] = ''; $this->cleanStringCache; case PathautoManagerInterface::PUNCTUATION_REPLACE: $this->cleanStringCache['punctuation'][$details['value']] = $this->cleanStringCache['separator']; break; case PathautoManagerInterface::PUNCTUATION_DO_NOTHING: // Literally do nothing. break; } } // Generate and cache the ignored words regular expression. $ignore_words = $config->get('ignore_words'); $ignore_words_regex = preg_replace(array('/^[,\\s]+|[,\\s]+$/', '/[,\\s]+/'), array('', '\\b|\\b'), $ignore_words); if ($ignore_words_regex) { $this->cleanStringCache['ignore_words_regex'] = '\\b' . $ignore_words_regex . '\\b'; if (function_exists('mb_eregi_replace')) { mb_regex_encoding('UTF-8'); $this->cleanStringCache['ignore_words_callback'] = 'mb_eregi_replace'; } else { $this->cleanStringCache['ignore_words_callback'] = 'preg_replace'; $this->cleanStringCache['ignore_words_regex'] = '/' . $this->cleanStringCache['ignore_words_regex'] . '/i'; } } } // Empty strings do not need any processing. if ($string === '' || $string === NULL) { return ''; } $langcode = NULL; if (!empty($options['language'])) { $langcode = $options['language']->getId(); } elseif (!empty($options['langcode'])) { $langcode = $options['langcode']; } // Check if the string has already been processed, and if so return the // cached result. if (isset($this->cleanStringCache['strings'][$langcode][(string) $string])) { return $this->cleanStringCache['strings'][$langcode][(string) $string]; } // Remove all HTML tags from the string. $output = Html::decodeEntities($string); $output = PlainTextOutput::renderFromHtml($output); // Optionally transliterate. if ($this->cleanStringCache['transliterate']) { // If the reduce strings to letters and numbers is enabled, don't bother // replacing unknown characters with a question mark. Use an empty string // instead. $output = $this->transliteration->transliterate($output, $langcode, $this->cleanStringCache['reduce_ascii'] ? '' : '?'); } // Replace or drop punctuation based on user settings. $output = strtr($output, $this->cleanStringCache['punctuation']); // Reduce strings to letters and numbers. if ($this->cleanStringCache['reduce_ascii']) { $output = preg_replace('/[^a-zA-Z0-9\\/]+/', $this->cleanStringCache['separator'], $output); } // Get rid of words that are on the ignore list. if ($this->cleanStringCache['ignore_words_regex']) { $words_removed = $this->cleanStringCache['ignore_words_callback']($this->cleanStringCache['ignore_words_regex'], '', $output); if (Unicode::strlen(trim($words_removed)) > 0) { $output = $words_removed; } } // Always replace whitespace with the separator. $output = preg_replace('/\\s+/', $this->cleanStringCache['separator'], $output); // Trim duplicates and remove trailing and leading separators. $output = $this->getCleanSeparators($this->getCleanSeparators($output, $this->cleanStringCache['separator'])); // Optionally convert to lower case. if ($this->cleanStringCache['lowercase']) { $output = Unicode::strtolower($output); } // Shorten to a logical place based on word boundaries. $output = Unicode::truncate($output, $this->cleanStringCache['maxlength'], TRUE); // Cache this result in the static array. $this->cleanStringCache['strings'][$langcode][(string) $string] = $output; return $output; }
/** * Checks for meta refresh tag and if found call drupalGet() recursively. * * This function looks for the http-equiv attribute to be set to "Refresh" and * is case-sensitive. * * @return * Either the new page content or FALSE. */ protected function checkForMetaRefresh() { if (strpos($this->getRawContent(), '<meta ') && $this->parse() && (!isset($this->maximumMetaRefreshCount) || $this->metaRefreshCount < $this->maximumMetaRefreshCount)) { $refresh = $this->xpath('//meta[@http-equiv="Refresh"]'); if (!empty($refresh)) { // Parse the content attribute of the meta tag for the format: // "[delay]: URL=[page_to_redirect_to]". if (preg_match('/\\d+;\\s*URL=(?<url>.*)/i', $refresh[0]['content'], $match)) { $this->metaRefreshCount++; return $this->drupalGet($this->getAbsoluteUrl(Html::decodeEntities($match['url']))); } } } return FALSE; }
protected function setDefaultSubjectIfNeeded(&$comment) { // Extract subject from comment body. if (!isset($comment['subject'])) { $comment['subject'] = ''; } if (trim($comment['subject']) == '') { if (isset($comment['comment_body'])) { // The body may be in any format, so: // 1) Filter it into HTML // 2) Strip out all HTML tags // 3) Convert entities back to plain-text. $comment_text = $comment['comment_body']['value']; $comment['subject'] = Unicode::truncate(trim(Html::decodeEntities(strip_tags($comment_text))), 29, TRUE, TRUE); } // Edge cases where the comment body is populated only by HTML tags will // require a default subject. if ($comment['subject'] == '') { $comment['subject'] = t('(No subject)'); } } }