Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
EmergencyCache
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
7 / 7
12
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getFiltersToCheckInGroup
100.00% covered (success)
100.00%
10 / 10
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 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    /** @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}