SECURITY: rate-limit and prevent blocked users from changing email
[lhc/web/wiklou.git] / includes / parser / StripState.php
index 298aad3..855ce1d 100644 (file)
  * @ingroup Parser
  */
 class StripState {
-       protected $prefix;
        protected $data;
        protected $regex;
 
-       protected $tempType, $tempMergePrefix;
+       protected $parser;
+
        protected $circularRefGuard;
-       protected $recursionLevel = 0;
+       protected $depth = 0;
+       protected $highestDepth = 0;
+       protected $expandSize = 0;
 
-       const UNSTRIP_RECURSION_LIMIT = 20;
+       protected $depthLimit = 20;
+       protected $sizeLimit = 5000000;
 
        /**
-        * @param string|null $prefix
-        * @since 1.26 The prefix argument should be omitted, as the strip marker
-        *  prefix string is now a constant.
+        * @param Parser|null $parser
+        * @param array $options
         */
-       public function __construct( $prefix = null ) {
-               if ( $prefix !== null ) {
-                       wfDeprecated( __METHOD__ . ' with called with $prefix argument' .
-                               ' (call with no arguments instead)', '1.26' );
-               }
+       public function __construct( Parser $parser = null, $options = [] ) {
                $this->data = [
                        'nowiki' => [],
                        'general' => []
                ];
                $this->regex = '/' . Parser::MARKER_PREFIX . "([^\x7f<>&'\"]+)" . Parser::MARKER_SUFFIX . '/';
                $this->circularRefGuard = [];
+               $this->parser = $parser;
+
+               if ( isset( $options['depthLimit'] ) ) {
+                       $this->depthLimit = $options['depthLimit'];
+               }
+               if ( isset( $options['sizeLimit'] ) ) {
+                       $this->sizeLimit = $options['sizeLimit'];
+               }
        }
 
        /**
@@ -122,56 +128,109 @@ class StripState {
                        return $text;
                }
 
-               $oldType = $this->tempType;
-               $this->tempType = $type;
-               $text = preg_replace_callback( $this->regex, [ $this, 'unstripCallback' ], $text );
-               $this->tempType = $oldType;
+               $callback = function ( $m ) use ( $type ) {
+                       $marker = $m[1];
+                       if ( isset( $this->data[$type][$marker] ) ) {
+                               if ( isset( $this->circularRefGuard[$marker] ) ) {
+                                       return $this->getWarning( 'parser-unstrip-loop-warning' );
+                               }
+
+                               if ( $this->depth > $this->highestDepth ) {
+                                       $this->highestDepth = $this->depth;
+                               }
+                               if ( $this->depth >= $this->depthLimit ) {
+                                       return $this->getLimitationWarning( 'unstrip-depth', $this->depthLimit );
+                               }
+
+                               $value = $this->data[$type][$marker];
+                               if ( $value instanceof Closure ) {
+                                       $value = $value();
+                               }
+
+                               $this->expandSize += strlen( $value );
+                               if ( $this->expandSize > $this->sizeLimit ) {
+                                       return $this->getLimitationWarning( 'unstrip-size', $this->sizeLimit );
+                               }
+
+                               $this->circularRefGuard[$marker] = true;
+                               $this->depth++;
+                               $ret = $this->unstripType( $type, $value );
+                               $this->depth--;
+                               unset( $this->circularRefGuard[$marker] );
+
+                               return $ret;
+                       } else {
+                               return $m[0];
+                       }
+               };
+
+               $text = preg_replace_callback( $this->regex, $callback, $text );
                return $text;
        }
 
        /**
-        * @param array $m
-        * @return array
+        * Get warning HTML and register a limitation warning with the parser
+        *
+        * @param string $type
+        * @param int $max
+        * @return string
         */
-       protected function unstripCallback( $m ) {
-               $marker = $m[1];
-               if ( isset( $this->data[$this->tempType][$marker] ) ) {
-                       if ( isset( $this->circularRefGuard[$marker] ) ) {
-                               return '<span class="error">'
-                                       . wfMessage( 'parser-unstrip-loop-warning' )->inContentLanguage()->text()
-                                       . '</span>';
-                       }
-                       if ( $this->recursionLevel >= self::UNSTRIP_RECURSION_LIMIT ) {
-                               return '<span class="error">' .
-                                       wfMessage( 'parser-unstrip-recursion-limit' )
-                                               ->numParams( self::UNSTRIP_RECURSION_LIMIT )->inContentLanguage()->text() .
-                                       '</span>';
-                       }
-                       $this->circularRefGuard[$marker] = true;
-                       $this->recursionLevel++;
-                       $value = $this->data[$this->tempType][$marker];
-                       if ( $value instanceof Closure ) {
-                               $value = $value();
-                       }
-                       $ret = $this->unstripType( $this->tempType, $value );
-                       $this->recursionLevel--;
-                       unset( $this->circularRefGuard[$marker] );
-                       return $ret;
-               } else {
-                       return $m[0];
+       private function getLimitationWarning( $type, $max = '' ) {
+               if ( $this->parser ) {
+                       $this->parser->limitationWarn( $type, $max );
                }
+               return $this->getWarning( "$type-warning", $max );
+       }
+
+       /**
+        * Get warning HTML
+        *
+        * @param string $message
+        * @param int $max
+        * @return string
+        */
+       private function getWarning( $message, $max = '' ) {
+               return '<span class="error">' .
+                       wfMessage( $message )
+                               ->numParams( $max )->inContentLanguage()->text() .
+                       '</span>';
+       }
+
+       /**
+        * Get an array of parameters to pass to ParserOutput::setLimitReportData()
+        *
+        * @internal Should only be called by Parser
+        * @return array
+        */
+       public function getLimitReport() {
+               return [
+                       [ 'limitreport-unstrip-depth',
+                               [
+                                       $this->highestDepth,
+                                       $this->depthLimit
+                               ],
+                       ],
+                       [ 'limitreport-unstrip-size',
+                               [
+                                       $this->expandSize,
+                                       $this->sizeLimit
+                               ],
+                       ]
+               ];
        }
 
        /**
         * Get a StripState object which is sufficient to unstrip the given text.
         * It will contain the minimum subset of strip items necessary.
         *
+        * @deprecated since 1.31
         * @param string $text
-        *
         * @return StripState
         */
        public function getSubState( $text ) {
-               $subState = new StripState();
+               wfDeprecated( __METHOD__, '1.31' );
+
+               $subState = new StripState;
                $pos = 0;
                while ( true ) {
                        $startPos = strpos( $text, Parser::MARKER_PREFIX, $pos );
@@ -202,11 +261,14 @@ class StripState {
         * will not be preserved. The strings in the $texts array will have their
         * strip markers rewritten, the resulting array of strings will be returned.
         *
+        * @deprecated since 1.31
         * @param StripState $otherState
         * @param array $texts
         * @return array
         */
        public function merge( $otherState, $texts ) {
+               wfDeprecated( __METHOD__, '1.31' );
+
                $mergePrefix = wfRandomString( 16 );
 
                foreach ( $otherState->data as $type => $items ) {
@@ -215,21 +277,14 @@ class StripState {
                        }
                }
 
-               $this->tempMergePrefix = $mergePrefix;
-               $texts = preg_replace_callback( $otherState->regex, [ $this, 'mergeCallback' ], $texts );
-               $this->tempMergePrefix = null;
+               $callback = function ( $m ) use ( $mergePrefix ) {
+                       $key = $m[1];
+                       return Parser::MARKER_PREFIX . $mergePrefix . '-' . $key . Parser::MARKER_SUFFIX;
+               };
+               $texts = preg_replace_callback( $otherState->regex, $callback, $texts );
                return $texts;
        }
 
-       /**
-        * @param array $m
-        * @return string
-        */
-       protected function mergeCallback( $m ) {
-               $key = $m[1];
-               return Parser::MARKER_PREFIX . $this->tempMergePrefix . '-' . $key . Parser::MARKER_SUFFIX;
-       }
-
        /**
         * Remove any strip markers found in the given text.
         *