vendor/heimrichhannot/contao-request-bundle/src/Component/HttpFoundation/Request.php line 43

Open in your IDE?
  1. <?php
  2. /*
  3.  * Copyright (c) 2021 Heimrich & Hannot GmbH
  4.  *
  5.  * @license LGPL-3.0-or-later
  6.  */
  7. namespace HeimrichHannot\RequestBundle\Component\HttpFoundation;
  8. use Contao\CoreBundle\Framework\ContaoFrameworkInterface;
  9. use Contao\CoreBundle\Routing\ScopeMatcher;
  10. use Contao\Input;
  11. use Contao\StringUtil;
  12. use Contao\Validator;
  13. use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
  14. use Symfony\Component\HttpFoundation\ParameterBag;
  15. use Symfony\Component\HttpFoundation\RequestStack;
  16. use Wa72\HtmlPageDom\HtmlPageCrawler;
  17. class Request extends \Symfony\Component\HttpFoundation\Request
  18. {
  19.     /**
  20.      * Query string parameters ($_GET).
  21.      *
  22.      * @var QueryParameterBag
  23.      */
  24.     public $query;
  25.     /**
  26.      * @var ContaoFrameworkInterface
  27.      */
  28.     protected $framework;
  29.     /**
  30.      * @var ScopeMatcher
  31.      */
  32.     protected $scopeMatcher;
  33.     /**
  34.      * Request constructor.
  35.      */
  36.     public function __construct(ContaoFrameworkInterface $frameworkRequestStack $requestStackScopeMatcher $scopeMatcher)
  37.     {
  38.         $this->framework $framework;
  39.         $this->scopeMatcher $scopeMatcher;
  40.         $request $requestStack->getCurrentRequest();
  41.         if (null === $request) {
  42.             parent::__construct([], [], [], [], [], [], null);
  43.         } else {
  44.             parent::__construct(
  45.                 $this->checkCurrentRequest($request->query),
  46.                 $this->checkCurrentRequest($request->request),
  47.                 $this->checkCurrentRequest($request->attributes),
  48.                 $this->checkCurrentRequest($request->cookies),
  49.                 $this->checkCurrentRequest($request->files),
  50.                 $this->checkCurrentRequest($request->server),
  51.                 $request->getContent()
  52.             );
  53.         }
  54.         // As long as contao adds unused parameters to $_GET and $_POST Globals inside \Contao\Input, we have to add them inside custom ParameterBag classes
  55.         $this->query = new QueryParameterBag($request && $request->query $request->query->all() : []);
  56.     }
  57.     /**
  58.      * For test purposes use \Symfony\Component\HttpFoundation\Request::create() for dummy data.
  59.      *
  60.      * @return \Symfony\Component\HttpFoundation\Request
  61.      */
  62.     public function setRequest(\Symfony\Component\HttpFoundation\Request $request)
  63.     {
  64.         $this->initialize($request->query->all(), $request->request->all(), $request->attributes->all(), $request->cookies->all(), $request->files->all(), $request->server->all(), $request->getContent());
  65.         // As long as contao adds unused parameters to $_GET and $_POST Globals inside \Contao\Input, we have to add them inside custom ParameterBag classes
  66.         $this->query = new QueryParameterBag($request->query->all());
  67.         return $this;
  68.     }
  69.     /**
  70.      * Shorthand setter for query arguments ($_GET).
  71.      *
  72.      * @param string $key   The requested field
  73.      * @param mixed  $value The input value
  74.      */
  75.     public function setGet(string $key$value)
  76.     {
  77.         // Convert special characters (see #7829)
  78.         $key str_replace([' ''.''['], '_'$key);
  79.         $key Input::cleanKey($key);
  80.         if (null === $value) {
  81.             $this->query->remove($key);
  82.         } else {
  83.             $this->query->set($key$value);
  84.         }
  85.     }
  86.     /**
  87.      * Shorthand setter for request arguments ($_POST).
  88.      *
  89.      * @param string $key   The requested field
  90.      * @param mixed  $value The input value
  91.      */
  92.     public function setPost(string $key$value)
  93.     {
  94.         $key Input::cleanKey($key);
  95.         if (null === $value) {
  96.             $this->request->remove($key);
  97.         } else {
  98.             $this->request->set($key$value);
  99.         }
  100.     }
  101.     /**
  102.      * Shorthand getter for query arguments ($_GET).
  103.      *
  104.      * @param string $postKey        The requested field
  105.      * @param bool   $decodeEntities If true, all entities will be decoded
  106.      * @param bool   $tidy           If true, varValue is tidied up
  107.      *
  108.      * @return mixed If no $strkey is defined, return all cleaned query parameters, otherwise the cleaned requested query value
  109.      */
  110.     public function getGet(string $postKey nullbool $decodeEntities falsebool $tidy false)
  111.     {
  112.         if (null === $postKey) {
  113.             $postValues $this->query;
  114.             if ($decodeEntities) {
  115.                 foreach ($postValues as $key => &$value) {
  116.                     $value $this->clean($value$decodeEntitiestrue$tidy);
  117.                 }
  118.             }
  119.             return $postValues;
  120.         }
  121.         return $this->clean($this->query->get($postKey), $decodeEntitiestrue$tidy);
  122.     }
  123.     /**
  124.      * XSS clean, decodeEntities, tidy/strip tags, encode special characters and encode inserttags and return save, cleaned value(s).
  125.      *
  126.      * @param mixed $value            The input value
  127.      * @param bool  $decodeEntities   If true, all entities will be decoded
  128.      * @param bool  $encodeInsertTags If true, encode the opening and closing delimiters of insert tags
  129.      * @param bool  $tidy             If true, varValue is tidied up
  130.      * @param bool  $strictMode       If true, the xss cleaner removes also JavaScript event handlers
  131.      *
  132.      * @return mixed The cleaned value
  133.      */
  134.     public function clean($valuebool $decodeEntities falsebool $encodeInsertTags truebool $tidy truebool $strictMode true)
  135.     {
  136.         // do not clean, otherwise empty string will be returned, not null
  137.         if (null === $value) {
  138.             return $value;
  139.         }
  140.         if (\is_array($value)) {
  141.             foreach ($value as $i => $childValue) {
  142.                 $value[$i] = $this->clean($childValue$decodeEntities$encodeInsertTags$tidy$strictMode);
  143.             }
  144.             return $value;
  145.         }
  146.         // do not handle binary uuid
  147.         if (Validator::isUuid($value)) {
  148.             return $value;
  149.         }
  150.         $value $this->xssClean($value$strictMode);
  151.         if ($tidy) {
  152.             $value $this->tidy($value);
  153.         } else {
  154.             // decodeEntities for tidy is more complex, because non allowed tags should be displayed as readable text, not as html entity
  155.             $value Input::decodeEntities($value);
  156.         }
  157.         // do not encodeSpecialChars when tidy did run, otherwise non allowed tags will be encoded twice
  158.         if (!$decodeEntities && !$tidy) {
  159.             $value Input::encodeSpecialChars($value);
  160.         }
  161.         if ($encodeInsertTags) {
  162.             $value Input::encodeInsertTags($value);
  163.         }
  164.         return $value;
  165.     }
  166.     /**
  167.      * XSS clean, decodeEntities, tidy/strip tags, encode special characters and encode inserttags and return save, cleaned value(s).
  168.      *
  169.      * @param mixed  $value            The input value
  170.      * @param bool   $decodeEntities   If true, all entities will be decoded
  171.      * @param bool   $encodeInsertTags If true, encode the opening and closing delimiters of insert tags
  172.      * @param string $allowedTags      List of allowed html tags
  173.      * @param bool   $tidy             If true, varValue is tidied up
  174.      * @param bool   $strictMode       If true, the xss cleaner removes also JavaScript event handlers
  175.      *
  176.      * @return mixed The cleaned value
  177.      */
  178.     public function cleanHtml($valuebool $decodeEntities falsebool $encodeInsertTags truestring $allowedTags ''bool $tidy truebool $strictMode true)
  179.     {
  180.         // do not clean, otherwise empty string will be returned, not null
  181.         if (null === $value) {
  182.             return $value;
  183.         }
  184.         if (\is_array($value)) {
  185.             foreach ($value as $i => $childValue) {
  186.                 $value[$i] = $this->cleanHtml($childValue$decodeEntities$encodeInsertTags$allowedTags$tidy$strictMode);
  187.             }
  188.             return $value;
  189.         }
  190.         // do not handle binary uuid
  191.         if (Validator::isUuid($value)) {
  192.             return $value;
  193.         }
  194.         $value $this->xssClean($value$strictMode);
  195.         if ($tidy) {
  196.             $value $this->tidy($value$allowedTags$decodeEntities);
  197.         } else {
  198.             // decodeEntities for tidy is more complex, because non allowed tags should be displayed as readable text, not as html entity
  199.             $value Input::decodeEntities($value);
  200.         }
  201.         // do not encodeSpecialChars when tidy did run, otherwise non allowed tags will be encoded twice
  202.         if (!$decodeEntities && !$tidy) {
  203.             $value Input::encodeSpecialChars($value);
  204.         }
  205.         if ($encodeInsertTags) {
  206.             $value Input::encodeInsertTags($value);
  207.         }
  208.         return $value;
  209.     }
  210.     /**
  211.      * XSS clean, preserve basic entities encode inserttags and return raw unsafe but filtered value.
  212.      *
  213.      * @param mixed $value            The input value
  214.      * @param bool  $encodeInsertTags If true, encode the opening and closing delimiters of insert tags
  215.      * @param bool  $tidy             If true, varValue is tidied up
  216.      * @param bool  $strictMode       If true, the xss cleaner removes also JavaScript event handlers
  217.      *
  218.      * @return mixed The cleaned value
  219.      */
  220.     public function cleanRaw($valuebool $encodeInsertTags truebool $tidy falsebool $strictMode false)
  221.     {
  222.         // do not clean, otherwise empty string will be returned, not null
  223.         if (null === $value) {
  224.             return $value;
  225.         }
  226.         if (\is_array($value)) {
  227.             foreach ($value as $i => $childValue) {
  228.                 $value[$i] = $this->cleanRaw($childValue$encodeInsertTags$tidy$strictMode);
  229.             }
  230.             return $value;
  231.         }
  232.         // do not handle binary uuid
  233.         if (Validator::isUuid($value)) {
  234.             return $value;
  235.         }
  236.         $value $this->xssClean($value$strictMode);
  237.         if ($tidy) {
  238.             $value $this->tidy($value);
  239.         }
  240.         $value Input::preserveBasicEntities($value);
  241.         if ($encodeInsertTags) {
  242.             $value Input::encodeInsertTags($value);
  243.         }
  244.         return $value;
  245.     }
  246.     /**
  247.      * Returns true if the get parameter is defined.
  248.      *
  249.      * @param string $key The key
  250.      *
  251.      * @return bool true if the parameter exists, false otherwise
  252.      */
  253.     public function hasGet(string $key)
  254.     {
  255.         return $this->query->has($key);
  256.     }
  257.     /**
  258.      * Shorthand getter for request arguments ($_POST).
  259.      *
  260.      * @param string $key            The requested field
  261.      * @param bool   $decodeEntities If true, all entities will be decoded
  262.      * @param bool   $tidy           If true, varValue is tidied up
  263.      * @param bool   $strictMode     If true, the xss cleaner removes also JavaScript event handlers
  264.      *
  265.      * @return mixed|null If no $strKey is defined, return all cleaned query parameters, otherwise the cleaned requested query value
  266.      */
  267.     public function getPost(string $keybool $decodeEntities falsebool $tidy truebool $strictMode true)
  268.     {
  269.         if (!$this->request->has($key)) {
  270.             return null;
  271.         }
  272.         return $this->clean($this->request->get($key), $decodeEntities$this->scopeMatcher->isFrontendRequest($this), $tidy$strictMode);
  273.     }
  274.     /**
  275.      * Shorthand getter for request arguments ($_POST).
  276.      *
  277.      * @param string $strKey         The requested field
  278.      * @param bool   $decodeEntities If true, all entities will be decoded
  279.      * @param bool   $tidy           If true, varValue is tidied up
  280.      * @param bool   $strictMode     If true, the xss cleaner removes also JavaScript event handlers
  281.      *
  282.      * @return array If no $strKey is defined, return all cleaned query parameters, otherwise the cleaned requested query value
  283.      */
  284.     public function getAllPost(bool $decodeEntities falsebool $tidy truebool $strictMode true): array
  285.     {
  286.         $arrValues $this->request->all();
  287.         if (empty($arrValues)) {
  288.             return $arrValues;
  289.         }
  290.         foreach ($arrValues as $key => &$varValue) {
  291.             $varValue $this->clean($varValue$decodeEntities$this->scopeMatcher->isFrontendRequest($this), $tidy$strictMode);
  292.         }
  293.         return $arrValues;
  294.     }
  295.     /**
  296.      * Shorthand getter for request arguments ($_POST) preserving allowed HTML tags.
  297.      *
  298.      * @param string $key            The requested field
  299.      * @param bool   $decodeEntities If true, all entities will be decoded
  300.      * @param string $allowedTags    List of allowed html tags
  301.      * @param bool   $tidy           If true, varValue is tidied up
  302.      * @param bool   $strictMode     If true, the xss cleaner removes also JavaScript event handlers
  303.      *
  304.      * @return mixed|null If no $strKey is defined, return all cleaned query parameters, otherwise the cleaned requested query value
  305.      */
  306.     public function getPostHtml(string $keybool $decodeEntities falsestring $allowedTags ''bool $tidy truebool $strictMode true)
  307.     {
  308.         if (!$this->request->has($key)) {
  309.             return null;
  310.         }
  311.         return $this->cleanHtml($this->request->get($key), $decodeEntities$this->scopeMatcher->isFrontendRequest($this), $allowedTags$tidy$strictMode);
  312.     }
  313.     /**
  314.      * Shorthand getter for request arguments ($_POST) preserving allowed HTML tags.
  315.      *
  316.      * @param string $strKey         The requested field
  317.      * @param bool   $decodeEntities If true, all entities will be decoded
  318.      * @param string $allowedTags    List of allowed html tags
  319.      * @param bool   $tidy           If true, varValue is tidied up
  320.      * @param bool   $strictMode     If true, the xss cleaner removes also JavaScript event handlers
  321.      */
  322.     public function getAllPostHtml(bool $decodeEntities falsestring $allowedTags ''bool $tidy truebool $strictMode true): array
  323.     {
  324.         $arrValues $this->request->all();
  325.         if (empty($arrValues)) {
  326.             return $arrValues;
  327.         }
  328.         foreach ($arrValues as $key => &$varValue) {
  329.             $varValue $this->cleanHtml($varValue$decodeEntities$this->scopeMatcher->isFrontendRequest($this), $allowedTags$tidy$strictMode);
  330.         }
  331.         return $arrValues;
  332.     }
  333.     /**
  334.      * Shorthand getter for request arguments ($_POST), returning raw, unsafe but filtered values.
  335.      *
  336.      * @param string $key        The requested field
  337.      * @param bool   $tidy       If true, varValue is tidied up
  338.      * @param bool   $strictMode If true, the xss cleaner removes also JavaScript event handlers
  339.      *
  340.      * @return mixed|null If no $strkey is defined, return all cleaned query parameters, otherwise the cleaned requested query value
  341.      */
  342.     public function getPostRaw(string $keybool $tidy falsebool $strictMode false)
  343.     {
  344.         if (!$this->request->has($key)) {
  345.             return null;
  346.         }
  347.         return $this->cleanRaw($this->request->get($key), $this->scopeMatcher->isFrontendRequest($this), $tidy$strictMode);
  348.     }
  349.     /**
  350.      * Shorthand getter for request arguments ($_POST), returning raw, unsafe but filtered values.
  351.      *
  352.      * @param string $strKey     The requested field
  353.      * @param bool   $tidy       If true, varValue is tidied up
  354.      * @param bool   $strictMode If true, the xss cleaner removes also JavaScript event handlers
  355.      */
  356.     public function getAllPostRaw(bool $tidy falsebool $strictMode false): array
  357.     {
  358.         $arrValues $this->request->all();
  359.         if (empty($arrValues)) {
  360.             return $arrValues;
  361.         }
  362.         foreach ($arrValues as $key => &$varValue) {
  363.             $varValue $this->cleanRaw($varValue$this->scopeMatcher->isFrontendRequest($this), $tidy$strictMode);
  364.         }
  365.         return $arrValues;
  366.     }
  367.     /**
  368.      * Clean a value and try to prevent XSS attacks.
  369.      *
  370.      * @param mixed $varValue   A string or array
  371.      * @param bool  $strictMode If true, the function removes also JavaScript event handlers
  372.      *
  373.      * @return mixed The cleaned string or array
  374.      */
  375.     public function xssClean($varValuebool $strictMode false)
  376.     {
  377.         if (\is_array($varValue)) {
  378.             foreach ($varValue as $key => $value) {
  379.                 $varValue[$key] = $this->xssClean($value$strictMode);
  380.             }
  381.             return $varValue;
  382.         }
  383.         // do not xss clean binary uuids
  384.         if (Validator::isBinaryUuid($varValue)) {
  385.             return $varValue;
  386.         }
  387.         // Fix issue StringUtils::decodeEntites() returning empty string when value is 0 in some contao 4.9 versions
  388.         if ('0' !== $varValue && !== $varValue) {
  389.             $varValue StringUtil::decodeEntities($varValue);
  390.         }
  391.         $varValue preg_replace('/(&#[A-Za-z0-9]+);?/i''$1;'$varValue);
  392.         // fix: "><script>alert('xss')</script> or '></SCRIPT>">'><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>
  393.         $varValue preg_replace('/(?<!\w)(?>["|\']>)+(<[^\/^>]+>.*)/''$1'$varValue);
  394.         $varValue Input::xssClean($varValue$strictMode);
  395.         return $varValue;
  396.     }
  397.     /**
  398.      * Tidy an value.
  399.      *
  400.      * @param string $varValue       Input value
  401.      * @param string $allowedTags    Allowed tags as string `<p><span>`
  402.      * @param bool   $decodeEntities If true, all entities will be decoded
  403.      *
  404.      * @return string The tidied string
  405.      */
  406.     public function tidy($varValuestring $allowedTags ''bool $decodeEntities false): string
  407.     {
  408.         if (!$varValue) {
  409.             return $varValue;
  410.         }
  411.         // do not tidy non-xss critical characters for performance
  412.         if (!preg_match('#"|\'|<|>|\(|\)#'StringUtil::decodeEntities($varValue))) {
  413.             return $varValue;
  414.         }
  415.         // remove illegal white spaces after closing tag slash <br / >
  416.         $varValue preg_replace('@\/(\s+)>@''/>'$varValue);
  417.         // Encode opening tag arrow brackets
  418.         $varValue preg_replace_callback('/<(?(?=!--)!--[\s\S]*--|(?(?=\?)\?[\s\S]*\?|(?(?=\/)\/[^.\-\d][^\/\]\'"[!#$%&()*+,;<=>?@^`{|}~ ]*|[^.\-\d][^\/\]\'"[!#$%&()*+,;<=>?@^`{|}~ ]*(?:\s[^.\-\d][^\/\]\'"[!#$%&()*+,;<=>?@^`{|}~ ]*(?:=(?:"[^"]*"|\'[^\']*\'|[^\'"<\s]*))?)*)\s?\/?))>/', function ($matches) {
  419.             return substr_replace($matches[0], '&lt;'01);
  420.         }, $varValue);
  421.         // Encode less than signs that are no tags with [lt]
  422.         $varValue str_replace('<''[lt]'$varValue);
  423.         // After we saved less than signs with [lt] revert &lt; sign to <
  424.         $varValue StringUtil::decodeEntities($varValue);
  425.         // Restore HTML comments
  426.         $varValue str_replace(['&lt;!--''&lt;!['], ['<!--''<!['], $varValue);
  427.         // Recheck for encoded null bytes
  428.         while (false !== strpos($varValue'\\0')) {
  429.             $varValue str_replace('\\0'''$varValue);
  430.         }
  431.         $objCrawler = new HtmlPageCrawler($varValue);
  432.         if (!$objCrawler->isHtmlDocument()) {
  433.             $objCrawler = new HtmlPageCrawler('<div id="tidyWrapperx123x123xawec3">'.$varValue.'</div>');
  434.         }
  435.         $arrAllowedTags explode('<'str_replace('>'''$allowedTags));
  436.         $arrAllowedTags array_filter($arrAllowedTags);
  437.         try {
  438.             if (!empty($arrAllowedTags)) {
  439.                 $objCrawler->filter('*')->each(function ($node$i) use ($arrAllowedTags) {
  440.                     /** @var $node HtmlPageCrawler */
  441.                     // skip wrapper
  442.                     if ('tidyWrapperx123x123xawec3' === $node->getAttribute('id')) {
  443.                         return $node;
  444.                     }
  445.                     if (!\in_array($node->getNode(0)->tagName$arrAllowedTagstrue)) {
  446.                         $strHTML $node->saveHTML();
  447.                         $strHTML str_replace(['<''>'], ['[[xlt]]''[[xgt]]'], $strHTML);
  448.                         // remove unwanted tags and return the element text
  449.                         return $node->replaceWith($strHTML);
  450.                     }
  451.                     return $node;
  452.                 });
  453.             }
  454.             // unwrap div#tidyWrapper and set value to its innerHTML
  455.             if (!$objCrawler->isHtmlDocument()) {
  456.                 $varValue $objCrawler->filter('div#tidyWrapperx123x123xawec3')->getInnerHtml();
  457.             } else {
  458.                 $varValue $objCrawler->saveHTML();
  459.             }
  460.             // HTML documents or fragments, Crawler first converts all non-ASCII characters to entities (see: https://github.com/wasinger/htmlpagedom/issues/5)
  461.             $varValue StringUtil::decodeEntities($varValue);
  462.             // trim last [nbsp] occurance
  463.             $varValue preg_replace('@(\[nbsp\])+@'''$varValue);
  464.         } catch (SyntaxErrorException $e) {
  465.         }
  466.         $varValue $this->restoreBasicEntities($varValue$decodeEntities);
  467.         if (!$decodeEntities) {
  468.             $varValue Input::encodeSpecialChars($varValue);
  469.         }
  470.         // encode unwanted tag opening and closing brakets
  471.         $arrSearch = ['[[xlt]]''[[xgt]]'];
  472.         $arrReplace = ['&#60;''&#62;'];
  473.         $varValue str_replace($arrSearch$arrReplace$varValue);
  474.         return $varValue;
  475.     }
  476.     /**
  477.      * Restore basic entities.
  478.      *
  479.      * @param string $buffer         The string with the tags to be replaced
  480.      * @param bool   $decodeEntities If true, all entities will be decoded
  481.      *
  482.      * @return string The string with the original entities
  483.      */
  484.     public function restoreBasicEntities(string $bufferbool $decodeEntities false): string
  485.     {
  486.         $buffer str_replace(['[&]''[&amp;]''[lt]''[gt]''[nbsp]''[-]'], ['&amp;''&amp;''&lt;''&gt;''&nbsp;''&shy;'], $buffer);
  487.         if ($decodeEntities) {
  488.             $buffer StringUtil::decodeEntities($buffer);
  489.         }
  490.         return $buffer;
  491.     }
  492.     /**
  493.      * Returns true if the post parameter is defined.
  494.      *
  495.      * @param string $key The key
  496.      *
  497.      * @return bool true if the parameter exists, false otherwise
  498.      */
  499.     public function hasPost(string $key): bool
  500.     {
  501.         return $this->request->has($key);
  502.     }
  503.     /**
  504.      * @param $request
  505.      *
  506.      * @return array
  507.      */
  508.     protected function checkCurrentRequest($request)
  509.     {
  510.         if (null === $request || !$request instanceof ParameterBag) {
  511.             return [];
  512.         }
  513.         $parameters $request->all();
  514.         foreach ($parameters as $i => $parameter) {
  515.             if (null === $parameter) {
  516.                 unset($parameters[$i]);
  517.             }
  518.         }
  519.         return $parameters;
  520.     }
  521. }