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