Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
47 / 47 |
|
100.00% |
7 / 7 |
CRAP | |
100.00% |
1 / 1 |
EmergencyCache | |
100.00% |
47 / 47 |
|
100.00% |
7 / 7 |
12 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getFiltersToCheckInGroup | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
setNewForFilter | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
2 | |||
incrementForFilter | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
getForFilter | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
createGroupKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
createFilterKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\AbuseFilter; |
4 | |
5 | use BagOStuff; |
6 | |
7 | /** |
8 | * Helper class for EmergencyWatcher. Wrapper around cache which tracks hits of recently |
9 | * modified filters. |
10 | */ |
11 | class EmergencyCache { |
12 | |
13 | public const SERVICE_NAME = 'AbuseFilterEmergencyCache'; |
14 | |
15 | /** @var BagOStuff */ |
16 | private $stash; |
17 | |
18 | /** @var int[] */ |
19 | private $ttlPerGroup; |
20 | |
21 | /** |
22 | * @param BagOStuff $stash |
23 | * @param int[] $ttlPerGroup |
24 | */ |
25 | public function __construct( BagOStuff $stash, array $ttlPerGroup ) { |
26 | $this->stash = $stash; |
27 | $this->ttlPerGroup = $ttlPerGroup; |
28 | } |
29 | |
30 | /** |
31 | * Get recently modified filters in the group. Thanks to this, performance can be improved, |
32 | * because only a small subset of filters will need an update. |
33 | * |
34 | * @param string $group |
35 | * @return int[] |
36 | */ |
37 | public function getFiltersToCheckInGroup( string $group ): array { |
38 | $filterToExpiry = $this->stash->get( $this->createGroupKey( $group ) ); |
39 | if ( $filterToExpiry === false ) { |
40 | return []; |
41 | } |
42 | $time = (int)round( $this->stash->getCurrentTime() ); |
43 | return array_keys( array_filter( |
44 | $filterToExpiry, |
45 | static function ( $exp ) use ( $time ) { |
46 | return $exp > $time; |
47 | } |
48 | ) ); |
49 | } |
50 | |
51 | /** |
52 | * Create a new entry in cache for a filter and update the entry for the group. |
53 | * This method is usually called after the filter has been updated. |
54 | * |
55 | * @param int $filter |
56 | * @param string $group |
57 | * @return bool |
58 | */ |
59 | public function setNewForFilter( int $filter, string $group ): bool { |
60 | $ttl = $this->ttlPerGroup[$group] ?? $this->ttlPerGroup['default']; |
61 | $expiry = (int)round( $this->stash->getCurrentTime() + $ttl ); |
62 | $this->stash->merge( |
63 | $this->createGroupKey( $group ), |
64 | static function ( $cache, $key, $value ) use ( $filter, $expiry ) { |
65 | if ( $value === false ) { |
66 | $value = []; |
67 | } |
68 | // note that some filters may have already had their keys expired |
69 | // we are currently filtering them out in getFiltersToCheckInGroup |
70 | // but if necessary, it can be done here |
71 | $value[$filter] = $expiry; |
72 | return $value; |
73 | }, |
74 | $expiry |
75 | ); |
76 | return $this->stash->set( |
77 | $this->createFilterKey( $filter ), |
78 | [ 'total' => 0, 'matches' => 0, 'expiry' => $expiry ], |
79 | $expiry |
80 | ); |
81 | } |
82 | |
83 | /** |
84 | * Increase the filter's 'total' value by one and possibly also the 'matched' value. |
85 | * |
86 | * @param int $filter |
87 | * @param bool $matched Whether the filter matched the action |
88 | * @return bool |
89 | */ |
90 | public function incrementForFilter( int $filter, bool $matched ): bool { |
91 | return $this->stash->merge( |
92 | $this->createFilterKey( $filter ), |
93 | static function ( $cache, $key, $value, &$expiry ) use ( $matched ) { |
94 | if ( $value === false ) { |
95 | return false; |
96 | } |
97 | $value['total']++; |
98 | if ( $matched ) { |
99 | $value['matches']++; |
100 | } |
101 | // enforce the prior TTL |
102 | $expiry = $value['expiry']; |
103 | return $value; |
104 | } |
105 | ); |
106 | } |
107 | |
108 | /** |
109 | * Get the cache entry for the filter. Returns false when the key has already expired. |
110 | * Otherwise it returns the entry formatted as [ 'total' => number of actions, |
111 | * 'matches' => number of hits ] (since the last filter modification). |
112 | * |
113 | * @param int $filter |
114 | * @return array|false |
115 | */ |
116 | public function getForFilter( int $filter ) { |
117 | $value = $this->stash->get( $this->createFilterKey( $filter ) ); |
118 | if ( $value !== false ) { |
119 | unset( $value['expiry'] ); |
120 | } |
121 | return $value; |
122 | } |
123 | |
124 | /** |
125 | * @param string $group |
126 | * @return string |
127 | */ |
128 | private function createGroupKey( string $group ): string { |
129 | return $this->stash->makeKey( 'abusefilter', 'emergency', 'group', $group ); |
130 | } |
131 | |
132 | /** |
133 | * @param int $filter |
134 | * @return string |
135 | */ |
136 | private function createFilterKey( int $filter ): string { |
137 | return $this->stash->makeKey( 'abusefilter', 'emergency', 'filter', $filter ); |
138 | } |
139 | |
140 | } |