summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'AbuseFilter/tests/phpunit/unit/AbuseFilterParserTest.php')
-rw-r--r--AbuseFilter/tests/phpunit/unit/AbuseFilterParserTest.php1374
1 files changed, 1374 insertions, 0 deletions
diff --git a/AbuseFilter/tests/phpunit/unit/AbuseFilterParserTest.php b/AbuseFilter/tests/phpunit/unit/AbuseFilterParserTest.php
new file mode 100644
index 00000000..4449852b
--- /dev/null
+++ b/AbuseFilter/tests/phpunit/unit/AbuseFilterParserTest.php
@@ -0,0 +1,1374 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @license GPL-2.0-or-later
+ * @author Marius Hoch < hoo@online.de >
+ */
+
+use MediaWiki\Extension\AbuseFilter\Hooks\AbuseFilterHookRunner;
+use MediaWiki\Extension\AbuseFilter\KeywordsManager;
+use Psr\Log\NullLogger;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group Test
+ * @group AbuseFilter
+ * @group AbuseFilterParser
+ *
+ * @covers AbuseFilterCachingParser
+ * @covers AFPTreeParser
+ * @covers AFPTransitionBase
+ * @covers AFPTreeNode
+ * @covers AFPSyntaxTree
+ * @covers AFPParserState
+ * @covers AbuseFilterParser
+ * @covers AbuseFilterTokenizer
+ * @covers AFPToken
+ * @covers AFPUserVisibleException
+ * @covers AFPException
+ * @covers AFPData
+ * @covers AFComputedVariable
+ */
+class AbuseFilterParserTest extends AbuseFilterParserTestCase {
+ /**
+ * @param string $rule The rule to parse
+ * @dataProvider readTests
+ */
+ public function testParser( $rule ) {
+ foreach ( $this->getParsers() as $parser ) {
+ $this->assertTrue( $parser->parse( $rule ), 'Parser used: ' . get_class( $parser ) );
+ }
+ }
+
+ /**
+ * @return Generator|array
+ */
+ public function readTests() {
+ $testPath = __DIR__ . "/../../parserTests";
+ $testFiles = glob( $testPath . "/*.t" );
+
+ foreach ( $testFiles as $testFile ) {
+ $testName = basename( substr( $testFile, 0, -2 ) );
+ $rule = trim( file_get_contents( $testFile ) );
+
+ yield $testName => [ $rule ];
+ }
+ }
+
+ /**
+ * Test expression evaluation
+ *
+ * @param string $expr The expression to evaluate
+ * @param string $expected The expected result
+ * @dataProvider provideExpressions
+ */
+ public function testEvaluateExpression( $expr, $expected ) {
+ foreach ( $this->getParsers() as $parser ) {
+ $actual = $parser->evaluateExpression( $expr );
+ $this->assertEquals( $expected, $actual );
+ }
+ }
+
+ /**
+ * Data provider for testEvaluateExpression
+ *
+ * @return array
+ */
+ public function provideExpressions() {
+ return [
+ [ '1 === 1', true ],
+ [ 'rescape( "abc* (def)" )', 'abc\* \(def\)' ],
+ [ 'str_replace( "foobarbaz", "bar", "-" )', 'foo-baz' ],
+ [ 'rmdoubles( "foobybboo" )', 'fobybo' ],
+ [ 'lcase("FÁmí")', 'fámí' ],
+ [ 'substr( "foobar", 0, 3 )', 'foo' ]
+ ];
+ }
+
+ /**
+ * Test empty (or almost empty) syntax and ensure it doesn't match
+ *
+ * @param string $code
+ * @dataProvider provideEmptySyntax
+ */
+ public function testEmptySyntax( $code ) {
+ foreach ( $this->getParsers() as $parser ) {
+ $this->assertFalse( $parser->parse( $code ) );
+ }
+ }
+
+ /**
+ * Data provider for testEmptySyntax
+ *
+ * @return array
+ */
+ public function provideEmptySyntax() {
+ return [
+ [ '' ],
+ [ ';;;;' ]
+ ];
+ }
+
+ /**
+ * Test a filter only containing a pair of empty parentheses. In the old parser, this should simply
+ * return false. In the new parser, it will throw.
+ */
+ public function testEmptyParenthesisOnly() {
+ $code = '()';
+ $constrArgs = [
+ $this->getLanguageMock(),
+ new EmptyBagOStuff(),
+ new NullLogger(),
+ new KeywordsManager( $this->createMock( AbuseFilterHookRunner::class ) )
+ ];
+
+ $parser = new AbuseFilterParser( ...$constrArgs );
+ $this->assertFalse( $parser->parse( $code ) );
+ $cachingParser = new AbuseFilterCachingParser( ...$constrArgs );
+ $this->expectException( AFPUserVisibleException::class );
+ $this->expectExceptionMessage( 'unexpectedtoken' );
+ $cachingParser->parse( $code );
+ }
+
+ /**
+ * Ensure that AbuseFilterTokenizer::OPERATOR_RE matches the contents
+ * and order of AbuseFilterTokenizer::$operators.
+ */
+ public function testOperatorRe() {
+ $quotedOps = array_map(
+ function ( $op ) {
+ return preg_quote( $op, '/' );
+ },
+ AbuseFilterTokenizer::OPERATORS
+ );
+ $operatorRe = '/(' . implode( '|', $quotedOps ) . ')/A';
+ $this->assertEquals( $operatorRe, AbuseFilterTokenizer::OPERATOR_RE );
+ }
+
+ /**
+ * Ensure the number of conditions counted for given expressions is right.
+ *
+ * @param string $rule The rule to parse
+ * @param int $expected The expected amount of used conditions
+ * @dataProvider condCountCases
+ */
+ public function testCondCount( $rule, $expected ) {
+ foreach ( $this->getParsers() as $parser ) {
+ $parserClass = get_class( $parser );
+ $countBefore = $parser->getCondCount();
+ $parser->parse( $rule );
+ $countAfter = $parser->getCondCount();
+ $actual = $countAfter - $countBefore;
+ $this->assertEquals( $expected, $actual, "Wrong condition count for $rule with $parserClass" );
+ }
+ }
+
+ /**
+ * Data provider for testCondCount method.
+ * @return array
+ */
+ public function condCountCases() {
+ return [
+ [ '((("a" == "b")))', 1 ],
+ [ 'contains_any("a", "b", "c")', 1 ],
+ [ '"a" == "b" & "b" == "c"', 1 ],
+ [ '"a" == "b" | "b" == "c"', 2 ],
+ [ '"a" in "b" + "c" in "d" + "e" in "f"', 3 ],
+ [ 'true', 0 ],
+ [ '"a" == "a" | "c" == "d"', 1 ],
+ [ '"a" == "b" & "c" == "d"', 1 ],
+ [ '1 = 0 & 2 * 3 * 4 <= 560 & "a" = "b"', 1 ],
+ [ '1 = 1 & 2 * 3 * 4 <= 560 & "a" = "b"', 3 ],
+ [ '1 = 1 | 2 * 3 * 4 <= 560 | "a" = "b"', 1 ],
+ [ '1 = 0 | 2 * 3 * 4 <= 560 | "a" = "b"', 2 ],
+ ];
+ }
+
+ /**
+ * Test for T204841
+ */
+ public function testArrayShortcircuit() {
+ $code = 'a := [false, false]; b := [false, false]; c := 42; d := [0,1];' .
+ 'a[0] != false & b[1] != false & (b[5**2/(5*(4+1))] !== a[43-c] | a[d[0]] === b[d[c-41]])';
+ foreach ( $this->getParsers() as $parser ) {
+ $this->assertFalse( $parser->parse( $code ), 'Parser: ' . get_class( $parser ) );
+ }
+ }
+
+ /**
+ * Test the 'expectednotfound' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @dataProvider expectedNotFound
+ */
+ public function testExpectedNotFoundException( $expr, $caller ) {
+ $this->exceptionTest( 'expectednotfound', $expr, $caller );
+ $this->exceptionTestInSkippedBlock( 'expectednotfound', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testExpectedNotFoundException.
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function expectedNotFound() {
+ return [
+ [ 'a:= [1,2,3]; a[1 = 4', 'doLevelSet' ],
+ [ "if 1 = 1 'foo'", 'doLevelConditions' ],
+ [ "if 1 = 1 then 'foo'", 'doLevelConditions' ],
+ [ "if 1 = 1 then 'foo' else 'bar'", 'doLevelConditions' ],
+ [ "a := 1 = 1 ? 'foo'", 'doLevelConditions' ],
+ [ '(1 = 1', 'doLevelBraces' ],
+ [ 'lcase = 3', 'doLevelFunction' ],
+ [ 'lcase( 3 = 1', 'doLevelFunction' ],
+ [ 'a := [1,2', 'doLevelAtom' ],
+ [ '1 = 1 | (1', 'skipOverBraces/doLevelParenthesis' ],
+ [ 'a := [1,2,3]; 3 = a[5', 'doLevelArrayElements' ],
+ [ 'if[3] := 1', 'doLevelConditions' ],
+ ];
+ }
+
+ /**
+ * Test the 'unexpectedatend' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @dataProvider unexpectedAtEnd
+ */
+ public function testUnexpectedAtEndException( $expr, $caller ) {
+ $this->exceptionTest( 'unexpectedatend', $expr, $caller );
+ $this->exceptionTestInSkippedBlock( 'unexpectedatend', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testUnexpectedAtEndException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function unexpectedAtEnd() {
+ return [
+ [ "'a' = 1 )", 'doLevelEntry' ],
+ ];
+ }
+
+ /**
+ * Test the 'unrecognisedvar' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @dataProvider unrecognisedVar
+ */
+ public function testUnrecognisedVarException( $expr, $caller ) {
+ $this->exceptionTest( 'unrecognisedvar', $expr, $caller );
+ $this->exceptionTestInSkippedBlock( 'unrecognisedvar', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testUnrecognisedVarException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function unrecognisedVar() {
+ return [
+ [ 'a[1] := 5', 'getVarValue' ],
+ [ 'a[] := 5', 'getVarValue' ],
+ [ 'a = 5', 'getVarValue' ],
+ [ 'timestamp[a]', 'getVarValue' ],
+ [ 'x := []; x[a] := 1', 'getVarValue' ],
+ ];
+ }
+
+ /**
+ * Special case, cannot use exceptionTestInSkippedBlock because that calls checkSyntax
+ */
+ public function testUnrecognisedArrayInSkippedBlock() {
+ $code = 'false & ( nonex[1] := 2 )';
+ // Old parser only
+ $parser = $this->getParsers()[0];
+ $this->expectException( AFPUserVisibleException::class );
+ $this->expectExceptionMessage( 'unrecognisedvar' );
+ $parser->parse( $code );
+ }
+
+ /**
+ * Test the 'notarray' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @dataProvider notArray
+ */
+ public function testNotArrayException( $expr, $caller ) {
+ $this->exceptionTest( 'notarray', $expr, $caller );
+ $this->exceptionTestInSkippedBlock( 'notarray', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testNotArrayException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function notArray() {
+ return [
+ [ 'a := 5; a[1] = 5', 'doLevelSet' ],
+ [ 'a := 1; 3 = a[5]', 'doLevelArrayElements' ],
+ [ 'a := 2; a[] := 2', '[different callers]' ],
+ [ 'a := 3; a[3] := 5', '[different callers]' ]
+ ];
+ }
+
+ /**
+ * Test the 'outofbounds' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @dataProvider outOfBounds
+ */
+ public function testOutOfBoundsException( $expr, $caller ) {
+ $this->exceptionTest( 'outofbounds', $expr, $caller );
+ $this->exceptionTestInSkippedBlock( 'outofbounds', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testOutOfBoundsException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function outOfBounds() {
+ return [
+ [ 'a := [2]; a[5] = 9', 'doLevelSet' ],
+ [ 'a := [1,2,3]; 3 = a[5]', 'doLevelArrayElements' ],
+ [ 'a := [1]; a[15] := 5', '[different callers]' ]
+ ];
+ }
+
+ /**
+ * Test the 'negativeindex' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @dataProvider negativeIndex
+ */
+ public function testNegativeIndexException( $expr, $caller ) {
+ $this->exceptionTest( 'negativeindex', $expr, $caller );
+ $this->exceptionTestInSkippedBlock( 'negativeindex', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testNegativeIndexException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function negativeIndex() {
+ return [
+ [ '[0][-1]', '' ],
+ [ "x := ['foo']; x[-1]", '' ],
+ [ "x := ['foo']; x[-1] := 2; x[-1] == 2", '' ],
+ [ "x := ['foo']; x[-5] := 2;", '' ]
+ ];
+ }
+
+ /**
+ * Test the 'unrecognisedkeyword' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @dataProvider unrecognisedKeyword
+ */
+ public function testUnrecognisedKeywordException( $expr, $caller ) {
+ $this->exceptionTest( 'unrecognisedkeyword', $expr, $caller );
+ $this->exceptionTestInSkippedBlock( 'unrecognisedkeyword', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testUnrecognisedKeywordException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function unrecognisedKeyword() {
+ return [
+ [ '5 = rlike', 'doLevelAtom' ],
+ [ 'then := 45', 'doLevelAtom' ],
+ ];
+ }
+
+ /**
+ * Test the 'unexpectedtoken' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @dataProvider unexpectedToken
+ */
+ public function testUnexpectedTokenException( $expr, $caller ) {
+ $this->exceptionTest( 'unexpectedtoken', $expr, $caller );
+ $this->exceptionTestInSkippedBlock( 'unexpectedtoken', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testUnexpectedTokenException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function unexpectedToken() {
+ return [
+ [ '1 =? 1', 'doLevelAtom' ],
+ ];
+ }
+
+ /**
+ * Test the 'disabledvar' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @dataProvider disabledVar
+ */
+ public function testDisabledVarException( $expr, $caller ) {
+ $this->exceptionTest( 'disabledvar', $expr, $caller );
+ $this->exceptionTestInSkippedBlock( 'disabledvar', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testDisabledVarException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function disabledVar() {
+ return [
+ [ 'old_text = 1', 'getVarValue' ],
+ ];
+ }
+
+ /**
+ * Test the 'variablevariable' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @dataProvider variableVariable
+ */
+ public function testVariableVariableException( $expr, $caller ) {
+ $this->exceptionTest( 'variablevariable', $expr, $caller );
+ $this->exceptionTestInSkippedBlock( 'variablevariable', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testVariableVariableException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function variableVariable() {
+ return [
+ [ "set( 'x' + 'y', 1 )", 'doLevelFunction' ],
+ [ "set( 'x' + page_title, 1 )", 'doLevelFunction' ],
+ [ "set( page_title, 1 )", 'doLevelFunction' ],
+ [ "set( page_title + 'x' + ( page_namespace == 0 ? 'x' : 'y' )", 'doLevelFunction' ],
+ ];
+ }
+
+ /**
+ * Test the 'overridebuiltin' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @dataProvider overrideBuiltin
+ */
+ public function testOverrideBuiltinException( $expr, $caller ) {
+ $this->exceptionTest( 'overridebuiltin', $expr, $caller );
+ $this->exceptionTestInSkippedBlock( 'overridebuiltin', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testOverrideBuiltinException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function overrideBuiltin() {
+ return [
+ [ 'added_lines := 1', 'setUserVariable' ],
+ [ 'added_lines[] := 1', 'doLevelSet' ],
+ [ 'added_lines[3] := 1', 'doLevelSet' ],
+ [ 'page_id[3] := 1', 'doLevelSet' ],
+ [ 'true | (added_lines := 1);','setUserVariable' ],
+ [ 'if(true) then 1 else (added_lines := 1) end;', 'setUserVariable' ],
+ [ 'length := 45', 'setUserVariable' ],
+ [ 'set("added_lines", 45)', 'setUserVariable' ],
+ [ 'set("length", 45)', 'setUserVariable' ],
+ [ 'set("true", true)', 'setUserVariable' ],
+ ];
+ }
+
+ /**
+ * Test for overriding a function name. The parsers cannot agree on this: the old parser
+ * will try to get the value of the variable before knowing that it's parsing an assignment,
+ * hence throwing unrecognisedvar. The new parser already knows it's an assignment and immediately
+ * throws an overridebuiltin (which is more correct).
+ * @todo Merge this into testOverrideBuiltin as soon as the old parser is deleted
+ */
+ public function testOverrideFuncName() {
+ $code = 'contains_any[1] := "foo"';
+ foreach ( $this->getParsers() as $parser ) {
+ $pname = get_class( $parser );
+ $exc = $parser instanceof AbuseFilterCachingParser ? 'overridebuiltin' : 'unrecognisedvar';
+ try {
+ $parser->parse( $code );
+ } catch ( AFPException $e ) {
+ $this->assertEquals( $exc, $e->mExceptionID, "Wrong exception with parser $pname, got:\n$e" );
+ continue;
+ }
+ $this->fail( "Exception $exc not thrown with parser $pname." );
+ }
+ }
+
+ /**
+ * Test the 'regexfailure' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @dataProvider regexFailure
+ */
+ public function testRegexFailureException( $expr, $caller ) {
+ $this->exceptionTest( 'regexfailure', $expr, $caller );
+ $this->exceptionTestInSkippedBlock( 'regexfailure', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testRegexFailureException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function regexFailure() {
+ return [
+ [ "rcount('(','a')", 'funcRCount' ],
+ [ "get_matches('this (should fail', 'any haystack')", 'funcGetMatches' ],
+ [ "'a' rlike '('", 'keywordRegex' ],
+ ];
+ }
+
+ /**
+ * Test the 'invalidiprange' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @dataProvider invalidIPRange
+ */
+ public function testInvalidIPRangeException( $expr, $caller ) {
+ $this->exceptionTest( 'invalidiprange', $expr, $caller );
+ $this->exceptionTestInSkippedBlock( 'invalidiprange', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testInvalidIPRangeException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function invalidIPRange() {
+ return [
+ [ "ip_in_range('0.0.0.0', 'lol')", 'funcIPInRange' ],
+ ];
+ }
+
+ /**
+ * Test functions which take exactly one parameters calling them
+ * without 0 params. They should throw a 'noparams' exception.
+ *
+ * @param string $func The function to test
+ * @dataProvider oneParamFuncs
+ */
+ public function testNoParamsException( $func ) {
+ $this->exceptionTest( 'noparams', "$func()", 'checkArgCount' );
+ $this->exceptionTestInSkippedBlock( 'noparams', "$func()", 'checkArgCount' );
+ }
+
+ /**
+ * Data provider for testNoParamsException, returns a list of
+ * functions taking a single parameter
+ *
+ * @return array
+ */
+ public function oneParamFuncs() {
+ return [
+ [ 'lcase' ],
+ [ 'ucase' ],
+ [ 'length' ],
+ [ 'strlen' ],
+ [ 'specialratio' ],
+ [ 'count' ],
+ [ 'rcount' ],
+ [ 'ccnorm' ],
+ [ 'sanitize' ],
+ [ 'rmspecials' ],
+ [ 'rmwhitespace' ],
+ [ 'rmdoubles' ],
+ [ 'norm' ],
+ [ 'rescape' ],
+ [ 'string' ],
+ [ 'int' ],
+ [ 'float' ],
+ [ 'bool' ],
+ ];
+ }
+
+ /**
+ * Test functions taking two parameters by providing only one.
+ * They should throw a 'notenoughargs' exception.
+ *
+ * @param string $func The function to test
+ * @dataProvider twoParamsFuncs
+ */
+ public function testNotEnoughArgsExceptionTwo( $func ) {
+ // Nevermind if the argument can't be string since we check the amount
+ // of parameters before anything else.
+ $code = "$func('foo')";
+ $this->exceptionTest( 'notenoughargs', $code, 'checkArgCount' );
+ $this->exceptionTestInSkippedBlock( 'notenoughargs', $code, 'checkArgCount' );
+ }
+
+ /**
+ * Data provider for testNotEnoughArgsExceptionTwo, returns the list of
+ * functions taking two parameters.
+ *
+ * @return array
+ */
+ public function twoParamsFuncs() {
+ return [
+ [ 'get_matches' ],
+ [ 'ip_in_range' ],
+ [ 'contains_any' ],
+ [ 'contains_all' ],
+ [ 'ccnorm_contains_any' ],
+ [ 'ccnorm_contains_all' ],
+ [ 'equals_to_any' ],
+ [ 'substr' ],
+ [ 'strpos' ],
+ [ 'set_var' ],
+ ];
+ }
+
+ /**
+ * Test functions taking three parameters by providing only two.
+ * They should throw a 'notenoughargs' exception.
+ *
+ * @param string $func The function to test
+ * @dataProvider threeParamsFuncs
+ */
+ public function testNotEnoughArgsExceptionThree( $func ) {
+ // Nevermind if the argument can't be string since we check the amount
+ // of parameters before anything else.
+ $code = "$func('foo', 'bar')";
+ $this->exceptionTest( 'notenoughargs', $code, 'checkArgCount' );
+ $this->exceptionTestInSkippedBlock( 'notenoughargs', $code, 'checkArgCount' );
+ }
+
+ /**
+ * Data provider for testNotEnoughArgsExceptionThree, returns the list of
+ * functions taking three parameters.
+ *
+ * @return array
+ */
+ public function threeParamsFuncs() {
+ return [
+ [ 'str_replace' ],
+ ];
+ }
+
+ /**
+ * @param string $code
+ * @dataProvider tooManyArgsFuncs
+ */
+ public function testTooManyArgumentsException( $code ) {
+ $this->exceptionTest( 'toomanyargs', $code, 'checkArgCount' );
+ $this->exceptionTestInSkippedBlock( 'toomanyargs', $code, 'checkArgCount' );
+ }
+
+ /**
+ * @return array
+ */
+ public function tooManyArgsFuncs() {
+ return [
+ [ "lcase( 'a', 'b' )" ],
+ [ "norm( 'a', 'b', 'c' )" ],
+ [ "count( 'a', 'b', 'c' )" ],
+ [ "ip_in_range( 'a', 'b', 'c' )" ],
+ [ "substr( 'a', 'b', 'c', 'd' )" ],
+ [ "str_replace( 'a', 'b', 'c', 'd', 'e' )" ],
+ ];
+ }
+
+ /**
+ * @param string $func
+ * @dataProvider variadicFuncs
+ */
+ public function testVariadicFuncsArbitraryArgsAllowed( $func ) {
+ $argsList = str_repeat( ', "arg"', 50 );
+ $code = "$func( 'arg' $argsList )";
+ foreach ( self::getParsers() as $parser ) {
+ $pname = get_class( $parser );
+ try {
+ $parser->parse( $code );
+ $this->assertTrue( true );
+ } catch ( AFPException $e ) {
+ $this->fail( "Got exception with parser $pname.\n$e" );
+ }
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function variadicFuncs() {
+ return [
+ [ 'contains_any' ],
+ [ 'contains_all' ],
+ [ 'equals_to_any' ],
+ ];
+ }
+
+ /**
+ * Check that calling a function with less arguments than required throws an exception
+ * when inside a skipped conditional branch.
+ *
+ * @param string $funcCode Code for a function call
+ * @param string $exceptionCode The ID of the expected exception
+ * @dataProvider provideFuncsForConditional
+ */
+ public function testCheckArgCountInConditional( $funcCode, $exceptionCode ) {
+ $code = "if ( 1==1 ) then ( 1 ) else ( $funcCode ) end;";
+ // AbuseFilterParser skips the parentheses altogether, so this is not supposed to work
+ $parser = new AbuseFilterCachingParser(
+ $this->getLanguageMock(),
+ new EmptyBagOStuff(),
+ new NullLogger(),
+ $this->createMock( KeywordsManager::class )
+ );
+ $parser->toggleConditionLimit( false );
+ try {
+ $parser->parse( $code );
+ $this->fail( 'No exception was thrown.' );
+ } catch ( AFPUserVisibleException $e ) {
+ $this->assertSame( $exceptionCode, $e->mExceptionID );
+ }
+ }
+
+ /**
+ * Data provider for testCheckArgCountInConditional
+ * @return array
+ */
+ public function provideFuncsForConditional() {
+ return [
+ [ 'count()', 'noparams' ],
+ [ 'bool()', 'noparams' ],
+ [ 'ip_in_range(1)', 'notenoughargs' ],
+ [ 'set_var("x")', 'notenoughargs' ],
+ [ 'str_replace("x","y")', 'notenoughargs' ]
+ ];
+ }
+
+ /**
+ * Check that deprecated variables are correctly translated to the new ones with a debug notice
+ *
+ * @param string $old The old name of the variable
+ * @param string $new The new name of the variable
+ * @dataProvider provideDeprecatedVars
+ *
+ * @covers AbuseFilterParser::getVarValue
+ * @covers AFPTreeParser::checkLogDeprecatedVar
+ */
+ public function testDeprecatedVars( $old, $new ) {
+ $keywordsManager = new KeywordsManager( $this->createMock( AbuseFilterHookRunner::class ) );
+ // Set it under the new name, and check that the old name points to it
+ $vars = AbuseFilterVariableHolder::newFromArray( [ $new => 'value' ], $keywordsManager );
+
+ foreach ( $this->getParsers() as $parser ) {
+ $pname = get_class( $parser );
+ $loggerMock = new TestLogger();
+ $loggerMock->setCollect( true );
+ $parser->setLogger( $loggerMock );
+
+ $parser->setVariables( $vars );
+ $actual = $parser->parse( "$old === $new" );
+
+ $loggerBuffer = $loggerMock->getBuffer();
+ // Check that the use has been logged
+ $found = false;
+ foreach ( $loggerBuffer as $entry ) {
+ $check = preg_match( '/^Deprecated variable/', $entry[1] );
+ if ( $check ) {
+ $found = true;
+ break;
+ }
+ }
+ if ( !$found ) {
+ $this->fail( "The use of the deprecated variable $old was not logged. Parser: $pname" );
+ }
+
+ $this->assertTrue( $actual, "Parser: $pname" );
+ }
+ }
+
+ /**
+ * Data provider for testDeprecatedVars
+ * @return Generator|array
+ */
+ public function provideDeprecatedVars() {
+ $keywordsManager = new KeywordsManager( $this->createMock( AbuseFilterHookRunner::class ) );
+ foreach ( $keywordsManager->getDeprecatedVariables() as $old => $new ) {
+ yield $old => [ $old, $new ];
+ }
+ }
+
+ /**
+ * Ensure that things like `'a' === 'b' === 'c'` or `1 < 2 < 3` are rejected, while `1 < 2 == 3`
+ * and `1 == 2 < 3` are not. (T218906)
+ * @param string $code Code to parse
+ * @param bool $valid Whether $code is valid (or should throw an exception)
+ * @dataProvider provideConsecutiveComparisons
+ */
+ public function testDisallowConsecutiveComparisons( $code, $valid ) {
+ foreach ( $this->getParsers() as $parser ) {
+ $pname = get_class( $parser );
+ $actuallyValid = true;
+ try {
+ $parser->parse( $code );
+ } catch ( AFPUserVisibleException $e ) {
+ $actuallyValid = false;
+ }
+
+ $this->assertSame(
+ $valid,
+ $actuallyValid,
+ 'The code should' . ( $valid ? ' ' : ' NOT ' ) . "be parsed correctly. Parser: $pname"
+ );
+ }
+ }
+
+ /**
+ * Data provider for testDisallowConsecutiveComparisons
+ *
+ * @return Generator
+ */
+ public function provideConsecutiveComparisons() {
+ // Same as AbuseFilterParser::doLevelCompares
+ $eqOps = [ '==', '===', '!=', '!==', '=' ];
+ $ordOps = [ '<', '>', '<=', '>=' ];
+ $ops = array_merge( $eqOps, $ordOps );
+ foreach ( $ops as $op1 ) {
+ foreach ( $ops as $op2 ) {
+ $testStr = "1 $op1 3.14 $op2 -1";
+ $valid = ( in_array( $op1, $eqOps ) && in_array( $op2, $ordOps ) ) ||
+ ( in_array( $op1, $ordOps ) && in_array( $op2, $eqOps ) );
+ yield $testStr => [ $testStr, $valid ];
+ }
+ }
+ // Some more cases with more than 2 comparisons
+ $extra = [
+ '1 === 1 < 3 === 0',
+ '1 === 1 < 3 === 0 < 555',
+ '1 < 3 === 0 < 555',
+ '1 < 3 === 0 < 555 !== 444',
+ '1 != 0 < 3 == 1 > 0 != 0'
+ ];
+ foreach ( $extra as $case ) {
+ yield $case => [ $case, false ];
+ }
+ }
+
+ /**
+ * Test that code declaring a variable in a skipped brace (because of shortcircuit)
+ * will be parsed without throwing an exception when later trying to use that var. T214674
+ *
+ * @param string $code Code to parse
+ * @dataProvider provideVarDeclarationInSkippedBlock
+ */
+ public function testVarDeclarationInSkippedBlock( $code ) {
+ foreach ( $this->getParsers() as $parser ) {
+ $pname = get_class( $parser );
+ try {
+ $this->assertFalse(
+ $parser->parse( $code ),
+ "Parser: $pname"
+ );
+ } catch ( AFPException $e ) {
+ $this->fail( "Got exception with parser: $pname\n$e" );
+ }
+ }
+ }
+
+ /**
+ * Data provider for testVarDeclarationInSkippedBlock
+ * @return array
+ */
+ public function provideVarDeclarationInSkippedBlock() {
+ return [
+ [ "x := [5]; false & (1 == 1; y := 'b'; x[1] := 'x'; 3 < 4); y != 'b' & x[1] != 'x'" ],
+ [ "(var := [1]); false & ( var[] := 'baz' ); count(var) > -1" ],
+ [ "(var := [1]); false & ( var[1] := 'baz' ); var[1] === 'baz'" ],
+ [ "false & (set('myvar', 1)); myvar contains 1" ],
+ [ "false & ( ( false & ( var := [1] ) ) | ( var[] := 2 ) ); var" ],
+ [ "false & ( ( false & ( var := [1] ); true ) | ( var[] := 2 ) ); var" ],
+ // The following tests are to ensure that we don't get a match
+ [ "false & ( var := 'foo'; x := get_matches( var, added_lines )[1] ); x != false" ],
+ [ "false & ( var := 'foo'); var !== null" ],
+ [ "false & ( var := 'foo'); var === null" ],
+ [ "false & (set('myvar', 'foo')); myvar === 'foo' | myvar !== 'foo'" ],
+ [ "false & ( var := 'foo'); var[0] !== 123456" ],
+ [ "false & ( var := 'foo'); var[0][123] !== 123456" ],
+ [ "false & (set('myvar', 'foo')); myvar[1][2] === 'foo' | myvar[1][2] !== 'foo'" ],
+ // Identifier before closing skipped brace, T214674#5374757
+ [ "false & ( var := 'foo'; 'x' in var )" ],
+ [ "false & ( var := 'foo'; added_lines irlike var )" ],
+ [ "false & ( if ( 1 == 1 ) then (var := 3) else (var := 4) end;); var !== 'foo'" ],
+ [ "if ( 1 === 1 ) then ( 0 ) else ( var := 1 ) end; var !== 'foo'" ],
+ [ "if ( 1=== 1 ) then (0) else ( false & ( var := 1 ) ) end; var !== 'foo'" ],
+ ];
+ }
+
+ /**
+ * Tests for the AFPData::DUNDEFINED type. No exceptions should be thrown.
+ *
+ * @param string $code To be parsed
+ * @dataProvider provideDUNDEFINED
+ */
+ public function testDUNDEFINED( $code ) {
+ foreach ( $this->getParsers() as $parser ) {
+ $pname = get_class( $parser );
+ // Note that some of the data sets will actually match at runtime, even if the variable
+ // they refer to is not set, due to the parser using GET_BC rather than GET_STRICT.
+ // TODO: Once T230256 is done, this can be changed to $this->assertFalse( $parser->parse( $code ) )
+ $this->assertTrue( $parser->checkSyntax( $code ), "Parser: $pname" );
+ }
+ }
+
+ /**
+ * Data provider for testDUNDEFINED. These bits of code must NOT match
+ *
+ * @return array
+ */
+ public function provideDUNDEFINED() {
+ return [
+ [ "5 / length( new_wikitext ) !== 3 ** edit_delta & " .
+ "float( timestamp / (user_age + 0.000001) ) !== 0.0" ],
+ [ "amount := float( timestamp / user_age); amount !== 0.0 & 64 / ( amount - 0.1 ) !== -640.0" ],
+ [ "36 / ( length( user_rights ) + 0.00001 ) !== 0" ],
+ [ "!('something' in added_lines)" ],
+ [ "!(user_groups rlike 'foo')" ],
+ [ "rcount('x', rescape(page_title) ) !== 0" ],
+ [ "norm(user_name) !== rmspecials('')" ],
+ [ "-user_editcount !== 1234567890" ],
+ [ "added_lines" ],
+ [ "removed_lines[0] !== 123456" ],
+ [ "-new_size" ],
+ [ "new_wikitext !== null" ],
+ [ "true & user_editcount" ],
+ [ "var:= 5; added_lines contains var" ],
+ [ "false & (var := [ 1,2,3 ]); var === [ 1,2,3 ]" ],
+ [ "page_age - user_editcount !== 1234567 - page_namespace" ],
+ [ "added_lines contains 'foo' ? 'foo' : false" ],
+ [ "timestamp / 12345 !== 'foobar'" ],
+ // Refuse to modify a DUNDEFINED offset as if it were an array
+ [ "false & (var := [ 1,2,3 ]); var[0] := true; var[0] === true" ],
+ [ "false & (var := [ 1,2,3 ]); var[] := 'baz'; 'baz' in var" ],
+ // But allow overwriting the whole variable
+ [ "false & (var := [ 1,2,3 ]); var := [4,5,6]; var !== [4,5,6]" ],
+ // Recursive DUNDEFINED replacement, T250570
+ [ '"x" in [ user_name ]' ],
+ [ 'string(user_name) in [ user_name ]' ],
+ [ 'string(user_name) in [ "x" ]' ],
+ [ '[ [ user_name ] ] in [ [ user_name ] ]' ],
+ [ 'equals_to_any( [ user_name ], [ user_name ] )' ],
+ ];
+ }
+
+ /**
+ * Test accessing builtin variables as arrays. This is always allowed when checking syntax, even
+ * if the variable is not an array (e.g. new_wikitext), but should fail when parsing.
+ *
+ * @param string $code
+ * @dataProvider provideBuiltinArrays
+ */
+ public function testBuiltinArrays( string $code ) {
+ foreach ( $this->getParsers() as $parser ) {
+ $pname = get_class( $parser );
+ $this->assertTrue( $parser->checkSyntax( $code ), "Parser: $pname" );
+
+ try {
+ $parser->parse( $code );
+ $this->fail( "Got no exception at parse-time. Parser: $pname" );
+ } catch ( AFPException $e ) {
+ $this->assertSame( 'notarray', $e->getMessage(), "Parser: $pname" );
+ }
+ }
+ }
+
+ /**
+ * Data provider for testBuiltinArrays
+ * @return array
+ */
+ public function provideBuiltinArrays() {
+ return [
+ [ "removed_lines[1] == 2" ],
+ [ "added_lines[0] contains 'x'" ],
+ [ "new_wikitext[1] !== 'xxx'" ]
+ ];
+ }
+
+ /**
+ * Test that empty operands are correctly logged in the old parser. Note that this test doesn't
+ * generate coverage *intentionally*. This is so that if the logEmptyOperand method becomes
+ * covered, there's likely a bug somewhere in the parser.
+ *
+ * @param string $code
+ * @param string $operandType
+ * @dataProvider provideEmptyOperands
+ */
+ public function testEmptyOperandsOldParser( $code, $operandType ) {
+ /** @var PHPUnit\Framework\MockObject\MockObject|AbuseFilterParser $mock */
+ $mock = $this->getMockBuilder( AbuseFilterParser::class )
+ ->setConstructorArgs( [
+ $this->getLanguageMock(),
+ new EmptyBagOStuff(),
+ new NullLogger(),
+ new KeywordsManager( $this->createMock( AbuseFilterHookRunner::class ) )
+ ] )
+ ->setMethods( [ 'logEmptyOperand' ] )
+ ->getMock();
+
+ $mock->expects( $this->once() )
+ ->method( 'logEmptyOperand' )
+ ->with( $operandType );
+
+ $mock->toggleConditionLimit( false );
+ $mock->parse( $code );
+ }
+
+ /**
+ * Test that empty operands raise an exception in the CachingParser
+ *
+ * @param string $code
+ * @dataProvider provideEmptyOperands
+ */
+ public function testEmptyOperandsCachingParser( $code ) {
+ static $parser = null;
+ if ( !$parser ) {
+ $parser = new AbuseFilterCachingParser(
+ $this->getLanguageMock(),
+ new EmptyBagOStuff(),
+ new NullLogger(),
+ $this->createMock( KeywordsManager::class )
+ );
+ $parser->toggleConditionLimit( false );
+ }
+ $this->expectException( AFPUserVisibleException::class );
+ $this->expectExceptionMessage( 'unexpectedtoken' );
+ $parser->parse( $code );
+ }
+
+ /**
+ * @return array
+ */
+ public function provideEmptyOperands() {
+ return [
+ [ '(0 |)', 'bool operand' ],
+ [ '(1 |)', 'bool operand' ],
+ [ '(0 &)', 'bool operand' ],
+ [ '(1 &)', 'bool operand' ],
+ [ '1==', 'compare operand' ],
+ [ '0<=', 'compare operand' ],
+ [ '1+', 'sum operand' ],
+ [ '0-', 'sum operand' ],
+ [ '1*', 'multiplication operand' ],
+ [ '1**', 'power operand' ],
+ [ '"string" contains', 'keyword operand' ],
+ [ '1 in', 'keyword operand' ],
+ [ "str_replace('a','b',)", 'non-variadic function argument' ],
+ [ "count('a',)", 'non-variadic function argument' ],
+ [ "(!)", 'bool inversion' ],
+ // `(false &!)` and `(true &!)`, originally reported in T156096,
+ // should be used in the future to test that they throw. However,
+ // using them now would log twice and thus make the test fail.
+ [ "var :=", 'var assignment' ],
+ [ "var :=[];var[] :=", 'array assignment' ],
+ [ "var :=[1];var[0] :=", 'array assignment' ],
+ [ "false ? false :", 'ternary else' ],
+ [ "true ? false :", 'ternary else' ],
+ [ "-", 'unary operand' ],
+ [ "+", 'unary operand' ],
+ [ 'if () then (1) end', 'parenthesized expression' ],
+ [ 'if () then (1) else (1) end', 'parenthesized expression' ],
+ [ 'if (true) then () end', 'parenthesized expression' ],
+ [ 'if (false) then () end', 'parenthesized expression' ],
+ [ 'if (true) then () else (3) end', 'parenthesized expression' ],
+ [ 'if (false) then () else (3) end', 'parenthesized expression' ],
+ [ 'if (true) then (1) else () end', 'parenthesized expression' ],
+ [ 'if (false) then (1) else () end', 'parenthesized expression' ],
+ [ '()', 'parenthesized expression' ]
+ ];
+ }
+
+ /**
+ * For the old parser, ensure that dangling commas for variadic functions aren't logged
+ * @param string $code
+ * @dataProvider provideDanglingCommasInVariargs
+ */
+ public function testDanglingCommasInVariargsNotLogged( $code ) {
+ /** @var PHPUnit\Framework\MockObject\MockObject|AbuseFilterParser $mock */
+ $mock = $this->getMockBuilder( AbuseFilterParser::class )
+ ->setConstructorArgs( [
+ $this->getLanguageMock(),
+ new EmptyBagOStuff(),
+ new NullLogger(),
+ new KeywordsManager( $this->createMock( AbuseFilterHookRunner::class ) )
+ ] )
+ ->setMethods( [ 'logEmptyOperand' ] )
+ ->getMock();
+
+ $mock->expects( $this->never() )
+ ->method( 'logEmptyOperand' )
+ ->with( 'non-variadic function argument' );
+
+ $mock->toggleConditionLimit( false );
+ $mock->parse( $code );
+ }
+
+ /**
+ * Ensure that the both parsers won't throw for dangling commas in variadic functions
+ * @param string $code
+ * @dataProvider provideDanglingCommasInVariargs
+ */
+ public function testDanglingCommasInVariargsAreValid( $code ) {
+ foreach ( $this->getParsers() as $parser ) {
+ $pname = get_class( $parser );
+ try {
+ $parser->parse( $code );
+ $this->assertTrue( true );
+ } catch ( AFPException $e ) {
+ $this->fail( "Got exception for dangling commas with parser $pname:\n$e" );
+ }
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function provideDanglingCommasInVariargs() {
+ return [
+ [ "contains_any('a','b','c',)" ],
+ [ "contains_all(1,1,1,1,1,1,1,)" ],
+ [ "equals_to_any(1,'foo',)" ],
+ ];
+ }
+
+ /**
+ * Ensure that an exception is thrown where there are extra commas in function calls, which
+ * are not the kind of allowed dangling commas.
+ *
+ * @param string $code
+ * @dataProvider provideExtraCommas
+ */
+ public function testExtraCommasNotAllowed( $code ) {
+ $this->exceptionTest( 'unexpectedtoken', $code, 'doLevelAtom' );
+ $this->exceptionTestInSkippedBlock( 'unexpectedtoken', $code, 'doLevelAtom' );
+ }
+
+ /**
+ * @return array
+ */
+ public function provideExtraCommas() {
+ return [
+ [ "norm(,,,)" ],
+ [ "str_replace(,'x','y')" ],
+ [ "contains_any(,)" ],
+ [ "contains_any(,,)" ],
+ [ "contains_any(1,2,,)" ],
+ [ "contains_any(1,2,,3,)" ],
+ ];
+ }
+
+ /**
+ * Ensure that any error in the arguments to a keyword or function is reported when
+ * checking syntax (T234339)
+ * @param string $code
+ * @param string $expID Expected exception ID
+ * @dataProvider provideArgsErrorsInSyntaxCheck
+ */
+ public function testArgsErrorsInSyntaxCheck( $code, $expID ) {
+ $caller = '[unavailable]';
+ $this->exceptionTest( $expID, $code, $caller );
+ $this->exceptionTestInSkippedBlock( $expID, $code, $caller );
+ }
+
+ /**
+ * @return array
+ */
+ public function provideArgsErrorsInSyntaxCheck() {
+ return [
+ [ 'accountname rlike "("', 'regexfailure' ],
+ [ 'contains_any( new_wikitext, "foo", 3/0 )', 'dividebyzero' ],
+ [ 'contains_any( added_lines, [ user_name, [ 3/0 ] ] )', 'dividebyzero' ],
+ [ 'rcount( "(", added_lines )', 'regexfailure' ],
+ [ 'get_matches( "(", new_wikitext )', 'regexfailure' ],
+ [ 'added_lines contains string(3/0)', 'dividebyzero' ],
+ [ 'norm(new_text) irlike ")"', 'regexfailure' ],
+ [ 'ip_in_range( user_name, "foobar" )', 'invalidiprange' ],
+ ];
+ }
+
+ /**
+ * Ensure that every function in AbuseFilterParser::FUNCTIONS is also listed in
+ * AbuseFilterParser::FUNC_ARG_COUNT
+ */
+ public function testAllFunctionsHaveArgCount() {
+ $funcs = array_keys( AbuseFilterParser::FUNCTIONS );
+ sort( $funcs );
+ $argsCount = array_keys( AbuseFilterParser::FUNC_ARG_COUNT );
+ sort( $argsCount );
+ $this->assertSame( $funcs, $argsCount );
+ }
+
+ /**
+ * @covers AbuseFilterParser::__construct
+ */
+ public function testConstructorInitsVars() {
+ $lang = $this->getLanguageMock();
+ $cache = $this->createMock( BagOStuff::class );
+ $logger = new NullLogger();
+ $keywordsManager = $this->createMock( KeywordsManager::class );
+ $vars = new AbuseFilterVariableHolder( $keywordsManager );
+
+ $parser = new AbuseFilterParser( $lang, $cache, $logger, $keywordsManager, $vars );
+ $this->assertEquals( $vars, $parser->mVariables, 'Variables should be initialized' );
+ $pVars = TestingAccessWrapper::newFromObject( $parser->mVariables );
+ $this->assertSame( $logger, $pVars->logger, 'VarHolder logger should be initialized' );
+ }
+
+ /**
+ * @covers AbuseFilterParser::setFilter
+ */
+ public function testSetFilter() {
+ $parser = TestingAccessWrapper::newFromObject( $this->getParsers()[0] );
+ $this->assertNull( $parser->mFilter, 'precondition' );
+ $filter = 42;
+ $parser->setFilter( $filter );
+ $this->assertSame( $filter, $parser->mFilter );
+ }
+
+ /**
+ * @covers AbuseFilterParser::setCache
+ */
+ public function testSetCache() {
+ $parser = TestingAccessWrapper::newFromObject( $this->getParsers()[0] );
+ $cache = $this->createMock( BagOStuff::class );
+ $parser->setCache( $cache );
+ $this->assertSame( $cache, $parser->cache );
+ }
+
+ /**
+ * @covers AbuseFilterParser::setLogger
+ */
+ public function testSetLogger() {
+ $parser = TestingAccessWrapper::newFromObject( $this->getParsers()[0] );
+ $logger = new NullLogger();
+ $parser->setLogger( $logger );
+ $this->assertSame( $logger, $parser->logger );
+ }
+
+ /**
+ * @covers AbuseFilterParser::setStatsd
+ */
+ public function testSetStatsd() {
+ $parser = TestingAccessWrapper::newFromObject( $this->getParsers()[0] );
+ $statsd = $this->createMock( IBufferingStatsdDataFactory::class );
+ $parser->setStatsd( $statsd );
+ $this->assertSame( $statsd, $parser->statsd );
+ }
+
+ /**
+ * @covers AbuseFilterParser::getCondCount
+ * @covers AbuseFilterParser::resetCondCount
+ */
+ public function testCondCountMethods() {
+ $parser = TestingAccessWrapper::newFromObject( $this->getParsers()[0] );
+ $this->assertSame( 0, $parser->mCondCount, 'precondition' );
+ $val = 42;
+ $parser->mCondCount = $val;
+ $this->assertSame( $val, $parser->getCondCount(), 'after set' );
+ $parser->resetCondCount( $val );
+ $this->assertSame( 0, $parser->getCondCount(), 'after reset' );
+ }
+
+ /**
+ * @covers AbuseFilterParser::toggleConditionLimit
+ */
+ public function testToggleConditionLimit() {
+ $parser = TestingAccessWrapper::newFromObject( $this->getParsers()[0] );
+
+ $parser->toggleConditionLimit( false );
+ $this->assertFalse( $parser->condLimitEnabled );
+
+ $parser->toggleConditionLimit( true );
+ $this->assertTrue( $parser->condLimitEnabled );
+ }
+
+ /**
+ * @covers AbuseFilterParser::clearFuncCache
+ */
+ public function testClearFuncCache() {
+ $parser = TestingAccessWrapper::newFromObject( $this->getParsers()[0] );
+
+ $parser->funcCache = [ 1, 2, 3 ];
+
+ $parser->clearFuncCache();
+ $this->assertSame( [], $parser->funcCache );
+ }
+
+ /**
+ * @covers AbuseFilterParser::setVariables
+ */
+ public function testSetVariables() {
+ $parser = TestingAccessWrapper::newFromObject( $this->getParsers()[0] );
+ $keywordsManager = new KeywordsManager( $this->createMock( AbuseFilterHookRunner::class ) );
+ $vars = new AbuseFilterVariableHolder( $keywordsManager );
+ $parser->setVariables( $vars );
+ $this->assertSame( $vars, $parser->mVariables );
+ }
+}