123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- <?php
- /**
- * Copyright © Magento, Inc. All rights reserved.
- * See COPYING.txt for license details.
- */
- namespace Magento\Framework;
- /**
- * Magento escape methods
- *
- * @api
- * @since 100.0.2
- */
- class Escaper
- {
- /**
- * @var \Magento\Framework\ZendEscaper
- */
- private $escaper;
- /**
- * @var \Psr\Log\LoggerInterface
- */
- private $logger;
- /**
- * @var string[]
- */
- private $notAllowedTags = ['script', 'img', 'embed', 'iframe', 'video', 'source', 'object', 'audio'];
- /**
- * @var string[]
- */
- private $allowedAttributes = ['id', 'class', 'href', 'target', 'title', 'style'];
- /**
- * @var string
- */
- private static $xssFiltrationPattern =
- '/((javascript(\\\\x3a|:|%3A))|(data(\\\\x3a|:|%3A))|(vbscript:))|'
- . '((\\\\x6A\\\\x61\\\\x76\\\\x61\\\\x73\\\\x63\\\\x72\\\\x69\\\\x70\\\\x74(\\\\x3a|:|%3A))|'
- . '(\\\\x64\\\\x61\\\\x74\\\\x61(\\\\x3a|:|%3A)))/i';
- /**
- * @var string[]
- */
- private $escapeAsUrlAttributes = ['href'];
- /**
- * Escape string for HTML context.
- *
- * AllowedTags will not be escaped, except the following: script, img, embed,
- * iframe, video, source, object, audio
- *
- * @param string|array $data
- * @param array|null $allowedTags
- * @return string|array
- */
- public function escapeHtml($data, $allowedTags = null)
- {
- if (!is_array($data)) {
- $data = (string)$data;
- }
- if (is_array($data)) {
- $result = [];
- foreach ($data as $item) {
- $result[] = $this->escapeHtml($item, $allowedTags);
- }
- } elseif (strlen($data)) {
- if (is_array($allowedTags) && !empty($allowedTags)) {
- $allowedTags = $this->filterProhibitedTags($allowedTags);
- $wrapperElementId = uniqid();
- $domDocument = new \DOMDocument('1.0', 'UTF-8');
- set_error_handler(
- function ($errorNumber, $errorString) {
- throw new \Exception($errorString, $errorNumber);
- }
- );
- $data = $this->prepareUnescapedCharacters($data);
- $string = mb_convert_encoding($data, 'HTML-ENTITIES', 'UTF-8');
- try {
- $domDocument->loadHTML(
- '<html><body id="' . $wrapperElementId . '">' . $string . '</body></html>'
- );
- } catch (\Exception $e) {
- restore_error_handler();
- $this->getLogger()->critical($e);
- }
- restore_error_handler();
- $this->removeNotAllowedTags($domDocument, $allowedTags);
- $this->removeNotAllowedAttributes($domDocument);
- $this->escapeText($domDocument);
- $this->escapeAttributeValues($domDocument);
- $result = mb_convert_encoding($domDocument->saveHTML(), 'UTF-8', 'HTML-ENTITIES');
- preg_match('/<body id="' . $wrapperElementId . '">(.+)<\/body><\/html>$/si', $result, $matches);
- return !empty($matches) ? $matches[1] : '';
- } else {
- $result = htmlspecialchars($data, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', false);
- }
- } else {
- $result = $data;
- }
- return $result;
- }
- /**
- * Used to replace characters, that mb_convert_encoding will not process
- *
- * @param string $data
- * @return string|null
- */
- private function prepareUnescapedCharacters(string $data): ?string
- {
- $patterns = ['/\&/u'];
- $replacements = ['&'];
- return \preg_replace($patterns, $replacements, $data);
- }
- /**
- * Remove not allowed tags
- *
- * @param \DOMDocument $domDocument
- * @param string[] $allowedTags
- * @return void
- */
- private function removeNotAllowedTags(\DOMDocument $domDocument, array $allowedTags)
- {
- $xpath = new \DOMXPath($domDocument);
- $nodes = $xpath->query(
- '//node()[name() != \''
- . implode('\' and name() != \'', array_merge($allowedTags, ['html', 'body']))
- . '\']'
- );
- foreach ($nodes as $node) {
- if ($node->nodeName != '#text' && $node->nodeName != '#comment') {
- $node->parentNode->replaceChild($domDocument->createTextNode($node->textContent), $node);
- }
- }
- }
- /**
- * Remove not allowed attributes
- *
- * @param \DOMDocument $domDocument
- * @return void
- */
- private function removeNotAllowedAttributes(\DOMDocument $domDocument)
- {
- $xpath = new \DOMXPath($domDocument);
- $nodes = $xpath->query(
- '//@*[name() != \'' . implode('\' and name() != \'', $this->allowedAttributes) . '\']'
- );
- foreach ($nodes as $node) {
- $node->parentNode->removeAttribute($node->nodeName);
- }
- }
- /**
- * Escape text
- *
- * @param \DOMDocument $domDocument
- * @return void
- */
- private function escapeText(\DOMDocument $domDocument)
- {
- $xpath = new \DOMXPath($domDocument);
- $nodes = $xpath->query('//text()');
- foreach ($nodes as $node) {
- $node->textContent = $this->escapeHtml($node->textContent);
- }
- }
- /**
- * Escape attribute values
- *
- * @param \DOMDocument $domDocument
- * @return void
- */
- private function escapeAttributeValues(\DOMDocument $domDocument)
- {
- $xpath = new \DOMXPath($domDocument);
- $nodes = $xpath->query('//@*');
- foreach ($nodes as $node) {
- $value = $this->escapeAttributeValue(
- $node->nodeName,
- $node->parentNode->getAttribute($node->nodeName)
- );
- $node->parentNode->setAttribute($node->nodeName, $value);
- }
- }
- /**
- * Escape attribute value using escapeHtml or escapeUrl
- *
- * @param string $name
- * @param string $value
- * @return string
- */
- private function escapeAttributeValue($name, $value)
- {
- return in_array($name, $this->escapeAsUrlAttributes) ? $this->escapeUrl($value) : $this->escapeHtml($value);
- }
- /**
- * Escape a string for the HTML attribute context
- *
- * @param string $string
- * @param boolean $escapeSingleQuote
- * @return string
- * @since 101.0.0
- */
- public function escapeHtmlAttr($string, $escapeSingleQuote = true)
- {
- if ($escapeSingleQuote) {
- return $this->getEscaper()->escapeHtmlAttr((string) $string);
- }
- return htmlspecialchars((string)$string, ENT_COMPAT, 'UTF-8', false);
- }
- /**
- * Escape URL
- *
- * @param string $string
- * @return string
- */
- public function escapeUrl($string)
- {
- return $this->escapeHtml($this->escapeXssInUrl($string));
- }
- /**
- * Encode URL
- *
- * @param string $string
- * @return string
- * @since 101.0.0
- */
- public function encodeUrlParam($string)
- {
- return $this->getEscaper()->escapeUrl($string);
- }
- /**
- * Escape string for the JavaScript context
- *
- * @param string $string
- * @return string
- * @since 101.0.0
- */
- public function escapeJs($string)
- {
- if ($string === '' || ctype_digit($string)) {
- return $string;
- }
- return preg_replace_callback(
- '/[^a-z0-9,\._]/iSu',
- function ($matches) {
- $chr = $matches[0];
- if (strlen($chr) != 1) {
- $chr = mb_convert_encoding($chr, 'UTF-16BE', 'UTF-8');
- $chr = ($chr === false) ? '' : $chr;
- }
- return sprintf('\\u%04s', strtoupper(bin2hex($chr)));
- },
- $string
- );
- }
- /**
- * Escape string for the CSS context
- *
- * @param string $string
- * @return string
- * @since 101.0.0
- */
- public function escapeCss($string)
- {
- return $this->getEscaper()->escapeCss($string);
- }
- /**
- * Escape quotes in java script
- *
- * @param string|array $data
- * @param string $quote
- * @return string|array
- * @deprecated 101.0.0
- */
- public function escapeJsQuote($data, $quote = '\'')
- {
- if (is_array($data)) {
- $result = [];
- foreach ($data as $item) {
- $result[] = $this->escapeJsQuote($item, $quote);
- }
- } else {
- $result = str_replace($quote, '\\' . $quote, (string)$data);
- }
- return $result;
- }
- /**
- * Escape xss in urls
- *
- * @param string $data
- * @return string
- * @deprecated 101.0.0
- */
- public function escapeXssInUrl($data)
- {
- return htmlspecialchars(
- $this->escapeScriptIdentifiers((string)$data),
- ENT_COMPAT | ENT_HTML5 | ENT_HTML401,
- 'UTF-8',
- false
- );
- }
- /**
- * Remove `javascript:`, `vbscript:`, `data:` words from the string.
- *
- * @param string $data
- * @return string
- */
- private function escapeScriptIdentifiers(string $data): string
- {
- $filteredData = preg_replace(self::$xssFiltrationPattern, ':', $data) ?: '';
- if (preg_match(self::$xssFiltrationPattern, $filteredData)) {
- $filteredData = $this->escapeScriptIdentifiers($filteredData);
- }
- return $filteredData;
- }
- /**
- * Escape quotes inside html attributes
- *
- * Use $addSlashes = false for escaping js that inside html attribute (onClick, onSubmit etc)
- *
- * @param string $data
- * @param bool $addSlashes
- * @return string
- * @deprecated 101.0.0
- */
- public function escapeQuote($data, $addSlashes = false)
- {
- if ($addSlashes === true) {
- $data = addslashes($data);
- }
- return htmlspecialchars($data, ENT_QUOTES, null, false);
- }
- /**
- * Get escaper
- *
- * @return \Magento\Framework\ZendEscaper
- * @deprecated 101.0.0
- */
- private function getEscaper()
- {
- if ($this->escaper == null) {
- $this->escaper = \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Framework\ZendEscaper::class);
- }
- return $this->escaper;
- }
- /**
- * Get logger
- *
- * @return \Psr\Log\LoggerInterface
- * @deprecated 101.0.0
- */
- private function getLogger()
- {
- if ($this->logger == null) {
- $this->logger = \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Psr\Log\LoggerInterface::class);
- }
- return $this->logger;
- }
- /**
- * Filter prohibited tags.
- *
- * @param string[] $allowedTags
- * @return string[]
- */
- private function filterProhibitedTags(array $allowedTags): array
- {
- $notAllowedTags = array_intersect(
- array_map('strtolower', $allowedTags),
- $this->notAllowedTags
- );
- if (!empty($notAllowedTags)) {
- $this->getLogger()->critical(
- 'The following tag(s) are not allowed: ' . implode(', ', $notAllowedTags)
- );
- $allowedTags = array_diff($allowedTags, $this->notAllowedTags);
- }
- return $allowedTags;
- }
- }
|