Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
EditStashCache
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
5 / 5
7
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 store
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 seek
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 logCache
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getStashKey
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter;
4
5use InvalidArgumentException;
6use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
7use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager;
8use MediaWiki\Linker\LinkTarget;
9use Psr\Log\LoggerInterface;
10use Wikimedia\ObjectCache\BagOStuff;
11use Wikimedia\Stats\IBufferingStatsdDataFactory;
12
13/**
14 * Wrapper around cache for storing and retrieving data from edit stash
15 */
16class EditStashCache {
17
18    private const CACHE_VERSION = 'v5';
19
20    /** @var BagOStuff */
21    private $cache;
22
23    /** @var IBufferingStatsdDataFactory */
24    private $statsdDataFactory;
25
26    /** @var VariablesManager */
27    private $variablesManager;
28
29    /** @var LoggerInterface */
30    private $logger;
31
32    /** @var LinkTarget */
33    private $target;
34
35    /** @var string */
36    private $group;
37
38    /**
39     * @param BagOStuff $cache
40     * @param IBufferingStatsdDataFactory $statsdDataFactory
41     * @param VariablesManager $variablesManager
42     * @param LoggerInterface $logger
43     * @param LinkTarget $target
44     * @param string $group
45     */
46    public function __construct(
47        BagOStuff $cache,
48        IBufferingStatsdDataFactory $statsdDataFactory,
49        VariablesManager $variablesManager,
50        LoggerInterface $logger,
51        LinkTarget $target,
52        string $group
53    ) {
54        $this->cache = $cache;
55        $this->statsdDataFactory = $statsdDataFactory;
56        $this->variablesManager = $variablesManager;
57        $this->logger = $logger;
58        $this->target = $target;
59        $this->group = $group;
60    }
61
62    /**
63     * @param VariableHolder $vars For creating the key
64     * @param array $data Data to store
65     */
66    public function store( VariableHolder $vars, array $data ): void {
67        $key = $this->getStashKey( $vars );
68        $this->cache->set( $key, $data, BagOStuff::TTL_MINUTE );
69        $this->logCache( 'store', $key );
70    }
71
72    /**
73     * Search the cache to find data for a previous execution done for the current edit.
74     *
75     * @param VariableHolder $vars For creating the key
76     * @return false|array False on cache miss, the array with data otherwise
77     */
78    public function seek( VariableHolder $vars ) {
79        $key = $this->getStashKey( $vars );
80        $value = $this->cache->get( $key );
81        $status = $value !== false ? 'hit' : 'miss';
82        $this->logCache( $status, $key );
83        return $value;
84    }
85
86    /**
87     * Log cache operations related to stashed edits, i.e. store, hit and miss
88     *
89     * @param string $type Either 'store', 'hit' or 'miss'
90     * @param string $key The cache key used
91     * @throws InvalidArgumentException
92     */
93    private function logCache( string $type, string $key ): void {
94        if ( !in_array( $type, [ 'store', 'hit', 'miss' ] ) ) {
95            // @codeCoverageIgnoreStart
96            throw new InvalidArgumentException( '$type must be either "store", "hit" or "miss"' );
97            // @codeCoverageIgnoreEnd
98        }
99        $this->logger->debug(
100            __METHOD__ . ": cache {logtype} for '{target}' (key {key}).",
101            [ 'logtype' => $type, 'target' => $this->target, 'key' => $key ]
102        );
103        $this->statsdDataFactory->increment( "abusefilter.check-stash.$type" );
104    }
105
106    /**
107     * Get the stash key for the current variables
108     *
109     * @param VariableHolder $vars
110     * @return string
111     */
112    private function getStashKey( VariableHolder $vars ): string {
113        $inputVars = $this->variablesManager->exportNonLazyVars( $vars );
114        // Exclude noisy fields that have superficial changes
115        $excludedVars = [
116            'old_html' => true,
117            'new_html' => true,
118            'user_age' => true,
119            'timestamp' => true,
120            'page_age' => true,
121            'page_last_edit_age' => true,
122        ];
123
124        $inputVars = array_diff_key( $inputVars, $excludedVars );
125        ksort( $inputVars );
126        $hash = md5( serialize( $inputVars ) );
127
128        return $this->cache->makeKey(
129            'abusefilter',
130            'check-stash',
131            $this->group,
132            $hash,
133            self::CACHE_VERSION
134        );
135    }
136
137}