summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Evans <grknight@gentoo.org>2019-04-11 11:08:13 -0400
committerBrian Evans <grknight@gentoo.org>2019-04-11 11:08:13 -0400
commite6f63b37820d165b55e4c9bf262b3d6d92e28c67 (patch)
treedf5db2f24d45da64a8e3d90104cc5021dff3bf9c /AbuseFilter/tests
parentDrop Flow extension (diff)
downloadextensions-e6f63b37820d165b55e4c9bf262b3d6d92e28c67.tar.gz
extensions-e6f63b37820d165b55e4c9bf262b3d6d92e28c67.tar.bz2
extensions-e6f63b37820d165b55e4c9bf262b3d6d92e28c67.zip
Upgrade AbuseFilter to 1.32
Signed-off-by: Brian Evans <grknight@gentoo.org>
Diffstat (limited to 'AbuseFilter/tests')
-rw-r--r--AbuseFilter/tests/parserTests/arith.t28
-rw-r--r--AbuseFilter/tests/parserTests/array-assignment.r (renamed from AbuseFilter/tests/parserTests/list-assignment.r)0
-rw-r--r--AbuseFilter/tests/parserTests/array-assignment.t6
-rw-r--r--AbuseFilter/tests/parserTests/array-comparisons.r (renamed from AbuseFilter/tests/parserTests/list-inequality.r)0
-rw-r--r--AbuseFilter/tests/parserTests/array-comparisons.t15
-rw-r--r--AbuseFilter/tests/parserTests/atombraces.r1
-rw-r--r--AbuseFilter/tests/parserTests/atombraces.t1
-rw-r--r--AbuseFilter/tests/parserTests/cast.t2
-rw-r--r--AbuseFilter/tests/parserTests/ccnorm-contains-all.r (renamed from AbuseFilter/tests/parserTests/shortcircuit.r)0
-rw-r--r--AbuseFilter/tests/parserTests/ccnorm-contains-all.t1
-rw-r--r--AbuseFilter/tests/parserTests/ccnorm-contains-any.r (renamed from AbuseFilter/tests/parserTests/whitespace.r)0
-rw-r--r--AbuseFilter/tests/parserTests/ccnorm-contains-any.t1
-rw-r--r--AbuseFilter/tests/parserTests/comment.t5
-rw-r--r--AbuseFilter/tests/parserTests/concatenation.r1
-rw-r--r--AbuseFilter/tests/parserTests/concatenation.t7
-rw-r--r--AbuseFilter/tests/parserTests/contains-all.r1
-rw-r--r--AbuseFilter/tests/parserTests/contains-all.t1
-rw-r--r--AbuseFilter/tests/parserTests/contains-any.r1
-rw-r--r--AbuseFilter/tests/parserTests/contains-any.t1
-rw-r--r--AbuseFilter/tests/parserTests/contains.r1
-rw-r--r--AbuseFilter/tests/parserTests/contains.t7
-rw-r--r--AbuseFilter/tests/parserTests/containsfunction.r1
-rw-r--r--AbuseFilter/tests/parserTests/containsfunction.t2
-rw-r--r--AbuseFilter/tests/parserTests/count.t14
-rw-r--r--AbuseFilter/tests/parserTests/equals-to-any.r1
-rw-r--r--AbuseFilter/tests/parserTests/equals-to-any.t4
-rw-r--r--AbuseFilter/tests/parserTests/expn.t2
-rw-r--r--AbuseFilter/tests/parserTests/float.t2
-rw-r--r--AbuseFilter/tests/parserTests/get-matches.r1
-rw-r--r--AbuseFilter/tests/parserTests/get-matches.t4
-rw-r--r--AbuseFilter/tests/parserTests/ifthen.t3
-rw-r--r--AbuseFilter/tests/parserTests/in.t2
-rw-r--r--AbuseFilter/tests/parserTests/lazyboolinvert.r1
-rw-r--r--AbuseFilter/tests/parserTests/lazyboolinvert.t1
-rw-r--r--AbuseFilter/tests/parserTests/lazyfunction.r1
-rw-r--r--AbuseFilter/tests/parserTests/lazyfunction.t1
-rw-r--r--AbuseFilter/tests/parserTests/lazykeyword.r1
-rw-r--r--AbuseFilter/tests/parserTests/lazykeyword.t1
-rw-r--r--AbuseFilter/tests/parserTests/lazypow.r1
-rw-r--r--AbuseFilter/tests/parserTests/lazypow.t1
-rw-r--r--AbuseFilter/tests/parserTests/lazysum.r1
-rw-r--r--AbuseFilter/tests/parserTests/lazysum.t1
-rw-r--r--AbuseFilter/tests/parserTests/lazyunarys.r1
-rw-r--r--AbuseFilter/tests/parserTests/lazyunarys.t1
-rw-r--r--AbuseFilter/tests/parserTests/list-assignment.t6
-rw-r--r--AbuseFilter/tests/parserTests/list-inequality.t3
-rw-r--r--AbuseFilter/tests/parserTests/multipleskipbraces.r1
-rw-r--r--AbuseFilter/tests/parserTests/multipleskipbraces.t1
-rw-r--r--AbuseFilter/tests/parserTests/mwexamples-arithmetic.r1
-rw-r--r--AbuseFilter/tests/parserTests/mwexamples-arithmetic.t8
-rw-r--r--AbuseFilter/tests/parserTests/mwexamples-arrays.r1
-rw-r--r--AbuseFilter/tests/parserTests/mwexamples-arrays.t10
-rw-r--r--AbuseFilter/tests/parserTests/mwexamples-bools.r1
-rw-r--r--AbuseFilter/tests/parserTests/mwexamples-bools.t13
-rw-r--r--AbuseFilter/tests/parserTests/mwexamples-comparisons.r1
-rw-r--r--AbuseFilter/tests/parserTests/mwexamples-comparisons.t22
-rw-r--r--AbuseFilter/tests/parserTests/mwexamples-functions.r1
-rw-r--r--AbuseFilter/tests/parserTests/mwexamples-functions.t22
-rw-r--r--AbuseFilter/tests/parserTests/mwexamples-keywords.r1
-rw-r--r--AbuseFilter/tests/parserTests/mwexamples-keywords.t10
-rw-r--r--AbuseFilter/tests/parserTests/ord.t9
-rw-r--r--AbuseFilter/tests/parserTests/rcount.r1
-rw-r--r--AbuseFilter/tests/parserTests/rcount.t3
-rw-r--r--AbuseFilter/tests/parserTests/rmwhitespace.r1
-rw-r--r--AbuseFilter/tests/parserTests/rmwhitespace.t2
-rw-r--r--AbuseFilter/tests/parserTests/sanitize.r1
-rw-r--r--AbuseFilter/tests/parserTests/sanitize.t1
-rw-r--r--AbuseFilter/tests/parserTests/shortcircuit-and.r1
-rw-r--r--AbuseFilter/tests/parserTests/shortcircuit-and.t2
-rw-r--r--AbuseFilter/tests/parserTests/shortcircuit-or.r1
-rw-r--r--AbuseFilter/tests/parserTests/shortcircuit-or.t (renamed from AbuseFilter/tests/parserTests/shortcircuit.t)0
-rw-r--r--AbuseFilter/tests/parserTests/specialratio.t3
-rw-r--r--AbuseFilter/tests/parserTests/string.t7
-rw-r--r--AbuseFilter/tests/parserTests/strpos.r1
-rw-r--r--AbuseFilter/tests/parserTests/strpos.t4
-rw-r--r--AbuseFilter/tests/parserTests/substr.r1
-rw-r--r--AbuseFilter/tests/parserTests/substr.t2
-rw-r--r--AbuseFilter/tests/phan/config.php19
-rw-r--r--AbuseFilter/tests/phpunit/AFPDataTest.php122
-rw-r--r--AbuseFilter/tests/phpunit/AbuseFilterConsequencesTest.php902
-rw-r--r--AbuseFilter/tests/phpunit/AbuseFilterParserTest.php744
-rw-r--r--AbuseFilter/tests/phpunit/AbuseFilterSaveTest.php596
-rw-r--r--AbuseFilter/tests/phpunit/AbuseFilterTest.php1413
-rw-r--r--AbuseFilter/tests/phpunit/AbuseFilterTokenizerTest.php146
-rw-r--r--AbuseFilter/tests/phpunit/parserTest.php145
85 files changed, 4187 insertions, 170 deletions
diff --git a/AbuseFilter/tests/parserTests/arith.t b/AbuseFilter/tests/parserTests/arith.t
index e3c42e32..1f88e9df 100644
--- a/AbuseFilter/tests/parserTests/arith.t
+++ b/AbuseFilter/tests/parserTests/arith.t
@@ -1 +1,27 @@
-(1 + 1 == 2) & (5 - 3 = 2) & (2 * 3 = 6) & (10 / 2 = 5) & (10 % 7 = 3) & (2 ** 4 = 16)
+(1 + 1 === 2) &
+(1.5 + 1.5 === 3.0) &
+(2.5 + 1 === 3.5) &
+(0 + 1 === 1) &
+(2.5 + 0 === 2.5) &
+(5 - 3 === 2) &
+(5 - 3.5 === 1.5) &
+(5.5 - 3.5 === 2.0) &
+(1 - 0 === 1) &
+(2.5 - 0 === 2.5) &
+(2 * 3 === 6) &
+(2 * 3.5 === 7.0) &
+(2.5 * 3.5 === 8.75) &
+(2.5 * 0 === 0.0) &
+(10 / 2 === 5) &
+(10 / 2.5 === 4.0) &
+(18 / 36 === 0.5) &
+(0 / 36 === 0) &
+(12.5 / 2.5 === 5.0) &
+(10.5 / 2.5 === 4.2) &
+(10 % 7 === 3) &
+(10.48762 % 7 === 3) &
+(10 % 7.123576 === 3) &
+(2 ** 4 === 16) &
+(2.5 ** 2 === 6.25) &
+(2.5 ** 0 === 1.0) &
+(1000 ** 0 === 1) \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/list-assignment.r b/AbuseFilter/tests/parserTests/array-assignment.r
index 4736e080..4736e080 100644
--- a/AbuseFilter/tests/parserTests/list-assignment.r
+++ b/AbuseFilter/tests/parserTests/array-assignment.r
diff --git a/AbuseFilter/tests/parserTests/array-assignment.t b/AbuseFilter/tests/parserTests/array-assignment.t
new file mode 100644
index 00000000..a22294e3
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/array-assignment.t
@@ -0,0 +1,6 @@
+test_array := [ [1, 2], [3, 4] ];
+
+test_array[1] := 42;
+test_array[] := 17;
+
+test_array[0][0] == 1 & test_array[0][1] == 2 & test_array[1] == 42 & test_array[2] == 17
diff --git a/AbuseFilter/tests/parserTests/list-inequality.r b/AbuseFilter/tests/parserTests/array-comparisons.r
index 4736e080..4736e080 100644
--- a/AbuseFilter/tests/parserTests/list-inequality.r
+++ b/AbuseFilter/tests/parserTests/array-comparisons.r
diff --git a/AbuseFilter/tests/parserTests/array-comparisons.t b/AbuseFilter/tests/parserTests/array-comparisons.t
new file mode 100644
index 00000000..dc6122f5
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/array-comparisons.t
@@ -0,0 +1,15 @@
+a := [1, 2, 3];
+b := [1, 2, 3];
+c := [2, 3, 4];
+d := [1, 2, 3, 4];
+e := ['1', '2', '3'];
+f := [[['1']]];
+g := [[[1]]];
+h := [[1, 2], 3];
+i := [['1', 2], '3'];
+j := [1];
+k := ['1'];
+l := [];
+
+a == b & a === b & a != c & b != d & a == e & a !== e & f == g & f !== g & h == i & h !== i & e != i & j != 1 &
+k != '1' & l == false & l == null & l !== false & l !== null & false == l & null == l & false !== l & null !== l
diff --git a/AbuseFilter/tests/parserTests/atombraces.r b/AbuseFilter/tests/parserTests/atombraces.r
new file mode 100644
index 00000000..f4efce9f
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/atombraces.r
@@ -0,0 +1 @@
+NOT MATCH
diff --git a/AbuseFilter/tests/parserTests/atombraces.t b/AbuseFilter/tests/parserTests/atombraces.t
new file mode 100644
index 00000000..5bb50190
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/atombraces.t
@@ -0,0 +1 @@
+( )
diff --git a/AbuseFilter/tests/parserTests/cast.t b/AbuseFilter/tests/parserTests/cast.t
index 0ec2720e..c0bc317c 100644
--- a/AbuseFilter/tests/parserTests/cast.t
+++ b/AbuseFilter/tests/parserTests/cast.t
@@ -1 +1 @@
-(string(1) === "1") & (int("1") === 1) & (float(1) === 1.0)
+(string(1) === "1") & (int("1") === 1) & (float(1) === 1.0) & bool(1) & !bool(0)
diff --git a/AbuseFilter/tests/parserTests/shortcircuit.r b/AbuseFilter/tests/parserTests/ccnorm-contains-all.r
index 4736e080..4736e080 100644
--- a/AbuseFilter/tests/parserTests/shortcircuit.r
+++ b/AbuseFilter/tests/parserTests/ccnorm-contains-all.r
diff --git a/AbuseFilter/tests/parserTests/ccnorm-contains-all.t b/AbuseFilter/tests/parserTests/ccnorm-contains-all.t
new file mode 100644
index 00000000..9a8635c8
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/ccnorm-contains-all.t
@@ -0,0 +1 @@
+ccnorm_contains_all("the f00 is on the b4r", "foo", "is on", "bar")
diff --git a/AbuseFilter/tests/parserTests/whitespace.r b/AbuseFilter/tests/parserTests/ccnorm-contains-any.r
index 4736e080..4736e080 100644
--- a/AbuseFilter/tests/parserTests/whitespace.r
+++ b/AbuseFilter/tests/parserTests/ccnorm-contains-any.r
diff --git a/AbuseFilter/tests/parserTests/ccnorm-contains-any.t b/AbuseFilter/tests/parserTests/ccnorm-contains-any.t
new file mode 100644
index 00000000..6aeac35c
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/ccnorm-contains-any.t
@@ -0,0 +1 @@
+ccnorm_contains_any("like 4ny0ne else", "foo", "aNyon3") & ccnorm_contains_any("street f1ghter","F1ght")
diff --git a/AbuseFilter/tests/parserTests/comment.t b/AbuseFilter/tests/parserTests/comment.t
index 2ddf5829..3252b87a 100644
--- a/AbuseFilter/tests/parserTests/comment.t
+++ b/AbuseFilter/tests/parserTests/comment.t
@@ -1 +1,4 @@
-1 /* a */ == /* b */ "1" /* c */
+1 /* a */ == /* b */ "1" /* c */ /* &
+
+1/0
+*/
diff --git a/AbuseFilter/tests/parserTests/concatenation.r b/AbuseFilter/tests/parserTests/concatenation.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/concatenation.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/concatenation.t b/AbuseFilter/tests/parserTests/concatenation.t
new file mode 100644
index 00000000..c4a8065f
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/concatenation.t
@@ -0,0 +1,7 @@
+'foo' + 'bar' === 'foobar' &
+'' + 'foo' + '' === 'foo' &
+'foo' + ' ' + 'bar' === 'foo bar' &
+'foo' + 234 === 'foo234' &
+452 + 'foo' === '452foo' &
+'foo' + false === 'foo' &
+'foo' + [ 'bar', 'foo' ] === 'foobar\nfoo\n'
diff --git a/AbuseFilter/tests/parserTests/contains-all.r b/AbuseFilter/tests/parserTests/contains-all.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/contains-all.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/contains-all.t b/AbuseFilter/tests/parserTests/contains-all.t
new file mode 100644
index 00000000..f8b81b24
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/contains-all.t
@@ -0,0 +1 @@
+contains_all("the foo is on the bar", "foo", "is on", "bar") & !(contains_all(['foo', 'bar', 'hey'], 'foo', 'bar', 'sup')) & contains_all([1, 2, 3], '1', '2', '3') \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/contains-any.r b/AbuseFilter/tests/parserTests/contains-any.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/contains-any.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/contains-any.t b/AbuseFilter/tests/parserTests/contains-any.t
new file mode 100644
index 00000000..56f1c8bd
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/contains-any.t
@@ -0,0 +1 @@
+contains_any("like anyone else", "else", "someone") & contains_any("street fighter", "fight") & !(contains_any('My foo is cute', 'bar', 'wtf')) & contains_any([[1], [2,3]], 1)
diff --git a/AbuseFilter/tests/parserTests/contains.r b/AbuseFilter/tests/parserTests/contains.r
new file mode 100644
index 00000000..f629599c
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/contains.r
@@ -0,0 +1 @@
+MATCH \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/contains.t b/AbuseFilter/tests/parserTests/contains.t
new file mode 100644
index 00000000..5aeb638a
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/contains.t
@@ -0,0 +1,7 @@
+"quux" contains "ux" &
+['1', 'foo'] contains '1' &
+'fo obar' contains 'foo' === false &
+['foo'] contains 'f' &
+'' contains 'a' === false &
+'a' contains '' === false &
+'' contains '' === false \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/containsfunction.r b/AbuseFilter/tests/parserTests/containsfunction.r
new file mode 100644
index 00000000..f629599c
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/containsfunction.r
@@ -0,0 +1 @@
+MATCH \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/containsfunction.t b/AbuseFilter/tests/parserTests/containsfunction.t
new file mode 100644
index 00000000..418c9e2c
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/containsfunction.t
@@ -0,0 +1,2 @@
+contains_any( "", "a") === false &
+contains_any( "a", "", "a") === true \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/count.t b/AbuseFilter/tests/parserTests/count.t
index 5733b0b2..cc9e719f 100644
--- a/AbuseFilter/tests/parserTests/count.t
+++ b/AbuseFilter/tests/parserTests/count.t
@@ -1,6 +1,8 @@
-count("a,b,c,d") = 4 &
-count(",", "a,b,c,d") = 3 &
-count("", "abcd") = 0 &
-count("a", "abab") = 2 &
-count("ab", "abab") = 2 &
-count("aa", "aaaaa") = 2
+count("a,b,c,d") === 4 &
+count(",", "a,b,c,d") === 3 &
+count("", "abcd") === 0 &
+count("a", "abab") === 2 &
+count("ab", "abab") === 2 &
+count("aa", "aaaaa") === 2 &
+count( [ "a", "b", "c" ] ) === 3 &
+count( [] ) === 0
diff --git a/AbuseFilter/tests/parserTests/equals-to-any.r b/AbuseFilter/tests/parserTests/equals-to-any.r
new file mode 100644
index 00000000..f629599c
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/equals-to-any.r
@@ -0,0 +1 @@
+MATCH \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/equals-to-any.t b/AbuseFilter/tests/parserTests/equals-to-any.t
new file mode 100644
index 00000000..e10220ca
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/equals-to-any.t
@@ -0,0 +1,4 @@
+equals_to_any( "foo", "bar", "foo", "pizza" ) &
+equals_to_any( 15, 3, 77, 18, 15 ) &
+equals_to_any( "", 3, 77, 18, 15, "duh" ) === false &
+equals_to_any( "", 3, 77, 18, 15, "duh", "" ) \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/expn.t b/AbuseFilter/tests/parserTests/expn.t
index bd39c386..17849182 100644
--- a/AbuseFilter/tests/parserTests/expn.t
+++ b/AbuseFilter/tests/parserTests/expn.t
@@ -1,2 +1,2 @@
/* In filter language, the exponentiation is left-associative */
-(2 ** 3 ** 2) == 64
+(2 ** 3 ** 2) === 64
diff --git a/AbuseFilter/tests/parserTests/float.t b/AbuseFilter/tests/parserTests/float.t
index 9ce8c919..e31067e0 100644
--- a/AbuseFilter/tests/parserTests/float.t
+++ b/AbuseFilter/tests/parserTests/float.t
@@ -1 +1 @@
-(5 / 2 = 2) & (5. / 2 = 2.5) & (5 / 2. = 2.5) & (int(.5) = 0)
+(5 / 2 === 2.5) & (int(5 / 2) === 2) & (5. / 2 === 2.5) & (5 / 2. === 2.5) & (int(.5) === 0)
diff --git a/AbuseFilter/tests/parserTests/get-matches.r b/AbuseFilter/tests/parserTests/get-matches.r
new file mode 100644
index 00000000..f629599c
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/get-matches.r
@@ -0,0 +1 @@
+MATCH \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/get-matches.t b/AbuseFilter/tests/parserTests/get-matches.t
new file mode 100644
index 00000000..88e163b4
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/get-matches.t
@@ -0,0 +1,4 @@
+/* More complete tests for get_matches are in AbuseFilterParserTest.php */
+a := get_matches('I am a (dog|cat)', 'What did you say?');
+get_matches('The (truth|pineapple) is (?:rarely)? pure and (nee*v(ah|er) sh?imple)', 'The truth is rarely pure and never simple, Wilde said') == ['The truth is rarely pure and never simple', 'truth', 'never simple', 'er'] &
+a === [false, false] \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/ifthen.t b/AbuseFilter/tests/parserTests/ifthen.t
index 160d1ab8..c22a3947 100644
--- a/AbuseFilter/tests/parserTests/ifthen.t
+++ b/AbuseFilter/tests/parserTests/ifthen.t
@@ -1 +1,2 @@
-(if 1 then 2 else 3 end) == 2
+(if 1 then 2 else 3 end) === 2 &
+(if false then 2 else 3 end) === 3
diff --git a/AbuseFilter/tests/parserTests/in.t b/AbuseFilter/tests/parserTests/in.t
index 467639ce..73d480d3 100644
--- a/AbuseFilter/tests/parserTests/in.t
+++ b/AbuseFilter/tests/parserTests/in.t
@@ -1 +1 @@
-"foo" in "foobar" & "quux" contains "ux"
+"foo" in "foobar" & '1' in ['1', 'foo'] & !('foo' in 'fo obar') & 'f' in ['foo']
diff --git a/AbuseFilter/tests/parserTests/lazyboolinvert.r b/AbuseFilter/tests/parserTests/lazyboolinvert.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/lazyboolinvert.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/lazyboolinvert.t b/AbuseFilter/tests/parserTests/lazyboolinvert.t
new file mode 100644
index 00000000..71f5f213
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/lazyboolinvert.t
@@ -0,0 +1 @@
+1 === 1 | !true
diff --git a/AbuseFilter/tests/parserTests/lazyfunction.r b/AbuseFilter/tests/parserTests/lazyfunction.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/lazyfunction.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/lazyfunction.t b/AbuseFilter/tests/parserTests/lazyfunction.t
new file mode 100644
index 00000000..4c77b374
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/lazyfunction.t
@@ -0,0 +1 @@
+1 === 1 | contains_any( "a", "b")
diff --git a/AbuseFilter/tests/parserTests/lazykeyword.r b/AbuseFilter/tests/parserTests/lazykeyword.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/lazykeyword.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/lazykeyword.t b/AbuseFilter/tests/parserTests/lazykeyword.t
new file mode 100644
index 00000000..da680aef
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/lazykeyword.t
@@ -0,0 +1 @@
+1 === 1 | "a" like "b"
diff --git a/AbuseFilter/tests/parserTests/lazypow.r b/AbuseFilter/tests/parserTests/lazypow.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/lazypow.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/lazypow.t b/AbuseFilter/tests/parserTests/lazypow.t
new file mode 100644
index 00000000..8b650e27
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/lazypow.t
@@ -0,0 +1 @@
+1 === 1 | 2**3 === 8
diff --git a/AbuseFilter/tests/parserTests/lazysum.r b/AbuseFilter/tests/parserTests/lazysum.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/lazysum.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/lazysum.t b/AbuseFilter/tests/parserTests/lazysum.t
new file mode 100644
index 00000000..9fedb3f3
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/lazysum.t
@@ -0,0 +1 @@
+1 === 1 | 2 + 3 === 5
diff --git a/AbuseFilter/tests/parserTests/lazyunarys.r b/AbuseFilter/tests/parserTests/lazyunarys.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/lazyunarys.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/lazyunarys.t b/AbuseFilter/tests/parserTests/lazyunarys.t
new file mode 100644
index 00000000..51a1ce3e
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/lazyunarys.t
@@ -0,0 +1 @@
+1 === 1 | -4 !== 4
diff --git a/AbuseFilter/tests/parserTests/list-assignment.t b/AbuseFilter/tests/parserTests/list-assignment.t
deleted file mode 100644
index c055541b..00000000
--- a/AbuseFilter/tests/parserTests/list-assignment.t
+++ /dev/null
@@ -1,6 +0,0 @@
-test_list := [ [1, 2], [3, 4] ];
-
-test_list[1] := 42;
-test_list[] := 17;
-
-test_list[0][0] == 1 & test_list[0][1] == 2 & test_list[1] == 42 & test_list[2] == 17
diff --git a/AbuseFilter/tests/parserTests/list-inequality.t b/AbuseFilter/tests/parserTests/list-inequality.t
deleted file mode 100644
index 6ffed4f0..00000000
--- a/AbuseFilter/tests/parserTests/list-inequality.t
+++ /dev/null
@@ -1,3 +0,0 @@
-a := [1, 2, 3];
-
-a != a
diff --git a/AbuseFilter/tests/parserTests/multipleskipbraces.r b/AbuseFilter/tests/parserTests/multipleskipbraces.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/multipleskipbraces.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/multipleskipbraces.t b/AbuseFilter/tests/parserTests/multipleskipbraces.t
new file mode 100644
index 00000000..ea8def7f
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/multipleskipbraces.t
@@ -0,0 +1 @@
+1 === 1 | ( ( ( 3 === 2 ) ) )
diff --git a/AbuseFilter/tests/parserTests/mwexamples-arithmetic.r b/AbuseFilter/tests/parserTests/mwexamples-arithmetic.r
new file mode 100644
index 00000000..f629599c
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/mwexamples-arithmetic.r
@@ -0,0 +1 @@
+MATCH \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/mwexamples-arithmetic.t b/AbuseFilter/tests/parserTests/mwexamples-arithmetic.t
new file mode 100644
index 00000000..7cfaf4af
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/mwexamples-arithmetic.t
@@ -0,0 +1,8 @@
+/* Examples from [[mw:Extension:AbuseFilter/Rules format#Arithmetic]] */
+
+1 + 1 === 2 &
+2 * 2 === 4 &
+12 / 24 === 0.5 &
+24 / 12 === 2 &
+9 ** 2 === 81 &
+6 % 5 === 1 \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/mwexamples-arrays.r b/AbuseFilter/tests/parserTests/mwexamples-arrays.r
new file mode 100644
index 00000000..f629599c
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/mwexamples-arrays.r
@@ -0,0 +1 @@
+MATCH \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/mwexamples-arrays.t b/AbuseFilter/tests/parserTests/mwexamples-arrays.t
new file mode 100644
index 00000000..91538fed
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/mwexamples-arrays.t
@@ -0,0 +1,10 @@
+/* Examples from [[mw:Extension:AbuseFilter/Rules format#Lists]] */
+
+a_array := [ 5, 6, 7, 10];
+a_array[0] == 5 &
+length(a_array) == 4 &
+string(a_array) == "5\n6\n7\n10\n" &
+5 in a_array == true &
+'5' in a_array == true &
+'5\n6' in a_array == true &
+1 in a_array == true \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/mwexamples-bools.r b/AbuseFilter/tests/parserTests/mwexamples-bools.r
new file mode 100644
index 00000000..f629599c
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/mwexamples-bools.r
@@ -0,0 +1 @@
+MATCH \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/mwexamples-bools.t b/AbuseFilter/tests/parserTests/mwexamples-bools.t
new file mode 100644
index 00000000..a8e88539
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/mwexamples-bools.t
@@ -0,0 +1,13 @@
+/* Examples from [[mw:Extension:AbuseFilter/Rules format#Boolean operations]] */
+
+(1 | 1) &
+(1 | 0) &
+!(0 | 0) &
+(1 & 1) &
+!(1 & 0) &
+!(0 & 0) &
+!(1 ^ 1) &
+(1 ^ 0) &
+!(0 ^ 0) &
+!(!1)
+
diff --git a/AbuseFilter/tests/parserTests/mwexamples-comparisons.r b/AbuseFilter/tests/parserTests/mwexamples-comparisons.r
new file mode 100644
index 00000000..f629599c
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/mwexamples-comparisons.r
@@ -0,0 +1 @@
+MATCH \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/mwexamples-comparisons.t b/AbuseFilter/tests/parserTests/mwexamples-comparisons.t
new file mode 100644
index 00000000..76582590
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/mwexamples-comparisons.t
@@ -0,0 +1,22 @@
+/* Examples from [[mw:Extension:AbuseFilter/Rules format#Simple comparisons]] */
+
+!(1 == 2) &
+(1 <= 2) &
+!(1 >= 2) &
+(1 != 2) &
+(1 < 2) &
+!(1 > 2) &
+(2 = 2) &
+('' == false) &
+!('' === false) &
+(1 == true) &
+!(1 === true) &
+(['1','2','3'] == ['1','2','3']) &
+([1,2,3] === [1,2,3]) &
+(['1','2','3'] == [1,2,3]) &
+!(['1','2','3'] === [1,2,3]) &
+([1,1,''] == [true, true, false]) &
+([] == false) &
+([] == null) &
+!(['1'] == '1')
+
diff --git a/AbuseFilter/tests/parserTests/mwexamples-functions.r b/AbuseFilter/tests/parserTests/mwexamples-functions.r
new file mode 100644
index 00000000..f629599c
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/mwexamples-functions.r
@@ -0,0 +1 @@
+MATCH \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/mwexamples-functions.t b/AbuseFilter/tests/parserTests/mwexamples-functions.t
new file mode 100644
index 00000000..ea5bf1b1
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/mwexamples-functions.t
@@ -0,0 +1,22 @@
+/* Examples from [[mw:Extension:AbuseFilter/Rules format#Functions]] */
+
+length( "Wikipedia" ) === 9 &
+lcase( "WikiPedia" ) === 'wikipedia' &
+ccnorm( "w1k1p3d14" ) === 'WIKIPEDIA' &
+ccnorm( "ωɨƙɩᑭƐƉ1α" ) === 'WIKIPEDIA' &
+ccnorm_contains_any( "w1k1p3d14", "wiKiP3D1A", "foo", "bar" ) === true &
+ccnorm_contains_any( "w1k1p3d14", "foo", "bar", "baz" ) === false &
+ccnorm_contains_any( "w1k1p3d14 is 4w3s0me", "bar", "baz", "some" ) === true &
+ccnorm( "ìíîïĩїį!ľ₤ĺľḷĿ" ) === 'IIIIIII!LLLLLL' &
+norm( "!!ω..ɨ..ƙ..ɩ..ᑭᑭ..Ɛ.Ɖ@@1%%α!!" ) === 'WIKIPEDAIA' &
+norm( "F00 B@rr" ) === 'FOBAR' &
+rmdoubles( "foobybboo" ) === 'fobybo' &
+specialratio( "Wikipedia!" ) === 0.1 &
+count( "foo", "foofooboofoo" ) === 3 &
+count( "foo,bar,baz" ) === 3 &
+rmspecials( "FOOBAR!!1" ) === 'FOOBAR1' &
+rescape( "abc* (def)" ) === 'abc\* \(def\)' &
+str_replace( "foobarbaz", "bar", "-" ) === 'foo-baz' &
+ip_in_range( "127.0.10.0", "127.0.0.0/12" ) === true &
+contains_any( "foobar", "x", "y", "f" ) === true &
+get_matches( "(foo?ba+r) is (so+ good)", "fobaaar is soooo good to eat" ) === ['fobaaar is soooo good', 'fobaaar', 'soooo good'] \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/mwexamples-keywords.r b/AbuseFilter/tests/parserTests/mwexamples-keywords.r
new file mode 100644
index 00000000..f629599c
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/mwexamples-keywords.r
@@ -0,0 +1 @@
+MATCH \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/mwexamples-keywords.t b/AbuseFilter/tests/parserTests/mwexamples-keywords.t
new file mode 100644
index 00000000..d2fbb4a5
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/mwexamples-keywords.t
@@ -0,0 +1,10 @@
+/* Examples from [[mw:Extension:AbuseFilter/Rules format#Keywords]] */
+
+("1234" like "12?4") &
+("1234" like "12*") &
+("foo" in "foobar") &
+("foobar" contains "foo") &
+("o" in ["foo", "bar"]) &
+("foo" regex "\w+") &
+("a\b" regex "a\\\\b") &
+("a\b" regex "a\x5C\x5Cb") \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/ord.t b/AbuseFilter/tests/parserTests/ord.t
index a82aafa7..6c94f4b6 100644
--- a/AbuseFilter/tests/parserTests/ord.t
+++ b/AbuseFilter/tests/parserTests/ord.t
@@ -1 +1,8 @@
-(1 > 0) & (0 < 1) & (2 >= 2) & (2 <= 2)
+1 > 0 &
+0 < 1 &
+2 >= 2 &
+2 <= 2 &
+0.1 < 0.2 &
+0.001 <= 0.001 &
+-0.01 < 0.01 &
+0 >= -0.0001
diff --git a/AbuseFilter/tests/parserTests/rcount.r b/AbuseFilter/tests/parserTests/rcount.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/rcount.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/rcount.t b/AbuseFilter/tests/parserTests/rcount.t
new file mode 100644
index 00000000..a514dad5
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/rcount.t
@@ -0,0 +1,3 @@
+rcount("a,b,c,d") = 4 &
+rcount(".", "abcd") = 4
+
diff --git a/AbuseFilter/tests/parserTests/rmwhitespace.r b/AbuseFilter/tests/parserTests/rmwhitespace.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/rmwhitespace.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/rmwhitespace.t b/AbuseFilter/tests/parserTests/rmwhitespace.t
new file mode 100644
index 00000000..d6da2114
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/rmwhitespace.t
@@ -0,0 +1,2 @@
+rmwhitespace( "foobar" ) === "foobar" &
+rmwhitespace( "foo bar bar foo" ) === "foobarbarfoo"
diff --git a/AbuseFilter/tests/parserTests/sanitize.r b/AbuseFilter/tests/parserTests/sanitize.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/sanitize.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/sanitize.t b/AbuseFilter/tests/parserTests/sanitize.t
new file mode 100644
index 00000000..06036c0c
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/sanitize.t
@@ -0,0 +1 @@
+sanitize('&#1740;&#1705;') = 'یک'
diff --git a/AbuseFilter/tests/parserTests/shortcircuit-and.r b/AbuseFilter/tests/parserTests/shortcircuit-and.r
new file mode 100644
index 00000000..33a8a805
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/shortcircuit-and.r
@@ -0,0 +1 @@
+NOT_MATCH \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/shortcircuit-and.t b/AbuseFilter/tests/parserTests/shortcircuit-and.t
new file mode 100644
index 00000000..d87746de
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/shortcircuit-and.t
@@ -0,0 +1,2 @@
+/* The division by zero should not be executed and not crash the filter */
+false & 1/0 \ No newline at end of file
diff --git a/AbuseFilter/tests/parserTests/shortcircuit-or.r b/AbuseFilter/tests/parserTests/shortcircuit-or.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/shortcircuit-or.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/shortcircuit.t b/AbuseFilter/tests/parserTests/shortcircuit-or.t
index bec088f6..bec088f6 100644
--- a/AbuseFilter/tests/parserTests/shortcircuit.t
+++ b/AbuseFilter/tests/parserTests/shortcircuit-or.t
diff --git a/AbuseFilter/tests/parserTests/specialratio.t b/AbuseFilter/tests/parserTests/specialratio.t
index c4a3565a..5a1380a8 100644
--- a/AbuseFilter/tests/parserTests/specialratio.t
+++ b/AbuseFilter/tests/parserTests/specialratio.t
@@ -1 +1,2 @@
-specialratio("foó;") = 0.25
+specialratio("foó;") === 0.25 &
+specialratio("") === 0.0
diff --git a/AbuseFilter/tests/parserTests/string.t b/AbuseFilter/tests/parserTests/string.t
index f6036cef..47685ed9 100644
--- a/AbuseFilter/tests/parserTests/string.t
+++ b/AbuseFilter/tests/parserTests/string.t
@@ -1 +1,6 @@
-"a\tb" = "a b" & "a\qb" = "a\qb"
+"a\tb" === "a b" &
+"a\qb" === "a\qb" &
+"a\"b" === 'a"b' &
+"a\rb" !== "a\r\nb" &
+"\x66\x6f\x6f" === "foo" &
+"some\xstring" === "somexstring"
diff --git a/AbuseFilter/tests/parserTests/strpos.r b/AbuseFilter/tests/parserTests/strpos.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/strpos.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/strpos.t b/AbuseFilter/tests/parserTests/strpos.t
new file mode 100644
index 00000000..65312318
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/strpos.t
@@ -0,0 +1,4 @@
+strpos( "foobarfoo", "foo" ) === 0 &
+strpos( "foobarfoo", "" ) === -1 &
+strpos( "foobarfoo", "foo", 1 ) === 6 &
+strpos( "foobarfoo", "lol" ) === -1
diff --git a/AbuseFilter/tests/parserTests/substr.r b/AbuseFilter/tests/parserTests/substr.r
new file mode 100644
index 00000000..4736e080
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/substr.r
@@ -0,0 +1 @@
+MATCH
diff --git a/AbuseFilter/tests/parserTests/substr.t b/AbuseFilter/tests/parserTests/substr.t
new file mode 100644
index 00000000..b1135066
--- /dev/null
+++ b/AbuseFilter/tests/parserTests/substr.t
@@ -0,0 +1,2 @@
+substr( "foobar", 0, 3 ) === "foo" &
+substr( "barfoo", 4 ) === "oo"
diff --git a/AbuseFilter/tests/phan/config.php b/AbuseFilter/tests/phan/config.php
new file mode 100644
index 00000000..99685d08
--- /dev/null
+++ b/AbuseFilter/tests/phan/config.php
@@ -0,0 +1,19 @@
+<?php
+
+$cfg = require __DIR__ . '/../../vendor/mediawiki/mediawiki-phan-config/src/config.php';
+
+$cfg['directory_list'] = array_merge(
+ $cfg['directory_list'],
+ [
+ './../../extensions/CheckUser',
+ ]
+);
+
+$cfg['exclude_analysis_directory_list'] = array_merge(
+ $cfg['exclude_analysis_directory_list'],
+ [
+ './../../extensions/CheckUser',
+ ]
+);
+
+return $cfg;
diff --git a/AbuseFilter/tests/phpunit/AFPDataTest.php b/AbuseFilter/tests/phpunit/AFPDataTest.php
new file mode 100644
index 00000000..88315da4
--- /dev/null
+++ b/AbuseFilter/tests/phpunit/AFPDataTest.php
@@ -0,0 +1,122 @@
+<?php
+/**
+ * Tests for the AFPData class
+ *
+ * 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
+ */
+
+/**
+ * @group Test
+ * @group AbuseFilter
+ *
+ * @covers AFPData
+ * @covers AbuseFilterTokenizer
+ * @covers AFPToken
+ * @covers AFPUserVisibleException
+ * @covers AFPException
+ * @covers AbuseFilterParser
+ */
+class AFPDataTest extends MediaWikiTestCase {
+ /**
+ * @return AbuseFilterParser
+ */
+ public static function getParser() {
+ static $parser = null;
+ if ( !$parser ) {
+ $parser = new AbuseFilterParser();
+ } else {
+ $parser->resetState();
+ }
+ return $parser;
+ }
+
+ /**
+ * Base method for testing exceptions
+ *
+ * @param string $excep Identifier of the exception (e.g. 'unexpectedtoken')
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ */
+ private function exceptionTest( $excep, $expr, $caller ) {
+ $parser = self::getParser();
+ try {
+ $parser->parse( $expr );
+ } catch ( AFPUserVisibleException $e ) {
+ $this->assertEquals(
+ $excep,
+ $e->mExceptionID,
+ "Exception $excep not thrown in AFPData::$caller"
+ );
+ return;
+ }
+
+ $this->fail( "Exception $excep not thrown in AFPData::$caller" );
+ }
+
+ /**
+ * Test the 'regexfailure' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @covers AFPData::keywordRegex
+ * @dataProvider regexFailure
+ */
+ public function testRegexFailureException( $expr, $caller ) {
+ $this->exceptionTest( '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 [
+ [ "'a' rlike '('", 'keywordRegex' ],
+ ];
+ }
+
+ /**
+ * Test the 'dividebyzero' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @covers AFPData::mulRel
+ * @dataProvider divideByZero
+ */
+ public function testDivideByZeroException( $expr, $caller ) {
+ $this->exceptionTest( 'dividebyzero', $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 divideByZero() {
+ return [
+ [ '1/0', 'mulRel' ],
+ ];
+ }
+}
diff --git a/AbuseFilter/tests/phpunit/AbuseFilterConsequencesTest.php b/AbuseFilter/tests/phpunit/AbuseFilterConsequencesTest.php
new file mode 100644
index 00000000..4be865b5
--- /dev/null
+++ b/AbuseFilter/tests/phpunit/AbuseFilterConsequencesTest.php
@@ -0,0 +1,902 @@
+<?php
+/**
+ * Complete tests where filters are saved, actions are executed and the right
+ * consequences are expected to be taken
+ *
+ * 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
+ */
+
+/**
+ * @group Test
+ * @group AbuseFilter
+ * @group AbuseFilterConsequences
+ * @group Database
+ *
+ * @covers AbuseFilter
+ * @covers AbuseFilterHooks
+ * @covers AbuseFilterParser
+ * @covers AFPData
+ * @covers AbuseFilterTokenizer
+ * @covers AFPToken
+ * @covers AbuseFilterVariableHolder
+ * @covers AFComputedVariable
+ */
+class AbuseFilterConsequencesTest extends MediaWikiTestCase {
+ protected static $mUser;
+
+ /**
+ * @var array This tables will be deleted in parent::tearDown
+ */
+ protected $tablesUsed = [
+ 'abuse_filter',
+ 'abuse_filter_action',
+ 'abuse_filter_history',
+ 'abuse_filter_log',
+ 'page'
+ ];
+
+ // Properties of the filter rows that we're not interested in changing.
+ // Write them once to save space
+ protected static $defaultRowSection = [
+ 'af_user_text' => 'FilterTester',
+ 'af_user' => 0,
+ 'af_timestamp' => '20180707105743',
+ 'af_group' => 'default',
+ 'af_hit_count' => 0,
+ ];
+
+ // Filters that may be created, their key is the ID.
+ protected static $filters = [
+ 1 => [
+ 'af_id' => 1,
+ 'af_pattern' => 'added_lines irlike "foo"',
+ 'af_enabled' => 1,
+ 'af_comments' => 'Comments',
+ 'af_public_comments' => 'Mock filter for edit',
+ 'af_hidden' => 0,
+ 'af_throttled' => 0,
+ 'af_deleted' => 0,
+ 'af_actions' => 'warn,tag',
+ 'af_global' => 0,
+ 'actions' => [
+ 'warn' => [
+ 'abusefilter-my-warning'
+ ],
+ 'tag' => [
+ 'filtertag'
+ ]
+ ]
+ ],
+ 2 => [
+ 'af_id' => 2,
+ 'af_pattern' => 'action = "move" & moved_to_title contains "test" & moved_to_title === moved_to_text',
+ 'af_enabled' => 1,
+ 'af_comments' => 'No comment',
+ 'af_public_comments' => 'Mock filter for move',
+ 'af_hidden' => 1,
+ 'af_throttled' => 0,
+ 'af_deleted' => 0,
+ 'af_actions' => 'disallow,block',
+ 'af_global' => 0,
+ 'actions' => [
+ 'disallow' => [],
+ 'block' => [
+ 'blocktalk',
+ '8 hours',
+ 'infinity'
+ ]
+ ]
+ ],
+ 3 => [
+ 'af_id' => 3,
+ 'af_pattern' => 'action = "delete" & "test" in lcase(page_prefixedtitle) & page_prefixedtitle === article_prefixedtext',
+ 'af_enabled' => 1,
+ 'af_comments' => '',
+ 'af_public_comments' => 'Mock filter for delete',
+ 'af_hidden' => 0,
+ 'af_throttled' => 0,
+ 'af_deleted' => 0,
+ 'af_actions' => 'degroup',
+ 'af_global' => 0,
+ 'actions' => [
+ 'degroup' => []
+ ]
+ ],
+ 4 => [
+ 'af_id' => 4,
+ 'af_pattern' => 'action contains "createaccount" & accountname rlike "user" & page_title === article_text',
+ 'af_enabled' => 1,
+ 'af_comments' => '1',
+ 'af_public_comments' => 'Mock filter for createaccount',
+ 'af_hidden' => 1,
+ 'af_throttled' => 0,
+ 'af_deleted' => 0,
+ 'af_actions' => '',
+ 'af_global' => 0,
+ 'actions' => []
+ ],
+ 5 => [
+ 'af_id' => 5,
+ 'af_pattern' => 'user_name == "FilteredUser"',
+ 'af_enabled' => 1,
+ 'af_comments' => '',
+ 'af_public_comments' => 'Mock filter',
+ 'af_hidden' => 0,
+ 'af_throttled' => 0,
+ 'af_deleted' => 0,
+ 'af_actions' => 'tag',
+ 'af_global' => 0,
+ 'actions' => [
+ 'tag' => [
+ 'firstTag',
+ 'secondTag'
+ ]
+ ]
+ ],
+ 6 => [
+ 'af_id' => 6,
+ 'af_pattern' => 'edit_delta === 7',
+ 'af_enabled' => 1,
+ 'af_comments' => '',
+ 'af_public_comments' => 'Mock filter with edit_delta',
+ 'af_hidden' => 1,
+ 'af_throttled' => 0,
+ 'af_deleted' => 0,
+ 'af_actions' => 'disallow',
+ 'af_global' => 0,
+ 'actions' => [
+ 'disallow' => []
+ ]
+ ],
+ 7 => [
+ 'af_id' => 7,
+ 'af_pattern' => 'timestamp === int(timestamp)',
+ 'af_enabled' => 1,
+ 'af_comments' => '',
+ 'af_public_comments' => 'Mock filter with timestamp',
+ 'af_hidden' => 0,
+ 'af_throttled' => 0,
+ 'af_deleted' => 0,
+ 'af_actions' => 'degroup',
+ 'af_global' => 0,
+ 'actions' => [
+ 'degroup' => []
+ ]
+ ],
+ 8 => [
+ 'af_id' => 8,
+ 'af_pattern' => 'added_lines_pst irlike "\\[\\[Link\\|Link\\]\\]"',
+ 'af_enabled' => 1,
+ 'af_comments' => '',
+ 'af_public_comments' => 'Mock filter with pst',
+ 'af_hidden' => 0,
+ 'af_hit_count' => 0,
+ 'af_deleted' => 0,
+ 'af_actions' => 'disallow,block',
+ 'af_global' => 0,
+ 'actions' => [
+ 'disallow' => [],
+ 'block' => [
+ 'NoTalkBlockSet',
+ '4 hours',
+ '4 hours'
+ ]
+ ]
+ ],
+ 9 => [
+ 'af_id' => 9,
+ 'af_pattern' => 'new_size > old_size',
+ 'af_enabled' => 1,
+ 'af_comments' => '',
+ 'af_public_comments' => 'Mock filter with size',
+ 'af_hidden' => 1,
+ 'af_throttled' => 0,
+ 'af_deleted' => 0,
+ 'af_actions' => 'disallow,block',
+ 'af_global' => 0,
+ 'actions' => [
+ 'disallow' => [],
+ 'block' => [
+ 'blocktalk',
+ '3 hours',
+ '3 hours'
+ ]
+ ]
+ ],
+ 10 => [
+ 'af_id' => 10,
+ 'af_pattern' => '1 == 1',
+ 'af_enabled' => 1,
+ 'af_comments' => '',
+ 'af_public_comments' => 'Mock throttled filter',
+ 'af_hidden' => 1,
+ 'af_throttled' => 1,
+ 'af_deleted' => 0,
+ 'af_actions' => 'tag,block',
+ 'af_global' => 0,
+ 'actions' => [
+ 'tag' => [
+ 'testTag'
+ ],
+ 'block' => [
+ 'blocktalk',
+ 'infinity',
+ 'infinity'
+ ]
+ ]
+ ],
+ 11 => [
+ 'af_id' => 11,
+ 'af_pattern' => '1 == 1',
+ 'af_enabled' => 1,
+ 'af_comments' => '',
+ 'af_public_comments' => 'Mock filter which throttles',
+ 'af_hidden' => 0,
+ 'af_throttled' => 0,
+ 'af_deleted' => 0,
+ 'af_actions' => 'throttle,disallow',
+ 'af_global' => 0,
+ 'actions' => [
+ 'throttle' => [
+ 11,
+ '1,3600',
+ 'user'
+ ],
+ 'disallow' => []
+ ]
+ ],
+ 12 => [
+ 'af_id' => 12,
+ 'af_pattern' => 'page_title == user_name & user_name === page_title',
+ 'af_enabled' => 1,
+ 'af_comments' => '',
+ 'af_public_comments' => 'Mock filter for userpage',
+ 'af_hidden' => 0,
+ 'af_throttled' => 0,
+ 'af_deleted' => 0,
+ 'af_actions' => 'disallow,block,degroup',
+ 'af_global' => 0,
+ 'actions' => [
+ 'disallow' => [],
+ 'block' => [
+ 'blocktalk',
+ '8 hours',
+ '1 day'
+ ],
+ 'degroup' => []
+ ]
+ ],
+ 13 => [
+ 'af_id' => 13,
+ 'af_pattern' => '2 == 2',
+ 'af_enabled' => 1,
+ 'af_comments' => '',
+ 'af_public_comments' => 'Another throttled mock filter',
+ 'af_hidden' => 0,
+ 'af_throttled' => 1,
+ 'af_deleted' => 0,
+ 'af_actions' => 'block,degroup',
+ 'af_global' => 0,
+ 'actions' => [
+ 'block' => [
+ 'blocktalk',
+ '8 hours',
+ '1 day'
+ ],
+ 'degroup' => []
+ ]
+ ],
+ 14 => [
+ 'af_id' => 14,
+ 'af_pattern' => '5/int(article_text) == 3',
+ 'af_enabled' => 1,
+ 'af_comments' => '',
+ 'af_public_comments' => 'Filter with a possible division by zero',
+ 'af_hidden' => 0,
+ 'af_throttled' => 0,
+ 'af_deleted' => 0,
+ 'af_actions' => 'disallow',
+ 'af_global' => 0,
+ 'actions' => [
+ 'disallow' => []
+ ]
+ ]
+ ];
+
+ /**
+ * @see MediaWikiTestCase::setUp
+ */
+ protected function setUp() {
+ parent::setUp();
+ $user = User::newFromName( 'FilteredUser' );
+ $user->addToDatabase();
+ $user->addGroup( 'sysop' );
+ if ( $user->isBlocked() ) {
+ $block = Block::newFromTarget( $user );
+ $block->delete();
+ }
+ self::$mUser = $user;
+ // Make sure that the config we're using is the one we're expecting
+ $this->setMwGlobals( [
+ 'wgUser' => $user,
+ 'wgAbuseFilterActions' => [
+ 'throttle' => true,
+ 'warn' => true,
+ 'disallow' => true,
+ 'blockautopromote' => true,
+ 'block' => true,
+ 'rangeblock' => true,
+ 'degroup' => true,
+ 'tag' => true
+ ],
+ 'wgAbuseFilterRuntimeProfile' => true,
+ 'wgAbuseFilterProfile' => true
+ ] );
+ }
+
+ /**
+ * Performs an edit. Freely adapted from EditPageTest::assertEdit
+ *
+ * @param Title $title Title of the page to edit
+ * @param string $oldText Old content of the page
+ * @param string $newText The new content of the page
+ * @param string $summary The summary of the edit
+ * @return Status
+ */
+ private static function doEdit( $title, $oldText, $newText, $summary ) {
+ $page = WikiPage::factory( $title );
+ $content = ContentHandler::makeContent( $oldText, $title );
+ $page->doEditContent( $content, 'Creating the page for testing AbuseFilter.' );
+
+ $params = [
+ 'wpTextbox1' => $newText,
+ 'wpSummary' => $summary,
+ 'wpEditToken' => self::$mUser->getEditToken(),
+ 'wpEdittime' => $page->getTimestamp(),
+ 'wpStarttime' => wfTimestampNow(),
+ 'wpUnicodeCheck' => EditPage::UNICODE_CHECK,
+ 'wpSectionTitle' => '',
+ 'wpMinorEdit' => false,
+ 'wpWatchthis' => false
+ ];
+ $req = new FauxRequest( $params, true );
+
+ $article = new Article( $title );
+ $article->getContext()->setTitle( $title );
+ $article->getContext()->setUser( self::$mUser );
+ $ep = new EditPage( $article );
+ $ep->setContextTitle( $title );
+ $ep->importFormData( $req );
+ return $ep->internalAttemptSave( $result );
+ }
+
+ /**
+ * Executes an action to filter
+ *
+ * @param array $params Parameters of the action
+ * @param array $options Further options
+ * @return Status|Status[]
+ */
+ private static function doAction( $params, $options ) {
+ $type = array_shift( $params );
+ $target = array_shift( $params );
+ $target = Title::newFromText( $target );
+ // Make sure that previous blocks don't affect the test
+ self::$mUser->clearInstanceCache();
+
+ switch ( $type ) {
+ case 'edit':
+ if ( in_array( 'makeGoodEditFirst', $options ) ) {
+ $firstStatus = self::doEdit(
+ $target, $params['oldText'], $params['firstNewText'], $params['summary']
+ );
+ $secondStatus = self::doEdit(
+ $target, $params['firstNewText'], $params['secondNewText'], $params['summary']
+ );
+ $status = [ $firstStatus, $secondStatus ];
+ } else {
+ $status = self::doEdit( $target, $params['oldText'], $params['newText'], $params['summary'] );
+ }
+ break;
+ case 'move':
+ $move = new MovePage( $target, Title::newFromText( $params['newTitle'] ) );
+ $status = $move->checkPermissions( self::$mUser, 'AbuseFilter move test' );
+ break;
+ case 'delete':
+ $page = WikiPage::factory( $target );
+ $content = ContentHandler::makeContent( 'Page to be deleted in AbuseFilter test', $target );
+ $page->doEditContent( $content, 'Creating the page for testing deletion AbuseFilter.' );
+ $status = $page->doDeleteArticleReal( 'Testing deletion in AbuseFilter' );
+ break;
+ case 'createaccount':
+ $user = User::newFromName( $params['username'] );
+ $provider = new AbuseFilterPreAuthenticationProvider();
+ $status = $provider->testForAccountCreation( $user, $user, [] );
+ break;
+ }
+
+ // Clear cache since we'll need to retrieve some fresh data about the user
+ // like blocks and groups later when checking expected values
+ self::$mUser->clearInstanceCache();
+
+ return $status;
+ }
+
+ /**
+ * Creates new filters with the given ids, referred to self::$filters
+ *
+ * @param int[] $ids IDs of the filters to create
+ */
+ private static function createFilters( $ids ) {
+ global $wgAbuseFilterActions;
+ $dbw = wfGetDB( DB_MASTER );
+
+ foreach ( $ids as $id ) {
+ $filter = array_merge( self::$filters[$id], self::$defaultRowSection );
+ $actions = $filter['actions'];
+ unset( $filter['actions'] );
+
+ $dbw->replace(
+ 'abuse_filter',
+ [ 'af_id' ],
+ $filter,
+ __METHOD__
+ );
+
+ $actionRows = [];
+ foreach ( array_filter( $wgAbuseFilterActions ) as $action => $_ ) {
+ if ( isset( $actions[$action] ) ) {
+ $parameters = $actions[$action];
+
+ $thisRow = [
+ 'afa_filter' => $id,
+ 'afa_consequence' => $action,
+ 'afa_parameters' => implode( "\n", $parameters )
+ ];
+ $actionsRows[] = $thisRow;
+ }
+ }
+
+ $dbw->replace(
+ 'abuse_filter_action',
+ [ 'afa_filter' ],
+ $actionsRows,
+ __METHOD__
+ );
+ }
+ }
+
+ /**
+ * Creates new filters, execute an action and check the consequences
+ *
+ * @param string $testDescription A short description of the test, used for error reporting
+ * @param int[] $createIds IDs of the filters to create
+ * @param array $actionParams Details of the action we need to execute to trigger filters
+ * @param array $consequences The consequences we're expecting
+ * @param array $options Further options for the test
+ * @covers AbuseFilter
+ * @dataProvider provideFilters
+ */
+ public function testFilterConsequences(
+ $testDescription,
+ $createIds,
+ $actionParams,
+ $consequences,
+ $options
+ ) {
+ global $wgLang;
+ self::createFilters( $createIds );
+
+ if ( in_array( 'makeGoodEditFirst', $options ) ) {
+ $this->setMwGlobals( [
+ // Necessary to test throttle
+ 'wgMainCacheType' => CACHE_ANYTHING
+ ] );
+ }
+ if ( in_array( 'hitCondsLimit', $options ) ) {
+ $this->setMwGlobals( [
+ 'wgAbuseFilterConditionLimit' => 0
+ ] );
+ }
+ if ( in_array( 'hitTimeLimit', $options ) ) {
+ $this->setMwGlobals( [
+ 'wgAbuseFilterSlowFilterRuntimeLimit' => 0
+ ] );
+ }
+ if ( in_array( 'hitThrottleLimit', $options ) ) {
+ $this->setMwGlobals( [
+ 'wgAbuseFilterEmergencyDisableCount' => [
+ 'default' => 0
+ ]
+ ] );
+ }
+
+ $result = self::doAction( $actionParams, $options );
+
+ $expectedErrors = [];
+ $testErrorMessage = false;
+ foreach ( $consequences as $consequence => $ids ) {
+ foreach ( $ids as $id ) {
+ $params = self::$filters[$id]['actions'][$consequence];
+ $success = true;
+ switch ( $consequence ) {
+ case 'warn':
+ // Aborts the hook with the warning message as error.
+ $expectedErrors['warn'][] = $params[0];
+ break;
+ case 'disallow':
+ // Aborts the hook with 'abusefilter-disallowed' error.
+ $expectedErrors['disallow'][] = 'abusefilter-disallowed';
+ break;
+ case 'block':
+ // Aborts the hook with 'abusefilter-blocked-display' error. Should block
+ // the user with expected duration and options.
+ $userBlock = self::$mUser->getBlock( false );
+
+ if ( !$userBlock ) {
+ $testErrorMessage = "User isn't blocked.";
+ break;
+ }
+
+ $shouldPreventTalkEdit = $params[0] === 'blocktalk';
+ $edittalkCheck = $userBlock->prevents( 'editownusertalk' ) === $shouldPreventTalkEdit;
+ if ( !$edittalkCheck ) {
+ $testErrorMessage = 'The expected block option "edittalk" options does not ' .
+ 'match the actual one.';
+ break;
+ }
+
+ $expectedExpiry = SpecialBlock::parseExpiryInput( $params[2] );
+ // Get rid of non-numeric 'infinity' by setting it to 0
+ $actualExpiry = wfIsInfinity( $userBlock->getExpiry() ) ? 0 : $userBlock->getExpiry();
+ $expectedExpiry = wfIsInfinity( $expectedExpiry ) ? 0 : $expectedExpiry;
+ // We need to take into account code execution time. 10 seconds should be enough
+ $durationCheck = abs( strtotime( $actualExpiry ) - strtotime( $expectedExpiry ) ) < 10;
+ if ( !$durationCheck ) {
+ $testErrorMessage = "The expected block expiry ($expectedExpiry) does not " .
+ "match the actual one ($actualExpiry).";
+ break;
+ }
+
+ $expectedErrors['block'][] = 'abusefilter-blocked-display';
+ break;
+ case 'degroup':
+ // Aborts the hook with 'abusefilter-degrouped' error and degroups the user.
+ $expectedErrors['degroup'][] = 'abusefilter-degrouped';
+ $groupCheck = !in_array( 'sysop', self::$mUser->getEffectiveGroups() );
+ if ( !$groupCheck ) {
+ $testErrorMessage = 'The user was not degrouped.';
+ }
+ break;
+ case 'tag':
+ // Only add tags, to be retrieved in tag_summary table.
+ if ( $actionParams[1] === null ) {
+ // It's an account creation, so no tags.
+ break;
+ }
+ $title = Title::newFromText( $actionParams[1] );
+ $page = WikiPage::factory( $title );
+ $revId = $page->getLatest();
+ $dbr = wfGetDB( DB_REPLICA );
+ $appliedTags = $dbr->selectField(
+ 'tag_summary',
+ 'ts_tags',
+ [ 'ts_rev_id' => $revId ],
+ __METHOD__
+ );
+ $appliedTags = explode( ',', $appliedTags );
+
+ $tagCheck = count( array_diff( $params, $appliedTags ) ) === 0;
+ if ( !$tagCheck ) {
+ $expectedTags = $wgLang->commaList( $params );
+ $actualTags = $wgLang->commaList( $appliedTags );
+
+ $testErrorMessage = "Expected the edit to have the following tags: $expectedTags. " .
+ "Got the following instead: $actualTags.";
+ }
+ break;
+ case 'throttle':
+ // The action was executed twice and $result is an array of two Status objects.
+ if ( !$result[0]->isGood() ) {
+ // The first one should be fine
+ $testErrorMessage = "The first edit should have been saved, being only throttled.";
+ break;
+ }
+
+ $result = $result[1];
+ break;
+ }
+
+ if ( $testErrorMessage ) {
+ $this->fail( "$testErrorMessage Test description: $testDescription" );
+ }
+ }
+ }
+
+ if ( in_array( 'hitThrottleLimit', $options ) ) {
+ $dbr = wfGetDB( DB_REPLICA );
+ $throttled = true;
+ foreach ( $createIds as $filter ) {
+ $curThrottle = $dbr->selectField(
+ 'abuse_filter',
+ 'af_throttled',
+ [ 'af_id' => $filter ],
+ __METHOD__
+ );
+ $throttled &= $curThrottle;
+ }
+
+ if ( !$throttled ) {
+ $expectedThrottled = $wgLang->commaList( $createIds );
+ $this->fail( 'Expected the following filters to be automatically ' .
+ "throttled: $expectedThrottled." );
+ }
+ }
+
+ // Errors have a priority order
+ $expected = $expectedErrors['warn'] ?? $expectedErrors['degroup'] ??
+ $expectedErrors['block'] ?? $expectedErrors['disallow'] ?? null;
+ if ( isset( $expectedErrors['degroup'] ) && $expected === $expectedErrors['degroup'] &&
+ isset( $expectedErrors['block'] ) ) {
+ // Degroup and block warning can be fired together
+ $expected = array_merge( $expectedErrors['degroup'], $expectedErrors['block'] );
+ } elseif ( !is_array( $expected ) ) {
+ $expected = (array)$expected;
+ }
+
+ $errors = $result->getErrors();
+
+ $actual = [];
+ foreach ( $errors as $error ) {
+ $msg = $error['message'];
+ if ( strpos( $msg, 'abusefilter' ) !== false ) {
+ $actual[] = $msg;
+ }
+ }
+
+ $expectedDisplay = $wgLang->commaList( $expected );
+ $actualDisplay = $wgLang->commaList( $actual );
+
+ $this->assertEquals(
+ $expected,
+ $actual,
+ "The edit should have returned the following error messages: $expectedDisplay. " .
+ "Got $actualDisplay instead. Test description: $testDescription"
+ );
+ }
+
+ /**
+ * Data provider for creating and editing filters. For every test case, we pass
+ * - an array with the IDs of the filters to be created (listed in self::$filters),
+ * - an array with details of the action to execute in order to trigger the filters,
+ * - an array of expected consequences of the form
+ * [ 'consequence name' => [ IDs of the filter to take its parameters from ] ]
+ * Such IDs may be more than one if we have a warning that is shown twice.
+ * - an array with further options for testing
+ *
+ * @return array
+ */
+ public function provideFilters() {
+ return [
+ [
+ 'Basic test for "edit" action.',
+ [ 1, 2 ],
+ [
+ 'edit',
+ 'Test page',
+ 'oldText' => 'Some old text for the test.',
+ 'newText' => 'I like foo',
+ 'summary' => 'Test AbuseFilter for edit action.'
+ ],
+ [ 'warn' => [ 1 ] ],
+ []
+ ],
+ [
+ 'Basic test for "move" action.',
+ [ 2 ],
+ [
+ 'move',
+ 'Test page',
+ 'newTitle' => 'Another test page'
+ ],
+ [ 'disallow' => [ 2 ], 'block' => [ 2 ] ],
+ []
+ ],
+ [
+ 'Basic test for "delete" action.',
+ [ 2, 3 ],
+ [
+ 'delete',
+ 'Test page'
+ ],
+ [ 'degroup' => [ 3 ] ],
+ []
+ ],
+ [
+ 'Basic test for "createaccount" action.',
+ [ 1, 2, 3, 4 ],
+ [
+ 'createaccount',
+ null,
+ 'username' => 'AnotherUser'
+ ],
+ [],
+ []
+ ],
+ [
+ 'Test to check that all tags are applied.',
+ [ 5 ],
+ [
+ 'edit',
+ 'User:FilteredUser',
+ 'oldText' => 'Hey.',
+ 'newText' => 'I am a very nice user, really!',
+ 'summary' => ''
+ ],
+ [ 'tag' => [ 5 ] ],
+ []
+ ],
+ [
+ 'Test to check that the edit is disallowed.',
+ [ 6 ],
+ [
+ 'edit',
+ 'Help:Help',
+ 'oldText' => 'Some help.',
+ 'newText' => 'Some help for you',
+ 'summary' => 'Help! I need somebody'
+ ],
+ [ 'disallow' => [ 6 ] ],
+ []
+ ],
+ [
+ 'Test to check that degroup and block are executed together.',
+ [ 2, 3, 7, 8 ],
+ [
+ 'edit',
+ 'Link',
+ 'oldText' => 'What is a link?',
+ 'newText' => 'A link is something like this: [[Link|]].',
+ 'summary' => 'Explaining'
+ ],
+ [ 'degroup' => [ 7 ], 'block' => [ 8 ] ],
+ []
+ ],
+ [
+ 'Test to check that the block duration is the longest one.',
+ [ 8, 9 ],
+ [
+ 'edit',
+ 'Whatever',
+ 'oldText' => 'Whatever is whatever',
+ 'newText' => 'Whatever is whatever, whatever it is. BTW, here is a [[Link|]]',
+ 'summary' => 'Whatever'
+ ],
+ [ 'disallow' => [ 8 ], 'block' => [ 8 ] ],
+ []
+ ],
+ [
+ 'Test to check that throttled filters only execute "safe" actions.',
+ [ 10 ],
+ [
+ 'edit',
+ 'Buffalo',
+ 'oldText' => 'Buffalo',
+ 'newText' => 'Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo.',
+ 'summary' => 'Buffalo!'
+ ],
+ [ 'tag' => [ 10 ] ],
+ []
+ ],
+ [
+ 'Test to see that throttling works well.',
+ [ 11 ],
+ [
+ 'edit',
+ 'Throttle',
+ 'oldText' => 'What is throttle?',
+ 'firstNewText' => 'Throttle is something that should happen...',
+ 'secondNewText' => '... Right now!',
+ 'summary' => 'Throttle'
+ ],
+ [ 'throttle' => [ 11 ], 'disallow' => [ 11 ] ],
+ [ 'makeGoodEditFirst' ]
+ ],
+ [
+ 'Test to check that degroup and block are both executed and degroup warning is shown twice.',
+ [ 1, 3, 7, 12 ],
+ [
+ 'edit',
+ 'User:FilteredUser',
+ 'oldText' => '',
+ 'newText' => 'A couple of lines about me...',
+ 'summary' => 'My user page'
+ ],
+ [ 'block' => [ 12 ], 'degroup' => [ 7, 12 ] ],
+ []
+ ],
+ [
+ 'Test to check that every throttled filter only executes "safe" actions.',
+ [ 10, 13 ],
+ [
+ 'edit',
+ 'Tyger! Tyger! Burning bright',
+ 'oldText' => 'In the forests of the night',
+ 'newText' => 'What immortal hand or eye',
+ 'summary' => 'Could frame thy fearful symmetry?'
+ ],
+ [ 'tag' => [ 10 ] ],
+ []
+ ],
+ [
+ 'Test to check that runtime exceptions (division by zero) are correctly handled.',
+ [ 14 ],
+ [
+ 'edit',
+ '0',
+ 'oldText' => 'Old text',
+ 'newText' => 'New text',
+ 'summary' => 'Some summary'
+ ],
+ [],
+ []
+ ],
+ [
+ 'Test to check that the conditions limit works.',
+ [ 8, 10 ],
+ [
+ 'edit',
+ 'Anything',
+ 'oldText' => 'Bar',
+ 'newText' => 'Foo',
+ 'summary' => ''
+ ],
+ [],
+ [ 'hitCondsLimit' ]
+ ],
+ [
+ 'Test slow executions.',
+ [ 7, 12 ],
+ [
+ 'edit',
+ 'Something',
+ 'oldText' => 'Please allow me',
+ 'newText' => 'to introduce myself',
+ 'summary' => ''
+ ],
+ [ 'degroup' => [ 7 ] ],
+ [ 'hitTimeLimit' ]
+ ],
+ [
+ 'Test throttling a dangerous filter.',
+ [ 13 ],
+ [
+ 'edit',
+ 'My page',
+ 'oldText' => '',
+ 'newText' => 'AbuseFilter will not block me',
+ 'summary' => ''
+ ],
+ [],
+ [ 'hitThrottleLimit' ]
+ ],
+ ];
+ }
+}
diff --git a/AbuseFilter/tests/phpunit/AbuseFilterParserTest.php b/AbuseFilter/tests/phpunit/AbuseFilterParserTest.php
new file mode 100644
index 00000000..747e500a
--- /dev/null
+++ b/AbuseFilter/tests/phpunit/AbuseFilterParserTest.php
@@ -0,0 +1,744 @@
+<?php
+/**
+ * Tests for the AbuseFilter parser
+ *
+ * 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 >
+ */
+
+/**
+ * @group Test
+ * @group AbuseFilter
+ * @group AbuseFilterParser
+ *
+ * @covers AbuseFilterCachingParser
+ * @covers AFPTreeParser
+ * @covers AFPTreeNode
+ * @covers AFPParserState
+ * @covers AbuseFilterParser
+ * @covers AbuseFilterTokenizer
+ * @covers AFPToken
+ * @covers AFPUserVisibleException
+ * @covers AFPException
+ * @covers AFPData
+ * @covers AbuseFilterVariableHolder
+ * @covers AFComputedVariable
+ */
+class AbuseFilterParserTest extends MediaWikiTestCase {
+ /**
+ * @return AbuseFilterParser
+ */
+ public static function getParser() {
+ static $parser = null;
+ if ( !$parser ) {
+ $parser = new AbuseFilterParser();
+ } else {
+ $parser->resetState();
+ }
+ return $parser;
+ }
+
+ /**
+ * @return AbuseFilterParser[]
+ */
+ public static function getParsers() {
+ static $parsers = null;
+ if ( !$parsers ) {
+ $parsers = [
+ new AbuseFilterParser()
+ // @ToDo: Here we should also instantiate an AbuseFilterCachingParser as we'll have
+ // fixed its problems (T156095). Right now it may break otherwise working tests (see T201193)
+ ];
+ }
+ return $parsers;
+ }
+
+ /**
+ * @dataProvider readTests
+ */
+ public function testParser( $testName, $rule, $expected ) {
+ foreach ( self::getParsers() as $parser ) {
+ $actual = $parser->parse( $rule );
+ $this->assertEquals( $expected, $actual, 'Running parser test ' . $testName );
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function readTests() {
+ $tests = [];
+ $testPath = __DIR__ . "/../parserTests";
+ $testFiles = glob( $testPath . "/*.t" );
+
+ foreach ( $testFiles as $testFile ) {
+ $testName = substr( $testFile, 0, -2 );
+
+ $resultFile = $testName . '.r';
+ $rule = trim( file_get_contents( $testFile ) );
+ $result = trim( file_get_contents( $resultFile ) ) === 'MATCH';
+
+ $tests[] = [
+ basename( $testName ),
+ $rule,
+ $result
+ ];
+ }
+
+ return $tests;
+ }
+
+ /**
+ * Test expression evaluation
+ *
+ * @dataProvider provideExpressions
+ */
+ public function testEvaluateExpression( $expr, $expected ) {
+ foreach ( self::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' ]
+ ];
+ }
+
+ /**
+ * Ensure that AbuseFilterTokenizer::OPERATOR_RE matches the contents
+ * and order of AbuseFilterTokenizer::$operators.
+ */
+ public function testOperatorRe() {
+ $operatorRe = '/(' . implode( '|', array_map( function ( $op ) {
+ return preg_quote( $op, '/' );
+ }, AbuseFilterTokenizer::$operators ) ) . ')/A';
+ $this->assertEquals( $operatorRe, AbuseFilterTokenizer::OPERATOR_RE );
+ }
+
+ /**
+ * Ensure that AbuseFilterTokenizer::RADIX_RE matches the contents
+ * and order of AbuseFilterTokenizer::$bases.
+ */
+ public function testRadixRe() {
+ $baseClass = implode( '', array_keys( AbuseFilterTokenizer::$bases ) );
+ $radixRe = "/([0-9A-Fa-f]+(?:\.\d*)?|\.\d+)([$baseClass])?/Au";
+ $this->assertEquals( $radixRe, AbuseFilterTokenizer::RADIX_RE );
+ }
+
+ /**
+ * Ensure the number of conditions counted for given expressions is right.
+ *
+ * @dataProvider condCountCases
+ */
+ public function testCondCount( $rule, $expected ) {
+ $parser = self::getParser();
+ $countBefore = AbuseFilter::$condCount;
+ $parser->parse( $rule );
+ $countAfter = AbuseFilter::$condCount;
+ $actual = $countAfter - $countBefore;
+ $this->assertEquals( $expected, $actual, 'Condition count for ' . $rule );
+ }
+
+ /**
+ * Data provider for testCondCount method.
+ * @return array
+ */
+ public function condCountCases() {
+ return [
+ [ '((("a" == "b")))', 1 ],
+ [ 'contains_any("a", "b", "c")', 1 ],
+ [ '"a" == "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 ],
+ ];
+ }
+
+ /**
+ * Ensure get_matches function captures returns expected output.
+ * @param string $needle Regex to pass to get_matches.
+ * @param string $haystack String to run regex against.
+ * @param string[] $expected The expected values of the matched groups.
+ * @covers AbuseFilterParser::funcGetMatches
+ * @dataProvider getMatchesCases
+ */
+ public function testGetMatches( $needle, $haystack, $expected ) {
+ $parser = self::getParser();
+ $afpData = $parser->intEval( "get_matches('$needle', '$haystack')" )->data;
+
+ // Extract matches from AFPData.
+ $matches = array_map( function ( $afpDatum ) {
+ return $afpDatum->data;
+ }, $afpData );
+
+ $this->assertEquals( $expected, $matches );
+ }
+
+ /**
+ * Data provider for get_matches method.
+ * @return array
+ */
+ public function getMatchesCases() {
+ return [
+ [
+ 'You say (.*) \(and I say (.*)\)\.',
+ 'You say hello (and I say goodbye).',
+ [
+ 'You say hello (and I say goodbye).',
+ 'hello',
+ 'goodbye',
+ ],
+ ],
+ [
+ 'I(?: am)? the ((walrus|egg man).*)\!',
+ 'I am the egg man, I am the walrus !',
+ [
+ 'I am the egg man, I am the walrus !',
+ 'egg man, I am the walrus ',
+ 'egg man',
+ ],
+ ],
+ [
+ 'this (does) not match',
+ 'foo bar',
+ [
+ false,
+ false,
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Base method for testing exceptions
+ *
+ * @param string $excep Identifier of the exception (e.g. 'unexpectedtoken')
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ */
+ private function exceptionTest( $excep, $expr, $caller ) {
+ $parser = self::getParser();
+ try {
+ $parser->parse( $expr );
+ } catch ( AFPUserVisibleException $e ) {
+ $this->assertEquals(
+ $excep,
+ $e->mExceptionID,
+ "Exception $excep not thrown in AbuseFilterParser::$caller"
+ );
+ return;
+ }
+
+ $this->fail( "Exception $excep not thrown in AbuseFilterParser::$caller" );
+ }
+
+ /**
+ * Test the 'expectednotfound' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @covers AbuseFilterParser::doLevelSet
+ * @covers AbuseFilterParser::doLevelConditions
+ * @covers AbuseFilterParser::doLevelBraces
+ * @covers AbuseFilterParser::doLevelFunction
+ * @covers AbuseFilterParser::doLevelAtom
+ * @covers AbuseFilterParser::skipOverBraces
+ * @covers AbuseFilterParser::doLevelArrayElements
+ * @dataProvider expectedNotFound
+ */
+ public function testExpectedNotFoundException( $expr, $caller ) {
+ $this->exceptionTest( '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 | (', 'skipOverBraces' ],
+ [ 'a := [1,2,3]; 3 = a[5', 'doLevelArrayElements' ],
+ ];
+ }
+
+ /**
+ * Test the 'unexpectedatend' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @covers AbuseFilterParser::doLevelEntry
+ * @dataProvider unexpectedAtEnd
+ */
+ public function testUnexpectedAtEndException( $expr, $caller ) {
+ $this->exceptionTest( '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
+ * @covers AbuseFilterParser::doLevelSet
+ * @covers AbuseFilterParser::getVarValue
+ * @dataProvider unrecognisedVar
+ */
+ public function testUnrecognisedVarException( $expr, $caller ) {
+ $this->exceptionTest( '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', 'doLevelSet' ],
+ [ 'a = 5', 'getVarValue' ],
+ ];
+ }
+
+ /**
+ * Test the 'notarray' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @covers AbuseFilterParser::doLevelSet
+ * @covers AbuseFilterParser::doLevelArrayElements
+ * @dataProvider notArray
+ */
+ public function testNotArrayException( $expr, $caller ) {
+ $this->exceptionTest( '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' ],
+ ];
+ }
+
+ /**
+ * Test the 'outofbounds' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @covers AbuseFilterParser::doLevelSet
+ * @covers AbuseFilterParser::doLevelArrayElements
+ * @dataProvider outOfBounds
+ */
+ public function testOutOfBoundsException( $expr, $caller ) {
+ $this->exceptionTest( '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' ],
+ ];
+ }
+
+ /**
+ * Test the 'unrecognisedkeyword' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @covers AbuseFilterParser::doLevelAtom
+ * @dataProvider unrecognisedKeyword
+ */
+ public function testUnrecognisedKeywordException( $expr, $caller ) {
+ $this->exceptionTest( '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' ],
+ ];
+ }
+
+ /**
+ * Test the 'unexpectedtoken' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @covers AbuseFilterParser::doLevelAtom
+ * @dataProvider unexpectedToken
+ */
+ public function testUnexpectedTokenException( $expr, $caller ) {
+ $this->exceptionTest( '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
+ * @covers AbuseFilterParser::getVarValue
+ * @dataProvider disabledVar
+ */
+ public function testDisabledVarException( $expr, $caller ) {
+ $this->exceptionTest( '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 'overridebuiltin' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @covers AbuseFilterParser::setUserVariable
+ * @dataProvider overrideBuiltin
+ */
+ public function testOverrideBuiltinException( $expr, $caller ) {
+ $this->exceptionTest( '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' ],
+ ];
+ }
+
+ /**
+ * Test the 'regexfailure' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @covers AbuseFilterParser::funcRCount
+ * @covers AbuseFilterParser::funcGetMatches
+ * @dataProvider regexFailure
+ */
+ public function testRegexFailureException( $expr, $caller ) {
+ $this->exceptionTest( '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' ],
+ ];
+ }
+
+ /**
+ * Test the 'invalidiprange' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @covers AbuseFilterParser::funcIPInRange
+ * @dataProvider invalidIPRange
+ */
+ public function testInvalidIPRangeException( $expr, $caller ) {
+ $this->exceptionTest( '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
+ * @covers AbuseFilterParser::funcLc
+ * @covers AbuseFilterParser::funcUc
+ * @covers AbuseFilterParser::funcLen
+ * @covers AbuseFilterParser::funcSpecialRatio
+ * @covers AbuseFilterParser::funcCount
+ * @covers AbuseFilterParser::funcRCount
+ * @covers AbuseFilterParser::funcCCNorm
+ * @covers AbuseFilterParser::funcSanitize
+ * @covers AbuseFilterParser::funcRMSpecials
+ * @covers AbuseFilterParser::funcRMWhitespace
+ * @covers AbuseFilterParser::funcRMDoubles
+ * @covers AbuseFilterParser::funcNorm
+ * @covers AbuseFilterParser::funcStrRegexEscape
+ * @covers AbuseFilterParser::castString
+ * @covers AbuseFilterParser::castInt
+ * @covers AbuseFilterParser::castFloat
+ * @covers AbuseFilterParser::castBool
+ * @dataProvider oneParamFuncs
+ * @expectedException AFPUserVisibleException
+ * @expectedExceptionMessageRegExp /^No parameters given to function/
+ */
+ public function testNoParamsException( $func ) {
+ $parser = self::getParser();
+ $parser->parse( "$func()" );
+ }
+
+ /**
+ * 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
+ * @covers AbuseFilterParser::funcGetMatches
+ * @covers AbuseFilterParser::funcIPInRange
+ * @covers AbuseFilterParser::funcContainsAny
+ * @covers AbuseFilterParser::funcContainsAll
+ * @covers AbuseFilterParser::funcCCNormContainsAny
+ * @covers AbuseFilterParser::funcCCNormContainsAll
+ * @covers AbuseFilterParser::funcEqualsToAny
+ * @covers AbuseFilterParser::funcSubstr
+ * @covers AbuseFilterParser::funcStrPos
+ * @covers AbuseFilterParser::funcSetVar
+ * @dataProvider twoParamsFuncs
+ * @expectedException AFPUserVisibleException
+ * @expectedExceptionMessageRegExp /^Not enough arguments to function [^ ]+ called at character \d+.\nExpected 2 arguments, got 1/
+ */
+ public function testNotEnoughArgsExceptionTwo( $func ) {
+ $parser = self::getParser();
+ // Nevermind if the argument can't be string since we check the amount
+ // of parameters before anything else.
+ $parser->parse( "$func('foo')" );
+ }
+
+ /**
+ * 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
+ * @covers AbuseFilterParser::funcStrReplace
+ * @dataProvider threeParamsFuncs
+ * @expectedException AFPUserVisibleException
+ * @expectedExceptionMessageRegExp /^Not enough arguments to function [^ ]+ called at character \d+.\nExpected 3 arguments, got 2/
+ */
+ public function testNotEnoughArgsExceptionThree( $func ) {
+ $parser = self::getParser();
+ // Nevermind if the argument can't be string since we check the amount
+ // of parameters before anything else.
+ $parser->parse( "$func('foo', 'bar')" );
+ }
+
+ /**
+ * Data provider for testNotEnoughArgsExceptionThree, returns the list of
+ * functions taking three parameters.
+ *
+ * @return array
+ */
+ public function threeParamsFuncs() {
+ return [
+ [ 'str_replace' ],
+ ];
+ }
+
+ /**
+ * 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
+ */
+ public function testDeprecatedVars( $old, $new ) {
+ $loggerMock = new TestLogger();
+ $loggerMock->setCollect( true );
+ $this->setLogger( 'AbuseFilterDeprecatedVars', $loggerMock );
+
+ $parser = self::getParser();
+ $actual = $parser->parse( "$old === $new" );
+
+ $loggerBuffer = $loggerMock->getBuffer();
+ // Check that the use has been logged
+ $found = false;
+ foreach ( $loggerBuffer as $entry ) {
+ $check = preg_match( '/AbuseFilter: deprecated variable/', $entry[1] );
+ if ( $check ) {
+ $found = true;
+ break;
+ }
+ }
+ if ( !$found ) {
+ $this->fail( "The use of the deprecated variable $old was not logged." );
+ }
+
+ $this->assertTrue( $actual, "AbuseFilter deprecated variable $old is not parsed correctly" );
+ }
+
+ /**
+ * Data provider for testDeprecatedVars
+ * @return array
+ */
+ public function provideDeprecatedVars() {
+ $deprecated = AbuseFilter::$deprecatedVars;
+ $data = [];
+ foreach ( $deprecated as $old => $new ) {
+ $data[] = [ $old, $new ];
+ }
+ return $data;
+ }
+}
diff --git a/AbuseFilter/tests/phpunit/AbuseFilterSaveTest.php b/AbuseFilter/tests/phpunit/AbuseFilterSaveTest.php
new file mode 100644
index 00000000..6a5b3121
--- /dev/null
+++ b/AbuseFilter/tests/phpunit/AbuseFilterSaveTest.php
@@ -0,0 +1,596 @@
+<?php
+/**
+ * Tests for validating and saving a filter
+ *
+ * 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
+ */
+
+/**
+ * @group Test
+ * @group AbuseFilter
+ * @group AbuseFilterSave
+ * @group Database
+ *
+ * @covers AbuseFilter
+ * @covers AbuseFilterViewEdit
+ * @covers AbuseFilterParser
+ */
+class AbuseFilterSaveTest extends MediaWikiTestCase {
+ protected static $mUser, $mParameters;
+
+ /**
+ * @var array This tables will be deleted in parent::tearDown
+ */
+ protected $tablesUsed = [
+ 'abuse_filter',
+ 'abuse_filter_action',
+ 'abuse_filter_history',
+ 'abuse_filter_log'
+ ];
+
+ /**
+ * @see MediaWikiTestCase::setUp
+ */
+ protected function setUp() {
+ parent::setUp();
+ $user = User::newFromName( 'FilterTester' );
+ $user->addToDatabase();
+ $user->addGroup( 'filterEditor' );
+ RequestContext::getMain()->setUser( $user );
+ self::$mUser = $user;
+ // Make sure that the config we're using is the one we're expecting
+ $this->setMwGlobals( [
+ 'wgUser' => $user,
+ 'wgAbuseFilterRestrictions' => [
+ 'degroup' => true
+ ],
+ 'wgAbuseFilterIsCentral' => true,
+ 'wgAbuseFilterActions' => [
+ 'throttle' => true,
+ 'warn' => true,
+ 'disallow' => true,
+ 'blockautopromote' => true,
+ 'block' => true,
+ 'rangeblock' => true,
+ 'degroup' => true,
+ 'tag' => true
+ ],
+ 'wgAbuseFilterValidGroups' => [
+ 'default',
+ 'flow'
+ ]
+ ] );
+ $this->setGroupPermissions( [
+ 'filterEditor' => [
+ 'abusefilter-modify' => true,
+ 'abusefilter-modify-restricted' => false,
+ 'abusefilter-modify-global' => false,
+ ],
+ 'filterEditorGlobal' => [
+ 'abusefilter-modify' => true,
+ 'abusefilter-modify-global' => true,
+ ]
+ ] );
+ }
+
+ /**
+ * Gets an instance of AbuseFilterViewEdit ready for creating or editing filter
+ *
+ * @param string $filter 'new' for a new filter, its ID otherwise
+ * @return AbuseFilterViewEdit
+ */
+ private static function getViewEdit( $filter ) {
+ $special = new SpecialAbuseFilter();
+ $context = RequestContext::getMain( self::getRequest() );
+ $context->setRequest( self::getRequest() );
+
+ $special->setContext( $context );
+ $special->mFilter = $filter;
+ $viewEdit = new AbuseFilterViewEdit( $special, [ $filter ] );
+ // Being a static property, it's not deleted between tests
+ $viewEdit::$mLoadedRow = null;
+
+ return $viewEdit;
+ }
+
+ /**
+ * Creates a FauxRequest object
+ *
+ * @return FauxRequest
+ */
+ private static function getRequest() {
+ $params = [
+ 'wpFilterRules' => self::$mParameters['rules'],
+ 'wpFilterDescription' => self::$mParameters['description'],
+ 'wpFilterNotes' => self::$mParameters['notes'],
+ 'wpFilterGroup' => self::$mParameters['group'],
+ 'wpFilterEnabled' => self::$mParameters['enabled'],
+ 'wpFilterHidden' => self::$mParameters['hidden'],
+ 'wpFilterDeleted' => self::$mParameters['deleted'],
+ 'wpFilterGlobal' => self::$mParameters['global'],
+ 'wpFilterActionThrottle' => self::$mParameters['throttleEnabled'],
+ 'wpFilterThrottleCount' => self::$mParameters['throttleCount'],
+ 'wpFilterThrottlePeriod' => self::$mParameters['throttlePeriod'],
+ 'wpFilterThrottleGroups' => self::$mParameters['throttleGroups'],
+ 'wpFilterActionWarn' => self::$mParameters['warnEnabled'],
+ 'wpFilterWarnMessage' => self::$mParameters['warnMessage'],
+ 'wpFilterWarnMessageOther' => self::$mParameters['warnMessageOther'],
+ 'wpFilterActionDisallow' => self::$mParameters['disallowEnabled'],
+ 'wpFilterActionBlockautopromote' => self::$mParameters['blockautopromoteEnabled'],
+ 'wpFilterActionDegroup' => self::$mParameters['degroupEnabled'],
+ 'wpFilterActionBlock' => self::$mParameters['blockEnabled'],
+ 'wpFilterBlockTalk' => self::$mParameters['blockTalk'],
+ 'wpBlockAnonDuration' => self::$mParameters['blockAnons'],
+ 'wpBlockUserDuration' => self::$mParameters['blockUsers'],
+ 'wpFilterActionRangeblock' => self::$mParameters['rangeblockEnabled'],
+ 'wpFilterActionTag' => self::$mParameters['tagEnabled'],
+ 'wpFilterTags' => self::$mParameters['tagTags'],
+ ];
+
+ // Checkboxes aren't included at all if they aren't selected. We can remove them
+ // this way (instead of iterating a hardcoded list) since they're the only false values
+ $params = array_filter( $params, function ( $el ) {
+ return $el !== false;
+ } );
+
+ $request = new FauxRequest( $params, true );
+ return $request;
+ }
+
+ /**
+ * Creates $amount new filters, in case we need to test updating an existing one
+ *
+ * @param int $amount How many filters to create
+ */
+ private static function createNewFilters( $amount ) {
+ $defaultRow = [
+ 'af_pattern' => '/**/',
+ 'af_user' => 0,
+ 'af_user_text' => 'FilterTester',
+ 'af_timestamp' => wfTimestampNow(),
+ 'af_enabled' => 1,
+ 'af_comments' => '',
+ 'af_public_comments' => 'Mock filter',
+ 'af_hidden' => 0,
+ 'af_hit_count' => 0,
+ 'af_throttled' => 0,
+ 'af_deleted' => 0,
+ 'af_actions' => '',
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ];
+
+ $dbw = wfGetDB( DB_MASTER );
+ for ( $i = 1; $i <= $amount; $i++ ) {
+ $dbw->replace(
+ 'abuse_filter',
+ [ 'af_id' ],
+ $defaultRow,
+ __METHOD__
+ );
+ }
+ }
+
+ /**
+ * Validate and save a filter given its parameters
+ *
+ * @param array $args Parameters of the filter and metadata for the test
+ * @covers AbuseFilter::saveFilter
+ * @dataProvider provideFilters
+ */
+ public function testSaveFilter( $args ) {
+ // Preliminar stuff for the test
+ if ( $args['testData']['customUserGroup'] ) {
+ self::$mUser->addGroup( $args['testData']['customUserGroup'] );
+ }
+
+ if ( $args['testData']['needsOtherFilters'] ) {
+ self::createNewFilters( $args['testData']['needsOtherFilters'] );
+ }
+
+ $fixedParameters = [
+ 'id' => 'new',
+ 'notes' => '',
+ 'group' => 'default',
+ 'enabled' => true,
+ 'hidden' => false,
+ 'global' => false,
+ 'deleted' => false,
+ 'throttled' => 0,
+ 'throttleEnabled' => false,
+ 'throttleCount' => 0,
+ 'throttlePeriod' => 0,
+ 'throttleGroups' => '',
+ 'warnEnabled' => false,
+ 'warnMessage' => 'abusefilter-warning',
+ 'warnMessageOther' => '',
+ 'disallowEnabled' => false,
+ 'blockautopromoteEnabled' => false,
+ 'degroupEnabled' => false,
+ 'blockEnabled' => false,
+ 'blockTalk' => false,
+ 'blockAnons' => 'infinity',
+ 'blockUsers' => 'infinity',
+ 'rangeblockEnabled' => false,
+ 'tagEnabled' => false,
+ 'tagTags' => ''
+ ];
+
+ // Extract parameters from testset and build what we need to save a filter
+ // The values specified in the testset will overwrite the fixed ones.
+ self::$mParameters = $args['filterParameters'] + $fixedParameters;
+ $filter = self::$mParameters['id'];
+ $viewEdit = self::getViewEdit( $filter );
+ $request = self::getRequest();
+ list( $newRow, $actions ) = $viewEdit->loadRequest( $filter );
+ self::$mParameters['rowActions'] = implode( ',', array_keys( array_filter( $actions ) ) );
+
+ // Send data for validation and saving
+ $status = AbuseFilter::saveFilter( $viewEdit, $filter, $request, $newRow, $actions );
+
+ // Must be removed for the next test
+ if ( $args['testData']['customUserGroup'] ) {
+ self::$mUser->removeGroup( $args['testData']['customUserGroup'] );
+ }
+
+ $shouldFail = $args['testData']['shouldFail'];
+ $shouldBeSaved = $args['testData']['shouldBeSaved'];
+ $furtherInfo = null;
+ $expected = true;
+ if ( $shouldFail ) {
+ if ( $status->isGood() ) {
+ $furtherInfo = 'The filter validation returned a valid status.';
+ $result = false;
+ } else {
+ $result = $status->getErrors();
+ $result = $result[0]['message'];
+ $expected = $args['testData']['expectedMessage'];
+ }
+ } else {
+ if ( $shouldBeSaved ) {
+ $value = $status->getValue();
+ $result = $status->isGood() && is_array( $value ) && count( $value ) === 2 &&
+ is_numeric( $value[0] ) && is_numeric( $value[1] );
+ } else {
+ $result = $status->isGood() && $status->getValue() === false;
+ }
+ }
+
+ $errorMessage = $args['testData']['doingWhat'] . '. Expected: ' .
+ $args['testData']['expectedResult'] . '.';
+ if ( $furtherInfo ) {
+ $errorMessage .= "\nFurther info: " . $furtherInfo;
+ }
+ $this->assertEquals(
+ $expected,
+ $result,
+ $errorMessage
+ );
+ }
+
+ /**
+ * Data provider for creating and editing filters.
+ * @return array
+ */
+ public function provideFilters() {
+ return [
+ [
+ [
+ 'filterParameters' => [
+ 'rules' => '',
+ 'description' => '',
+ 'blockautopromoteEnabled' => true,
+ ],
+ 'testData' => [
+ 'doingWhat' => 'Trying to save a filter without description and rules',
+ 'expectedResult' => 'a "missing required fields" error message',
+ 'expectedMessage' => 'abusefilter-edit-missingfields',
+ 'shouldFail' => true,
+ 'shouldBeSaved' => false,
+ 'customUserGroup' => '',
+ 'needsOtherFilters' => false
+ ]
+ ]
+ ],
+ [
+ [
+ 'filterParameters' => [
+ 'rules' => '/* My rules */',
+ 'description' => 'Some new filter',
+ 'enabled' => false,
+ 'deleted' => true,
+ ],
+ 'testData' => [
+ 'doingWhat' => 'Trying to save a filter with only rules and description',
+ 'expectedResult' => 'the saving to be successful',
+ 'expectedMessage' => '',
+ 'shouldFail' => false,
+ 'shouldBeSaved' => true,
+ 'customUserGroup' => '',
+ 'needsOtherFilters' => false
+ ]
+ ]
+ ],
+ [
+ [
+ 'filterParameters' => [
+ 'rules' => 'rlike',
+ 'description' => 'This syntax aint good',
+ 'blockEnabled' => true,
+ 'blockTalk' => true,
+ 'blockAnons' => '8 hours',
+ ],
+ 'testData' => [
+ 'doingWhat' => 'Trying to save a filter with wrong syntax',
+ 'expectedResult' => 'a "wrong syntax" error message',
+ 'expectedMessage' => 'abusefilter-edit-badsyntax',
+ 'shouldFail' => true,
+ 'shouldBeSaved' => false,
+ 'customUserGroup' => '',
+ 'needsOtherFilters' => false
+ ]
+ ]
+ ],
+ [
+ [
+ 'filterParameters' => [
+ 'rules' => '1==1',
+ 'description' => 'Enabled and deleted',
+ 'deleted' => true,
+ 'blockEnabled' => true,
+ 'blockTalk' => true,
+ 'blockAnons' => '8 hours',
+ ],
+ 'testData' => [
+ 'doingWhat' => 'Trying to save a filter marking it both enabled and deleted',
+ 'expectedResult' => 'an error message',
+ 'expectedMessage' => 'abusefilter-edit-deleting-enabled',
+ 'shouldFail' => true,
+ 'shouldBeSaved' => false,
+ 'customUserGroup' => '',
+ 'needsOtherFilters' => false
+ ]
+ ]
+ ],
+ [
+ [
+ 'filterParameters' => [
+ 'rules' => '1==1',
+ 'description' => 'Reserved tag',
+ 'notes' => 'Some notes',
+ 'hidden' => true,
+ 'tagEnabled' => true,
+ 'tagTags' => 'mw-undo'
+ ],
+ 'testData' => [
+ 'doingWhat' => 'Trying to save a filter with a reserved tag',
+ 'expectedResult' => 'an error message saying that the tag cannot be used',
+ 'expectedMessage' => 'abusefilter-edit-bad-tags',
+ 'shouldFail' => true,
+ 'shouldBeSaved' => false,
+ 'customUserGroup' => '',
+ 'needsOtherFilters' => false
+ ]
+ ]
+ ],
+ [
+ [
+ 'filterParameters' => [
+ 'rules' => '1==1',
+ 'description' => 'Invalid tag',
+ 'notes' => 'Some notes',
+ 'tagEnabled' => true,
+ 'tagTags' => 'some|tag'
+ ],
+ 'testData' => [
+ 'doingWhat' => 'Trying to save a filter with an invalid tag',
+ 'expectedResult' => 'an error message saying that the tag cannot be used',
+ 'expectedMessage' => 'tags-create-invalid-chars',
+ 'shouldFail' => true,
+ 'shouldBeSaved' => false,
+ 'customUserGroup' => '',
+ 'needsOtherFilters' => false
+ ]
+ ]
+ ],
+ [
+ [
+ 'filterParameters' => [
+ 'rules' => '1==1',
+ 'description' => 'Global without perms',
+ 'global' => true,
+ 'disallowEnabled' => true,
+ ],
+ 'testData' => [
+ 'doingWhat' => 'Trying to save a global filter without enough rights',
+ 'expectedResult' => 'an error message saying that I do not have the required rights',
+ 'expectedMessage' => 'abusefilter-edit-notallowed-global',
+ 'shouldFail' => true,
+ 'shouldBeSaved' => false,
+ 'customUserGroup' => '',
+ 'needsOtherFilters' => false
+ ]
+ ]
+ ],
+ [
+ [
+ 'filterParameters' => [
+ 'rules' => '1==1',
+ 'description' => 'Global with invalid warn message',
+ 'global' => true,
+ 'warnEnabled' => true,
+ 'warnMessage' => 'abusefilter-beautiful-warning',
+ ],
+ 'testData' => [
+ 'doingWhat' => 'Trying to save a global filter with a custom warn message',
+ 'expectedResult' => 'an error message saying that custom warn messages ' .
+ 'cannot be used for global rules',
+ 'expectedMessage' => 'abusefilter-edit-notallowed-global-custom-msg',
+ 'shouldFail' => true,
+ 'shouldBeSaved' => false,
+ 'customUserGroup' => 'filterEditorGlobal',
+ 'needsOtherFilters' => false
+ ]
+ ]
+ ],
+ [
+ [
+ 'filterParameters' => [
+ 'rules' => '1==1',
+ 'description' => 'Restricted action',
+ 'degroupEnabled' => true,
+ ],
+ 'testData' => [
+ 'doingWhat' => 'Trying to save a filter with a restricted action',
+ 'expectedResult' => 'an error message saying that the action is restricted',
+ 'expectedMessage' => 'abusefilter-edit-restricted',
+ 'shouldFail' => true,
+ 'shouldBeSaved' => false,
+ 'customUserGroup' => '',
+ 'needsOtherFilters' => false
+ ]
+ ]
+ ],
+ [
+ [
+ 'filterParameters' => [
+ 'id' => '1',
+ 'rules' => '/**/',
+ 'description' => 'Mock filter',
+ ],
+ 'testData' => [
+ 'doingWhat' => 'Trying to save a filter without changing anything',
+ 'expectedResult' => 'the validation to pass without the filter being saved',
+ 'expectedMessage' => '',
+ 'shouldFail' => false,
+ 'shouldBeSaved' => false,
+ 'customUserGroup' => '',
+ 'needsOtherFilters' => 1
+ ]
+ ]
+ ],
+ [
+ [
+ 'filterParameters' => [
+ 'rules' => '1==1',
+ 'description' => 'Invalid throttle groups',
+ 'notes' => 'Throttle... Again',
+ 'throttleEnabled' => true,
+ 'throttleCount' => 11,
+ 'throttlePeriod' => 111,
+ 'throttleGroups' => 'user\nfoo'
+ ],
+ 'testData' => [
+ 'doingWhat' => 'Trying to save a filter with invalid throttle groups',
+ 'expectedResult' => 'an error message saying that throttle groups are invalid',
+ 'expectedMessage' => 'abusefilter-edit-invalid-throttlegroups',
+ 'shouldFail' => true,
+ 'shouldBeSaved' => false,
+ 'customUserGroup' => '',
+ 'needsOtherFilters' => false
+ ]
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * Check that our tag validation is working properly. Note that we only need one test
+ * for each called function. Consistency within ChangeTags functions should be
+ * assured by tests in core. The test for canAddTagsAccompanyingChange and canCreateTag
+ * are missing because they won't actually fail, never. Resolving T173917 would
+ * greatly improve the situation and could help writing better tests.
+ *
+ * @param string $tag The tag to validate
+ * @param string|null $error The expected error message. Null if validations should pass
+ * @covers AbuseFilter::isAllowedTag
+ * @dataProvider provideTags
+ */
+ public function testIsAllowedTag( $tag, $error ) {
+ $status = AbuseFilter::isAllowedTag( $tag );
+
+ if ( !$status->isGood() ) {
+ $actualError = $status->getErrors();
+ $actualError = $actualError[0]['message'];
+ } else {
+ $actualError = null;
+ if ( $error !== null ) {
+ $this->fail( "Tag validation returned a valid status instead of the expected '$error' error." );
+ }
+ }
+
+ $this->assertSame(
+ $error,
+ $actualError,
+ "Expected message '$error', got '$actualError' while validating the tag '$tag'."
+ );
+ }
+
+ /**
+ * Data provider for testIsAllowedTag
+ * @return array
+ */
+ public function provideTags() {
+ return [
+ [ 'a|b', 'tags-create-invalid-chars' ],
+ [ 'mw-undo', 'abusefilter-edit-bad-tags' ],
+ [ 'abusefilter-condition-limit', 'abusefilter-tag-reserved' ],
+ [ 'my_tag', null ],
+ ];
+ }
+
+ /**
+ * Check that throttle parameters validation works fine
+ *
+ * @param array $params Throttle parameters
+ * @param string|null $error The expected error message. Null if validations should pass
+ * @covers AbuseFilter::checkThrottleParameters
+ * @dataProvider provideThrottleParameters
+ */
+ public function testCheckThrottleParameters( $params, $error ) {
+ $result = AbuseFilter::checkThrottleParameters( $params );
+ $this->assertSame( $error, $result, 'Throttle parameter validation does not work as expected.' );
+ }
+
+ /**
+ * Data provider for testCheckThrottleParameters
+ * @return array
+ */
+ public function provideThrottleParameters() {
+ return [
+ [ [ '1', '5,23', 'user', 'ip', 'page,range', 'ip,user', 'range,ip' ], null ],
+ [ [ '1', '5.3,23', 'user', 'ip' ], 'abusefilter-edit-invalid-throttlecount' ],
+ [ [ '1', '-3,23', 'user', 'ip' ], 'abusefilter-edit-invalid-throttlecount' ],
+ [ [ '1', '5,2.3', 'user', 'ip' ], 'abusefilter-edit-invalid-throttleperiod' ],
+ [ [ '1', '4,-14', 'user', 'ip' ], 'abusefilter-edit-invalid-throttleperiod' ],
+ [ [ '1', '3,33' ], 'abusefilter-edit-empty-throttlegroups' ],
+ [ [ '1', '3,33', 'user', 'ip,foo,user' ], 'abusefilter-edit-invalid-throttlegroups' ],
+ [ [ '1', '3,33', 'foo', 'ip,user' ], 'abusefilter-edit-invalid-throttlegroups' ],
+ [ [ '1', '3,33', 'foo', 'ip,user,bar' ], 'abusefilter-edit-invalid-throttlegroups' ],
+ [ [ '1', '3,33', 'user', 'ip,page,user' ], null ],
+ [
+ [ '1', '3,33', 'ip', 'user','user,ip', 'ip,user', 'user,ip,user', 'user', 'ip,ip,user' ],
+ 'abusefilter-edit-duplicated-throttlegroups'
+ ],
+ [ [ '1', '3,33', 'ip,ip,user' ], 'abusefilter-edit-duplicated-throttlegroups' ],
+ [ [ '1', '3,33', 'user,ip', 'ip,user' ], 'abusefilter-edit-duplicated-throttlegroups' ],
+ ];
+ }
+}
diff --git a/AbuseFilter/tests/phpunit/AbuseFilterTest.php b/AbuseFilter/tests/phpunit/AbuseFilterTest.php
new file mode 100644
index 00000000..926116e2
--- /dev/null
+++ b/AbuseFilter/tests/phpunit/AbuseFilterTest.php
@@ -0,0 +1,1413 @@
+<?php
+/**
+ * Generic tests for utility functions in AbuseFilter
+ *
+ * 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
+ */
+
+/**
+ * @group Test
+ * @group AbuseFilter
+ * @group AbuseFilterGeneric
+ * @group Database
+ *
+ * @covers AbuseFilter
+ * @covers AFPData
+ * @covers AbuseFilterVariableHolder
+ * @covers AFComputedVariable
+ */
+class AbuseFilterTest extends MediaWikiTestCase {
+ /** @var User */
+ protected static $mUser;
+ /** @var Title */
+ protected static $mTitle;
+ /** @var WikiPage */
+ protected static $mPage;
+ /** @var AbuseFilterVariableHolder */
+ protected static $mVariables;
+
+ /**
+ * @var array These tables will be deleted in parent::tearDown.
+ * We need it to happen to make tests on fresh pages.
+ */
+ protected $tablesUsed = [
+ 'page',
+ 'page_restrictions',
+ 'abuse_filter',
+ 'abuse_filter_history',
+ 'abuse_filter_log',
+ 'abuse_filter_actions'
+ ];
+
+ /**
+ * @see MediaWikiTestCase::setUp
+ */
+ protected function setUp() {
+ parent::setUp();
+ MWTimestamp::setFakeTime( 1514700000 );
+ $user = User::newFromName( 'AnotherFilteredUser' );
+ $user->addToDatabase();
+ $user->addGroup( 'basicFilteredUser' );
+ self::$mUser = $user;
+ MWTimestamp::setFakeTime( false );
+
+ self::$mVariables = new AbuseFilterVariableHolder();
+
+ // Make sure that the config we're using is the one we're expecting
+ $this->setMwGlobals( [
+ 'wgUser' => $user,
+ 'wgRestrictionTypes' => [
+ 'create',
+ 'edit',
+ 'move',
+ 'upload'
+ ],
+ 'wgAbuseFilterRestrictions' => [
+ 'degroup' => true
+ ],
+ 'wgAbuseFilterIsCentral' => true,
+ 'wgAbuseFilterActions' => [
+ 'throttle' => true,
+ 'warn' => true,
+ 'disallow' => true,
+ 'blockautopromote' => true,
+ 'block' => true,
+ 'rangeblock' => true,
+ 'degroup' => true,
+ 'tag' => true
+ ],
+ 'wgAbuseFilterValidGroups' => [
+ 'default',
+ 'flow'
+ ],
+ 'wgEnableParserLimitReporting' => false
+ ] );
+ $this->setGroupPermissions( [
+ 'basicFilteredUser' => [
+ 'abusefilter-view' => true
+ ],
+ 'intermediateFilteredUser' => [
+ 'abusefilter-log' => true
+ ],
+ 'privilegedFilteredUser' => [
+ 'abusefilter-private' => true,
+ 'abusefilter-revert' => true
+ ]
+ ] );
+ }
+
+ /**
+ * @see MediaWikiTestCase::tearDown
+ */
+ protected function tearDown() {
+ MWTimestamp::setFakeTime( false );
+ $userGroups = self::$mUser->getGroups();
+ // We want to start fresh
+ foreach ( $userGroups as $group ) {
+ self::$mUser->removeGroup( $group );
+ }
+ parent::tearDown();
+ }
+
+ /**
+ * Given the name of a variable, naturally sets it to a determined amount
+ *
+ * @param string $var The variable name
+ * @return array the first position is the result (mixed), the second is a boolean
+ * indicating whether we've been able to compute the given variable
+ */
+ private static function computeExpectedUserVariable( $var ) {
+ $success = true;
+ switch ( $var ) {
+ case 'user_editcount':
+ // Create a page and make the user edit it 7 times
+ $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+ $page->doEditContent(
+ new WikitextContent( 'AbuseFilter test, page creation' ),
+ 'Testing page for AbuseFilter',
+ EDIT_NEW,
+ false,
+ self::$mUser
+ );
+ for ( $i = 1; $i <= 7; $i++ ) {
+ $page->doEditContent(
+ new WikitextContent( "AbuseFilter test, page revision #$i" ),
+ 'Testing page for AbuseFilter',
+ EDIT_UPDATE,
+ false,
+ self::$mUser
+ );
+ }
+ // Reload to reflect deferred update
+ self::$mUser->clearInstanceCache();
+ $result = 7;
+ break;
+ case 'user_name':
+ $result = self::$mUser->getName();
+ break;
+ case 'user_emailconfirm':
+ $time = wfTimestampNow();
+ self::$mUser->setEmailAuthenticationTimestamp( $time );
+ $result = $time;
+ break;
+ case 'user_groups':
+ self::$mUser->addGroup( 'intermediateFilteredUser' );
+ $result = self::$mUser->getEffectiveGroups();
+ break;
+ case 'user_rights':
+ self::$mUser->addGroup( 'privilegedFilteredUser' );
+ $result = self::$mUser->getRights();
+ break;
+ case 'user_blocked':
+ $block = new Block();
+ $block->setTarget( self::$mUser );
+ $block->setBlocker( 'UTSysop' );
+ $block->mReason = 'Testing AbuseFilter variable user_blocked';
+ $block->mExpiry = 'infinity';
+
+ $block->insert();
+ $result = true;
+ break;
+ default:
+ $success = false;
+ $result = null;
+ }
+ return [ $result, $success ];
+ }
+
+ /**
+ * Check that the generated user-related variables are correct
+ *
+ * @param string $varName The name of the variable we're currently testing
+ * @covers AbuseFilter::generateUserVars
+ * @dataProvider provideUserVars
+ */
+ public function testGenerateUserVars( $varName ) {
+ list( $computed, $successfully ) = self::computeExpectedUserVariable( $varName );
+ if ( !$successfully ) {
+ $this->fail( "Given unknown user-related variable $varName." );
+ }
+
+ $variableHolder = AbuseFilter::generateUserVars( self::$mUser );
+ $actual = $variableHolder->getVar( $varName )->toNative();
+ $this->assertSame(
+ $computed,
+ $actual,
+ "AbuseFilter variable $varName is computed wrongly."
+ );
+ }
+
+ /**
+ * Data provider for testGenerateUserVars
+ * @return array
+ */
+ public function provideUserVars() {
+ return [
+ [ 'user_editcount' ],
+ [ 'user_name' ],
+ [ 'user_emailconfirm' ],
+ [ 'user_groups' ],
+ [ 'user_rights' ],
+ [ 'user_blocked' ]
+ ];
+ }
+
+ /**
+ * Check that user_age is correct. Needs a separate function to take into account the
+ * difference between timestamps due to test execution time
+ *
+ * @covers AbuseFilter::generateUserVars
+ */
+ public function testUserAgeVar() {
+ // Set a fake timestamp so that execution time won't be a problem
+ MWTimestamp::setFakeTime( 1514700163 );
+ $variableHolder = AbuseFilter::generateUserVars( self::$mUser );
+ $actual = $variableHolder->getVar( 'user_age' )->toNative();
+
+ $this->assertEquals(
+ 163,
+ $actual,
+ "AbuseFilter variable user_age is computed wrongly. Expected: 163, actual: $actual."
+ );
+ }
+
+ /**
+ * Given the name of a variable, naturally sets it to a determined amount
+ *
+ * @param string $suffix The suffix of the variable
+ * @param string|null $options Further options for the test
+ * @return array the first position is the result (mixed), the second is a boolean
+ * indicating whether we've been able to compute the given variable. If false, then
+ * the result may be null if the requested variable doesn't exist, or false if there
+ * has been some other problem.
+ */
+ private static function computeExpectedTitleVariable( $suffix, $options = null ) {
+ self::$mTitle = Title::newFromText( 'AbuseFilter test' );
+ $page = WikiPage::factory( self::$mTitle );
+
+ if ( $options === 'restricted' ) {
+ $action = str_replace( '_restrictions_', '', $suffix );
+ $namespace = 0;
+ if ( $action === 'upload' ) {
+ // Only files can have it
+ $namespace = 6;
+ }
+ self::$mTitle = Title::makeTitle( $namespace, 'AbuseFilter restrictions test' );
+ $page = WikiPage::factory( self::$mTitle );
+ if ( $action !== 'create' ) {
+ // To apply other restrictions, the title has to exist
+ $page->doEditContent(
+ new WikitextContent( 'AbuseFilter test for title variables' ),
+ 'Testing page for AbuseFilter',
+ EDIT_NEW,
+ false,
+ self::$mUser
+ );
+ }
+ $_ = false;
+ $s = $page->doUpdateRestrictions(
+ [ $action => true ],
+ [ $action => 'infinity' ],
+ $_,
+ 'Testing restrictions for AbuseFilter',
+ self::$mUser
+ );
+ }
+ $success = true;
+ switch ( $suffix ) {
+ case '_id':
+ $result = self::$mTitle->getArticleID();
+ break;
+ case '_namespace':
+ $result = self::$mTitle->getNamespace();
+ break;
+ case '_title':
+ $result = self::$mTitle->getText();
+ break;
+ case '_prefixedtitle':
+ $result = self::$mTitle->getPrefixedText();
+ break;
+ case '_restrictions_create':
+ $restrictions = self::$mTitle->getRestrictions( 'create' );
+ $restrictions = count( $restrictions ) ? $restrictions : [];
+ $preliminarCheck = !( $options === 'restricted' xor count( $restrictions ) );
+ if ( $preliminarCheck ) {
+ $result = $restrictions;
+ } else {
+ $success = false;
+ $result = false;
+ }
+ break;
+ case '_restrictions_edit':
+ $restrictions = self::$mTitle->getRestrictions( 'edit' );
+ $restrictions = count( $restrictions ) ? $restrictions : [];
+ $preliminarCheck = !( $options === 'restricted' xor count( $restrictions ) );
+ if ( $preliminarCheck ) {
+ $result = $restrictions;
+ } else {
+ $success = false;
+ $result = false;
+ }
+ break;
+ case '_restrictions_move':
+ $restrictions = self::$mTitle->getRestrictions( 'move' );
+ $restrictions = count( $restrictions ) ? $restrictions : [];
+ $preliminarCheck = !( $options === 'restricted' xor count( $restrictions ) );
+ if ( $preliminarCheck ) {
+ $result = $restrictions;
+ } else {
+ $success = false;
+ $result = false;
+ }
+ break;
+ case '_restrictions_upload':
+ $restrictions = self::$mTitle->getRestrictions( 'upload' );
+ $restrictions = count( $restrictions ) ? $restrictions : [];
+ $preliminarCheck = !( $options === 'restricted' xor count( $restrictions ) );
+ if ( $preliminarCheck ) {
+ $result = $restrictions;
+ } else {
+ $success = false;
+ $result = false;
+ }
+ break;
+ case '_recent_contributors':
+ // Create the page and make a couple of edits from different users
+ $page->doEditContent(
+ new WikitextContent( 'AbuseFilter test for title variables' ),
+ 'Testing page for AbuseFilter',
+ EDIT_NEW,
+ false,
+ self::$mUser
+ );
+ $mockContributors = [ 'Alice', 'Bob', 'Charlie' ];
+ foreach ( $mockContributors as $user ) {
+ $page->doEditContent(
+ new WikitextContent( "AbuseFilter test, page revision by $user" ),
+ 'Testing page for AbuseFilter',
+ EDIT_UPDATE,
+ false,
+ User::newFromName( $user )
+ );
+ }
+ $contributors = array_reverse( $mockContributors );
+ array_push( $contributors, self::$mUser->getName() );
+ $result = $contributors;
+ break;
+ case '_first_contributor':
+ // Create the page and make a couple of edits from different users
+ $page->doEditContent(
+ new WikitextContent( 'AbuseFilter test for title variables' ),
+ 'Testing page for AbuseFilter',
+ EDIT_NEW,
+ false,
+ self::$mUser
+ );
+ $mockContributors = [ 'Alice', 'Bob', 'Charlie' ];
+ foreach ( $mockContributors as $user ) {
+ $page->doEditContent(
+ new WikitextContent( "AbuseFilter test, page revision by $user" ),
+ 'Testing page for AbuseFilter',
+ EDIT_UPDATE,
+ false,
+ User::newFromName( $user )
+ );
+ }
+ $result = self::$mUser->getName();
+ break;
+ default:
+ $success = false;
+ $result = null;
+ }
+ return [ $result, $success ];
+ }
+
+ /**
+ * Check that the generated title-related variables are correct
+ *
+ * @param string $prefix The prefix of the variables we're currently testing
+ * @param string $suffix The suffix of the variables we're currently testing
+ * @param string|null $options Whether we want to execute the test with specific options
+ * Right now, this can only be 'restricted' for restrictions variables; in this case,
+ * the tested title will have the requested restriction.
+ * @covers AbuseFilter::generateTitleVars
+ * @dataProvider provideTitleVars
+ */
+ public function testGenerateTitleVars( $prefix, $suffix, $options = null ) {
+ $varName = $prefix . $suffix;
+ list( $computed, $successfully ) = self::computeExpectedTitleVariable( $suffix, $options );
+ if ( !$successfully ) {
+ if ( $computed === null ) {
+ $this->fail( "Given unknown title-related variable $varName." );
+ } else {
+ $this->fail( "AbuseFilter variable $varName is computed wrongly." );
+ }
+ }
+
+ $variableHolder = AbuseFilter::generateTitleVars( self::$mTitle, $prefix );
+ $actual = $variableHolder->getVar( $varName )->toNative();
+ $this->assertSame(
+ $computed,
+ $actual,
+ "AbuseFilter variable $varName is computed wrongly."
+ );
+ }
+
+ /**
+ * Data provider for testGenerateUserVars
+ * @return array
+ */
+ public function provideTitleVars() {
+ return [
+ [ 'page', '_id' ],
+ [ 'page', '_namespace' ],
+ [ 'page', '_title' ],
+ [ 'page', '_prefixedtitle' ],
+ [ 'page', '_restrictions_create' ],
+ [ 'page', '_restrictions_create', 'restricted' ],
+ [ 'page', '_restrictions_edit' ],
+ [ 'page', '_restrictions_edit', 'restricted' ],
+ [ 'page', '_restrictions_move' ],
+ [ 'page', '_restrictions_move', 'restricted' ],
+ [ 'page', '_restrictions_upload' ],
+ [ 'page', '_restrictions_upload', 'restricted' ],
+ [ 'page', '_first_contributor' ],
+ [ 'page', '_recent_contributors' ],
+ [ 'moved_from', '_id' ],
+ [ 'moved_from', '_namespace' ],
+ [ 'moved_from', '_title' ],
+ [ 'moved_from', '_prefixedtitle' ],
+ [ 'moved_from', '_restrictions_create' ],
+ [ 'moved_from', '_restrictions_create', 'restricted' ],
+ [ 'moved_from', '_restrictions_edit' ],
+ [ 'moved_from', '_restrictions_edit', 'restricted' ],
+ [ 'moved_from', '_restrictions_move' ],
+ [ 'moved_from', '_restrictions_move', 'restricted' ],
+ [ 'moved_from', '_restrictions_upload' ],
+ [ 'moved_from', '_restrictions_upload', 'restricted' ],
+ [ 'moved_from', '_first_contributor' ],
+ [ 'moved_from', '_recent_contributors' ],
+ [ 'moved_to', '_id' ],
+ [ 'moved_to', '_namespace' ],
+ [ 'moved_to', '_title' ],
+ [ 'moved_to', '_prefixedtitle' ],
+ [ 'moved_to', '_restrictions_create' ],
+ [ 'moved_to', '_restrictions_create', 'restricted' ],
+ [ 'moved_to', '_restrictions_edit' ],
+ [ 'moved_to', '_restrictions_edit', 'restricted' ],
+ [ 'moved_to', '_restrictions_move' ],
+ [ 'moved_to', '_restrictions_move', 'restricted' ],
+ [ 'moved_to', '_restrictions_upload' ],
+ [ 'moved_to', '_restrictions_upload', 'restricted' ],
+ [ 'moved_to', '_first_contributor' ],
+ [ 'moved_to', '_recent_contributors' ],
+ ];
+ }
+
+ /**
+ * Check that _age variables are correct. They need a separate function to take into
+ * account the difference between timestamps due to test execution time
+ *
+ * @param string $prefix Prefix of the variable to test
+ * @covers AbuseFilter::generateTitleVars
+ * @dataProvider provideAgeVars
+ */
+ public function testAgeVars( $prefix ) {
+ $varName = $prefix . '_age';
+
+ MWTimestamp::setFakeTime( 1514700000 );
+ self::$mTitle = Title::newFromText( 'AbuseFilter test' );
+ $page = WikiPage::factory( self::$mTitle );
+ $page->doEditContent(
+ new WikitextContent( 'AbuseFilter _age variables test' ),
+ 'Testing page for AbuseFilter',
+ EDIT_NEW,
+ false,
+ self::$mUser
+ );
+
+ MWTimestamp::setFakeTime( 1514700123 );
+ $variableHolder = AbuseFilter::generateTitleVars( self::$mTitle, $prefix );
+ $actual = $variableHolder->getVar( $varName )->toNative();
+ $this->assertEquals(
+ 123,
+ $actual,
+ "AbuseFilter variable $varName is computed wrongly. Expected: 123, actual: $actual."
+ );
+ }
+
+ /**
+ * Data provider for testAgeVars
+ * @return array
+ */
+ public function provideAgeVars() {
+ return [
+ [ 'page' ],
+ [ 'moved_from' ],
+ [ 'moved_to' ],
+ ];
+ }
+
+ /**
+ * Check that version comparing works well
+ *
+ * @param array $firstVersion [ stdClass, array ]
+ * @param array $secondVersion [ stdClass, array ]
+ * @param array $expected The differences
+ * @covers AbuseFilter::compareVersions
+ * @dataProvider provideVersions
+ */
+ public function testCompareVersions( $firstVersion, $secondVersion, $expected ) {
+ $differences = AbuseFilter::compareVersions( $firstVersion, $secondVersion );
+
+ $this->assertSame(
+ $expected,
+ $differences,
+ 'AbuseFilter::compareVersions did not output the expected result.'
+ );
+ }
+
+ /**
+ * Data provider for testCompareVersions
+ * @return array
+ */
+ public function provideVersions() {
+ return [
+ [
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ],
+ [
+ 'disallow' => [
+ 'action' => 'disallow',
+ 'parameters' => []
+ ]
+ ]
+ ],
+ [
+ (object)[
+ 'af_public_comments' => 'OtherComments',
+ 'af_pattern' => '/*Other pattern*/',
+ 'af_comments' => 'Other comments',
+ 'af_deleted' => 1,
+ 'af_enabled' => 0,
+ 'af_hidden' => 1,
+ 'af_global' => 1,
+ 'af_group' => 'flow'
+ ],
+ [
+ 'disallow' => [
+ 'action' => 'disallow',
+ 'parameters' => []
+ ]
+ ]
+ ],
+ [
+ 'af_public_comments',
+ 'af_pattern',
+ 'af_comments',
+ 'af_deleted',
+ 'af_enabled',
+ 'af_hidden',
+ 'af_global',
+ 'af_group',
+ ]
+ ],
+ [
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ],
+ [
+ 'disallow' => [
+ 'action' => 'disallow',
+ 'parameters' => []
+ ]
+ ]
+ ],
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ],
+ [
+ 'disallow' => [
+ 'action' => 'disallow',
+ 'parameters' => []
+ ]
+ ]
+ ],
+ []
+ ],
+ [
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ],
+ [
+ 'disallow' => [
+ 'action' => 'disallow',
+ 'parameters' => []
+ ]
+ ]
+ ],
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ],
+ [
+ 'degroup' => [
+ 'action' => 'degroup',
+ 'parameters' => []
+ ]
+ ]
+ ],
+ [ 'actions' ]
+ ],
+ [
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ],
+ [
+ 'disallow' => [
+ 'action' => 'disallow',
+ 'parameters' => []
+ ]
+ ]
+ ],
+ [
+ (object)[
+ 'af_public_comments' => 'OtherComments',
+ 'af_pattern' => '/*Other pattern*/',
+ 'af_comments' => 'Other comments',
+ 'af_deleted' => 1,
+ 'af_enabled' => 0,
+ 'af_hidden' => 1,
+ 'af_global' => 1,
+ 'af_group' => 'flow'
+ ],
+ [
+ 'blockautopromote' => [
+ 'action' => 'blockautopromote',
+ 'parameters' => []
+ ]
+ ]
+ ],
+ [
+ 'af_public_comments',
+ 'af_pattern',
+ 'af_comments',
+ 'af_deleted',
+ 'af_enabled',
+ 'af_hidden',
+ 'af_global',
+ 'af_group',
+ 'actions'
+ ]
+ ],
+ [
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ],
+ [
+ 'disallow' => [
+ 'action' => 'disallow',
+ 'parameters' => []
+ ]
+ ]
+ ],
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ],
+ [
+ 'warn' => [
+ 'action' => 'warn',
+ 'parameters' => [
+ 'abusefilter-warning'
+ ]
+ ]
+ ]
+ ],
+ [ 'actions' ]
+ ],
+ [
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ],
+ [
+ 'warn' => [
+ 'action' => 'warn',
+ 'parameters' => [
+ 'abusefilter-warning'
+ ]
+ ]
+ ]
+ ],
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ],
+ [
+ 'disallow' => [
+ 'action' => 'disallow',
+ 'parameters' => []
+ ]
+ ]
+ ],
+ [ 'actions' ]
+ ],
+ [
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ],
+ [
+ 'warn' => [
+ 'action' => 'warn',
+ 'parameters' => [
+ 'abusefilter-warning'
+ ]
+ ]
+ ]
+ ],
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ],
+ [
+ 'warn' => [
+ 'action' => 'warn',
+ 'parameters' => [
+ 'abusefilter-my-best-warning'
+ ]
+ ],
+ 'degroup' => [
+ 'action' => 'degroup',
+ 'parameters' => []
+ ]
+ ]
+ ],
+ [ 'actions' ]
+ ],
+ [
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ],
+ [
+ 'warn' => [
+ 'action' => 'warn',
+ 'parameters' => [
+ 'abusefilter-warning'
+ ]
+ ]
+ ]
+ ],
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Other Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 1,
+ 'af_global' => 0,
+ 'af_group' => 'flow'
+ ],
+ [
+ 'warn' => [
+ 'action' => 'warn',
+ 'parameters' => [
+ 'abusefilter-my-best-warning'
+ ]
+ ]
+ ]
+ ],
+ [
+ 'af_pattern',
+ 'af_hidden',
+ 'af_group',
+ 'actions'
+ ]
+ ],
+ [
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'default'
+ ],
+ [
+ 'warn' => [
+ 'action' => 'warn',
+ 'parameters' => [
+ 'abusefilter-beautiful-warning'
+ ]
+ ]
+ ]
+ ],
+ [
+ (object)[
+ 'af_public_comments' => 'Comments',
+ 'af_pattern' => '/*Pattern*/',
+ 'af_comments' => 'Comments',
+ 'af_deleted' => 0,
+ 'af_enabled' => 1,
+ 'af_hidden' => 0,
+ 'af_global' => 0,
+ 'af_group' => 'flow'
+ ],
+ [
+ 'warn' => [
+ 'action' => 'warn',
+ 'parameters' => [
+ 'abusefilter-my-best-warning'
+ ]
+ ]
+ ]
+ ],
+ [
+ 'af_group',
+ 'actions'
+ ]
+ ],
+ ];
+ }
+
+ /**
+ * Check that row translating from abuse_filter_history to abuse_filter is working fine
+ *
+ * @param stdClass $row The row to translate
+ * @param array $expected The expected result
+ * @covers AbuseFilter::translateFromHistory
+ * @dataProvider provideHistoryRows
+ */
+ public function testTranslateFromHistory( $row, $expected ) {
+ $actual = AbuseFilter::translateFromHistory( $row );
+
+ $this->assertEquals(
+ $expected,
+ $actual,
+ 'AbuseFilter::translateFromHistory produced a wrong output.'
+ );
+ }
+
+ /**
+ * Data provider for testTranslateFromHistory
+ * @return array
+ */
+ public function provideHistoryRows() {
+ return [
+ [
+ (object)[
+ 'afh_filter' => 1,
+ 'afh_user' => 0,
+ 'afh_user_text' => 'FilteredUser',
+ 'afh_timestamp' => '20180706142932',
+ 'afh_pattern' => '/*Pattern*/',
+ 'afh_comments' => 'Comments',
+ 'afh_flags' => 'enabled,hidden',
+ 'afh_public_comments' => 'Description',
+ 'afh_actions' => serialize( [
+ 'degroup' => [],
+ 'disallow' => []
+ ] ),
+ 'afh_deleted' => 0,
+ 'afh_changed_fields' => 'actions',
+ 'afh_group' => 'default'
+ ],
+ [
+ (object)[
+ 'af_pattern' => '/*Pattern*/',
+ 'af_user' => 0,
+ 'af_user_text' => 'FilteredUser',
+ 'af_timestamp' => '20180706142932',
+ 'af_comments' => 'Comments',
+ 'af_public_comments' => 'Description',
+ 'af_deleted' => 0,
+ 'af_id' => 1,
+ 'af_group' => 'default',
+ 'af_hidden' => 1,
+ 'af_enabled' => 1
+ ],
+ [
+ 'degroup' => [
+ 'action' => 'degroup',
+ 'parameters' => []
+ ],
+ 'disallow' => [
+ 'action' => 'disallow',
+ 'parameters' => []
+ ]
+ ]
+ ]
+ ],
+ [
+ (object)[
+ 'afh_filter' => 5,
+ 'afh_user' => 0,
+ 'afh_user_text' => 'FilteredUser',
+ 'afh_timestamp' => '20180706145516',
+ 'afh_pattern' => '1 === 1',
+ 'afh_comments' => '',
+ 'afh_flags' => '',
+ 'afh_public_comments' => 'Our best filter',
+ 'afh_actions' => serialize( [
+ 'warn' => [
+ 'abusefilter-warning',
+ ''
+ ],
+ 'disallow' => [],
+ ] ),
+ 'afh_deleted' => 0,
+ 'afh_changed_fields' => 'af_pattern,af_comments,af_enabled,actions',
+ 'afh_group' => 'flow'
+ ],
+ [
+ (object)[
+ 'af_pattern' => '1 === 1',
+ 'af_user' => 0,
+ 'af_user_text' => 'FilteredUser',
+ 'af_timestamp' => '20180706145516',
+ 'af_comments' => '',
+ 'af_public_comments' => 'Our best filter',
+ 'af_deleted' => 0,
+ 'af_id' => 5,
+ 'af_group' => 'flow',
+ 'af_hidden' => 0,
+ 'af_enabled' => 0
+ ],
+ [
+ 'warn' => [
+ 'action' => 'warn',
+ 'parameters' => [
+ 'abusefilter-warning',
+ ''
+ ]
+ ],
+ 'disallow' => [
+ 'action' => 'disallow',
+ 'parameters' => []
+ ]
+ ]
+ ]
+ ],
+ [
+ (object)[
+ 'afh_filter' => 7,
+ 'afh_user' => 1,
+ 'afh_user_text' => 'AnotherUser',
+ 'afh_timestamp' => '20160511185604',
+ 'afh_pattern' => 'added_lines irlike "lol" & summary == "ggwp"',
+ 'afh_comments' => 'Show vandals no mercy, for you shall receive none.',
+ 'afh_flags' => 'enabled,hidden',
+ 'afh_public_comments' => 'Whatever',
+ 'afh_actions' => serialize( [
+ 'warn' => [
+ 'abusefilter-warning',
+ ''
+ ],
+ 'disallow' => [],
+ 'block' => [
+ 'blocktalk',
+ '8 hours',
+ 'infinity'
+ ]
+ ] ),
+ 'afh_deleted' => 0,
+ 'afh_changed_fields' => 'af_pattern,af_comments,af_enabled,af_public_comments,actions',
+ 'afh_group' => 'default'
+ ],
+ [
+ (object)[
+ 'af_pattern' => 'added_lines irlike "lol" & summary == "ggwp"',
+ 'af_user' => 1,
+ 'af_user_text' => 'AnotherUser',
+ 'af_timestamp' => '20160511185604',
+ 'af_comments' => 'Show vandals no mercy, for you shall receive none.',
+ 'af_public_comments' => 'Whatever',
+ 'af_deleted' => 0,
+ 'af_id' => 7,
+ 'af_group' => 'default',
+ 'af_hidden' => 1,
+ 'af_enabled' => 1
+ ],
+ [
+ 'warn' => [
+ 'action' => 'warn',
+ 'parameters' => [
+ 'abusefilter-warning',
+ ''
+ ]
+ ],
+ 'disallow' => [
+ 'action' => 'disallow',
+ 'parameters' => []
+ ],
+ 'block' => [
+ 'action' => 'block',
+ 'parameters' => [
+ 'blocktalk',
+ '8 hours',
+ 'infinity'
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ (object)[
+ 'afh_filter' => 131,
+ 'afh_user' => 15,
+ 'afh_user_text' => 'YetAnotherUser',
+ 'afh_timestamp' => '20180511185604',
+ 'afh_pattern' => 'user_name == "Thatguy"',
+ 'afh_comments' => '',
+ 'afh_flags' => 'hidden,deleted',
+ 'afh_public_comments' => 'No comment.',
+ 'afh_actions' => serialize( [
+ 'throttle' => [
+ '131',
+ '3,60',
+ 'user'
+ ],
+ 'tag' => [
+ 'mytag',
+ 'yourtag'
+ ]
+ ] ),
+ 'afh_deleted' => 1,
+ 'afh_changed_fields' => 'af_pattern',
+ 'afh_group' => 'default'
+ ],
+ [
+ (object)[
+ 'af_pattern' => 'user_name == "Thatguy"',
+ 'af_user' => 15,
+ 'af_user_text' => 'YetAnotherUser',
+ 'af_timestamp' => '20180511185604',
+ 'af_comments' => '',
+ 'af_public_comments' => 'No comment.',
+ 'af_deleted' => 1,
+ 'af_id' => 131,
+ 'af_group' => 'default',
+ 'af_hidden' => 1,
+ 'af_enabled' => 0
+ ],
+ [
+ 'throttle' => [
+ 'action' => 'throttle',
+ 'parameters' => [
+ '131',
+ '3,60',
+ 'user'
+ ]
+ ],
+ 'tag' => [
+ 'action' => 'tag',
+ 'parameters' => [
+ 'mytag',
+ 'yourtag'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * Given the name of a variable, naturally sets it to a determined amount
+ *
+ * @param string $old The old wikitext of the page
+ * @param string $new The new wikitext of the page
+ * @return array
+ */
+ private static function computeExpectedEditVariable( $old, $new ) {
+ global $wgParser;
+ $popts = ParserOptions::newFromUser( self::$mUser );
+ // Order matters here. Some variables rely on other ones.
+ $variables = [
+ 'new_html',
+ 'new_pst',
+ 'new_text',
+ 'edit_diff',
+ 'edit_diff_pst',
+ 'new_size',
+ 'old_size',
+ 'edit_delta',
+ 'added_lines',
+ 'removed_lines',
+ 'added_lines_pst',
+ 'all_links',
+ 'old_links',
+ 'added_links',
+ 'removed_links'
+ ];
+
+ // Set required variables
+ self::$mVariables->setVar( 'old_wikitext', $old );
+ self::$mVariables->setVar( 'new_wikitext', $new );
+ self::$mVariables->setVar( 'summary', 'Testing page for AbuseFilter' );
+
+ $computedVariables = [];
+ foreach ( $variables as $var ) {
+ $success = true;
+ // Reset text variables since some operations are changing them.
+ $oldText = $old;
+ $newText = $new;
+ switch ( $var ) {
+ case 'edit_diff_pst':
+ $newText = self::$mVariables->getVar( 'new_pst' )->toString();
+ // Intentional fall-through
+ case 'edit_diff':
+ $diffs = new Diff( explode( "\n", $oldText ), explode( "\n", $newText ) );
+ $format = new UnifiedDiffFormatter();
+ $result = $format->format( $diffs );
+ break;
+ case 'new_size':
+ $result = strlen( $newText );
+ break;
+ case 'old_size':
+ $result = strlen( $oldText );
+ break;
+ case 'edit_delta':
+ $result = strlen( $newText ) - strlen( $oldText );
+ break;
+ case 'added_lines_pst':
+ case 'added_lines':
+ case 'removed_lines':
+ $diffVariable = $var === 'added_lines_pst' ? 'edit_diff_pst' : 'edit_diff';
+ $diff = self::$mVariables->getVar( $diffVariable )->toString();
+ $line_prefix = $var === 'removed_lines' ? '-' : '+';
+ $diff_lines = explode( "\n", $diff );
+ $interest_lines = [];
+ foreach ( $diff_lines as $line ) {
+ if ( substr( $line, 0, 1 ) === $line_prefix ) {
+ $interest_lines[] = substr( $line, strlen( $line_prefix ) );
+ }
+ }
+ $result = $interest_lines;
+ break;
+ case 'new_text':
+ $newHtml = self::$mVariables->getVar( 'new_html' )->toString();
+ $result = StringUtils::delimiterReplace( '<', '>', '', $newHtml );
+ break;
+ case 'new_pst':
+ case 'new_html':
+ $article = self::$mPage;
+ $content = ContentHandler::makeContent( $newText, $article->getTitle() );
+ $editInfo = $article->prepareContentForEdit( $content );
+
+ if ( $var === 'new_pst' ) {
+ $result = $editInfo->pstContent->serialize( $editInfo->format );
+ } else {
+ $result = $editInfo->output->getText();
+ }
+ break;
+ case 'all_links':
+ $article = self::$mPage;
+ $content = ContentHandler::makeContent( $newText, $article->getTitle() );
+ $editInfo = $article->prepareContentForEdit( $content );
+ $result = array_keys( $editInfo->output->getExternalLinks() );
+ break;
+ case 'old_links':
+ $article = self::$mPage;
+ $popts->setTidy( true );
+ $edit = $wgParser->parse( $oldText, $article->getTitle(), $popts );
+ $result = array_keys( $edit->getExternalLinks() );
+ break;
+ case 'added_links':
+ case 'removed_links':
+ $oldLinks = self::$mVariables->getVar( 'old_links' )->toString();
+ $newLinks = self::$mVariables->getVar( 'all_links' )->toString();
+ $oldLinks = explode( "\n", $oldLinks );
+ $newLinks = explode( "\n", $newLinks );
+
+ if ( $var === 'added_links' ) {
+ $result = array_diff( $newLinks, $oldLinks );
+ } else {
+ $result = array_diff( $oldLinks, $newLinks );
+ }
+ break;
+ default:
+ $success = false;
+ $result = null;
+ }
+ $computedVariables[$var] = [ $result, $success ];
+ self::$mVariables->setVar( $var, $result );
+ }
+ return $computedVariables;
+ }
+
+ /**
+ * Check that the generated variables for edits are correct
+ *
+ * @param string $oldText The old wikitext of the page
+ * @param string $newText The new wikitext of the page
+ * @covers AbuseFilter::getEditVars
+ * @dataProvider provideEditVars
+ */
+ public function testGetEditVars( $oldText, $newText ) {
+ global $wgLang;
+ self::$mTitle = Title::makeTitle( 0, 'AbuseFilter test' );
+ self::$mPage = WikiPage::factory( self::$mTitle );
+
+ self::$mPage->doEditContent(
+ new WikitextContent( $oldText ),
+ 'Creating the test page',
+ EDIT_NEW,
+ false,
+ self::$mUser
+ );
+ self::$mPage->doEditContent(
+ new WikitextContent( $newText ),
+ 'Testing page for AbuseFilter',
+ EDIT_UPDATE,
+ false,
+ self::$mUser
+ );
+
+ $computeResult = self::computeExpectedEditVariable( $oldText, $newText );
+
+ $computedVariables = [];
+ foreach ( $computeResult as $varName => $computed ) {
+ if ( !$computed[1] ) {
+ $this->fail( "Given unknown edit variable $varName." );
+ }
+ $computedVariables[$varName] = $computed[0];
+ }
+
+ self::$mVariables->addHolders( AbuseFilter::getEditVars( self::$mTitle, self::$mPage ) );
+
+ $actualVariables = [];
+ foreach ( self::$mVariables->mVars as $varName => $_ ) {
+ $actualVariables[$varName] = self::$mVariables->getVar( $varName )->toNative();
+ }
+
+ $differences = [];
+ foreach ( $computedVariables as $var => $computed ) {
+ if ( !isset( $actualVariables[$var] ) ) {
+ $this->fail( "AbuseFilter::getEditVars didn't set the $var variable." );
+ } elseif ( $computed !== $actualVariables[$var] ) {
+ $differences[] = $var;
+ }
+ }
+
+ $this->assertCount(
+ 0,
+ $differences,
+ 'The following AbuseFilter variables are computed wrongly: ' . $wgLang->commaList( $differences )
+ );
+ }
+
+ /**
+ * Data provider for testGetEditVars
+ * @return array
+ */
+ public function provideEditVars() {
+ return [
+ [
+ '[https://www.mediawiki.it/wiki/Extension:AbuseFilter AbuseFilter] test page',
+ 'Adding something to compute edit variables. Here are some diacritics to make sure ' .
+ "the test behaves well with unicode: Là giù cascherò io altresì.\n名探偵コナン.\n" .
+ "[[Help:Pre Save Transform|]] should make the difference as well.\n" .
+ 'Instead, [https://www.mediawiki.it this] is an external link.'
+ ],
+ [
+ 'Adding something to compute edit variables. Here are some diacritics to make sure ' .
+ "the test behaves well with unicode: Là giù cascherò io altresì.\n名探偵コナン.\n" .
+ "[[Help:Pre Save Transform|]] should make the difference as well.\n" .
+ 'Instead, [https://www.mediawiki.it this] is an external link.',
+ '[https://www.mediawiki.it/wiki/Extension:AbuseFilter AbuseFilter] test page'
+ ],
+ [
+ "A '''foo''' is not a ''bar''.",
+ "Actually, according to [http://en.wikipedia.org ''Wikipedia''], a '''''foo''''' " .
+ 'is <small>more or less</small> the same as a <b>bar</b>, except that a foo is ' .
+ 'usually provided together with a [[cellar door|]] to make it work<ref>Yes, really</ref>.'
+ ],
+ [
+ 'This edit will be pretty smll',
+ 'This edit will be pretty small'
+ ]
+ ];
+ }
+}
diff --git a/AbuseFilter/tests/phpunit/AbuseFilterTokenizerTest.php b/AbuseFilter/tests/phpunit/AbuseFilterTokenizerTest.php
new file mode 100644
index 00000000..f573126f
--- /dev/null
+++ b/AbuseFilter/tests/phpunit/AbuseFilterTokenizerTest.php
@@ -0,0 +1,146 @@
+<?php
+/**
+ * Tests for the AFPData class
+ *
+ * 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
+ */
+
+/**
+ * @group Test
+ * @group AbuseFilter
+ *
+ * @covers AbuseFilterTokenizer
+ * @covers AFPToken
+ * @covers AbuseFilterParser
+ * @covers AFPUserVisibleException
+ * @covers AFPException
+ */
+class AbuseFilterTokenizerTest extends MediaWikiTestCase {
+ /**
+ * @return AbuseFilterParser
+ */
+ public static function getParser() {
+ static $parser = null;
+ if ( !$parser ) {
+ $parser = new AbuseFilterParser();
+ } else {
+ $parser->resetState();
+ }
+ return $parser;
+ }
+
+ /**
+ * Base method for testing exceptions
+ *
+ * @param string $excep Identifier of the exception (e.g. 'unexpectedtoken')
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ */
+ private function exceptionTest( $excep, $expr, $caller ) {
+ $parser = self::getParser();
+ try {
+ $parser->parse( $expr );
+ } catch ( AFPUserVisibleException $e ) {
+ $this->assertEquals(
+ $excep,
+ $e->mExceptionID,
+ "Exception $excep not thrown in AbuseFilterTokenizer::$caller"
+ );
+ return;
+ }
+
+ $this->fail( "Exception $excep not thrown in AbuseFilterTokenizer::$caller" );
+ }
+
+ /**
+ * Test the 'unclosedcomment' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @covers AbuseFilterTokenizer::nextToken
+ * @dataProvider unclosedComment
+ */
+ public function testUnclosedCommentException( $expr, $caller ) {
+ $this->exceptionTest( 'unclosedcomment', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testUnclosedCommentException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function unclosedComment() {
+ return [
+ [ ' /**** / * /', 'nextToken' ],
+ ];
+ }
+
+ /**
+ * Test the 'unrecognisedtoken' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @covers AbuseFilterTokenizer::nextToken
+ * @dataProvider unrecognisedToken
+ */
+ public function testUnrecognisedTokenException( $expr, $caller ) {
+ $this->exceptionTest( 'unrecognisedtoken', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testUnrecognisedTokenException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function unrecognisedToken() {
+ return [
+ [ '#', 'nextToken' ],
+ ];
+ }
+
+ /**
+ * Test the 'unclosedstring' exception
+ *
+ * @param string $expr The expression to test
+ * @param string $caller The function where the exception is thrown
+ * @covers AbuseFilterTokenizer::readStringLiteral
+ * @dataProvider unclosedString
+ */
+ public function testUnclosedStringException( $expr, $caller ) {
+ $this->exceptionTest( 'unclosedstring', $expr, $caller );
+ }
+
+ /**
+ * Data provider for testUnclosedStringException
+ * The second parameter is the function where the exception is raised.
+ * One expression for each throw.
+ *
+ * @return array
+ */
+ public function unclosedString() {
+ return [
+ [ '"', 'readStringLiteral' ],
+ ];
+ }
+}
diff --git a/AbuseFilter/tests/phpunit/parserTest.php b/AbuseFilter/tests/phpunit/parserTest.php
deleted file mode 100644
index b253e858..00000000
--- a/AbuseFilter/tests/phpunit/parserTest.php
+++ /dev/null
@@ -1,145 +0,0 @@
-<?php
-/**
- * Tests for the AbuseFilter parser
- *
- * 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
- *
- * @group Test
- * @group AbuseFilter
- *
- * @licence GNU GPL v2+
- * @author Marius Hoch < hoo@online.de >
- */
-class AbuseFilterParserTest extends MediaWikiTestCase {
- /**
- * @return AbuseFilterParser
- */
- static function getParser() {
- static $parser = null;
- if ( !$parser ) {
- $parser = new AbuseFilterParser();
- }
- return $parser;
- }
-
- /**
- * @return [AbuseFilterParser]
- */
- static function getParsers() {
- static $parsers = null;
- if ( !$parsers ) {
- $parsers = [
- new AbuseFilterParser(),
- new AbuseFilterCachingParser()
- ];
- }
- return $parsers;
- }
-
- /**
- * @dataProvider readTests
- */
- public function testParser( $testName, $rule, $expected ) {
- if ( !class_exists( 'AntiSpoof' ) && preg_match( '/(cc)?norm\(/i', $rule ) ) {
- // The norm and ccnorm parser functions aren't working correctly without AntiSpoof
- $this->markTestSkipped( 'Parser test ' . $testName . ' requires the AntiSpoof extension' );
- }
-
- foreach ( self::getParsers() as $parser ) {
- $actual = $parser->parse( $rule );
- $this->assertEquals( $expected, $actual, 'Running parser test ' . $testName );
- }
- }
-
- /**
- * @return array
- */
- public function readTests() {
- $tests = [];
- $testPath = __DIR__ . "/../parserTests";
- $testFiles = glob( $testPath . "/*.t" );
-
- foreach ( $testFiles as $testFile ) {
- $testName = substr( $testFile, 0, -2 );
-
- $resultFile = $testName . '.r';
- $rule = trim( file_get_contents( $testFile ) );
- $result = trim( file_get_contents( $resultFile ) ) == 'MATCH';
-
- $tests[] = [
- basename( $testName ),
- $rule,
- $result
- ];
- }
-
- return $tests;
- }
-
- /**
- * Ensure that AbuseFilterTokenizer::OPERATOR_RE matches the contents
- * and order of AbuseFilterTokenizer::$operators.
- */
- public function testOperatorRe() {
- $operatorRe = '/(' . implode( '|', array_map( function ( $op ) {
- return preg_quote( $op, '/' );
- }, AbuseFilterTokenizer::$operators ) ) . ')/A';
- $this->assertEquals( $operatorRe, AbuseFilterTokenizer::OPERATOR_RE );
- }
-
- /**
- * Ensure that AbuseFilterTokenizer::RADIX_RE matches the contents
- * and order of AbuseFilterTokenizer::$bases.
- */
- public function testRadixRe() {
- $baseClass = implode( '', array_keys( AbuseFilterTokenizer::$bases ) );
- $radixRe = "/([0-9A-Fa-f]+(?:\.\d*)?|\.\d+)([$baseClass])?/Au";
- $this->assertEquals( $radixRe, AbuseFilterTokenizer::RADIX_RE );
- }
-
- /**
- * Ensure the number of conditions counted for given expressions is right.
- *
- * @dataProvider condCountCases
- */
- public function testCondCount( $rule, $expected ) {
- $parser = self::getParser();
- // Set some variables for convenience writing test cases
- $parser->setVars( array_combine( range( 'a', 'f' ), range( 'a', 'f' ) ) );
- $countBefore = AbuseFilter::$condCount;
- $parser->parse( $rule );
- $countAfter = AbuseFilter::$condCount;
- $actual = $countAfter - $countBefore;
- $this->assertEquals( $expected, $actual, 'Condition count for ' . $rule );
- }
-
- /**
- * @return array
- */
- public function condCountCases() {
- return [
- [ '(((a == b)))', 1 ],
- [ 'contains_any(a, b, c)', 1 ],
- [ 'a == 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 ],
- ];
- }
-}