Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
70 / 70
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
VariablesManager
100.00% covered (success)
100.00%
70 / 70
100.00% covered (success)
100.00%
7 / 7
29
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
 translateDeprecatedVars
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getVar
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 dumpAllVars
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
9
 computeDBVars
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
3
 exportAllVars
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 exportNonLazyVars
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\Variables;
4
5use LogicException;
6use MediaWiki\Extension\AbuseFilter\KeywordsManager;
7use MediaWiki\Extension\AbuseFilter\Parser\AFPData;
8
9/**
10 * Service that allows manipulating a VariableHolder
11 */
12class VariablesManager {
13    public const SERVICE_NAME = 'AbuseFilterVariablesManager';
14    /**
15     * Used in self::getVar() to determine what to do if the requested variable is missing. See
16     * the docs of that method for an explanation.
17     */
18    public const GET_LAX = 0;
19    public const GET_STRICT = 1;
20    public const GET_BC = 2;
21
22    /** @var KeywordsManager */
23    private $keywordsManager;
24    /** @var LazyVariableComputer */
25    private $lazyComputer;
26
27    /**
28     * @param KeywordsManager $keywordsManager
29     * @param LazyVariableComputer $lazyComputer
30     */
31    public function __construct(
32        KeywordsManager $keywordsManager,
33        LazyVariableComputer $lazyComputer
34    ) {
35        $this->keywordsManager = $keywordsManager;
36        $this->lazyComputer = $lazyComputer;
37    }
38
39    /**
40     * Checks whether any deprecated variable is stored with the old name, and replaces it with
41     * the new name. This should normally only happen when a DB dump is retrieved from the DB.
42     *
43     * @param VariableHolder $holder
44     */
45    public function translateDeprecatedVars( VariableHolder $holder ): void {
46        $deprecatedVars = $this->keywordsManager->getDeprecatedVariables();
47        foreach ( $holder->getVars() as $name => $value ) {
48            if ( array_key_exists( $name, $deprecatedVars ) ) {
49                $holder->setVar( $deprecatedVars[$name], $value );
50                $holder->removeVar( $name );
51            }
52        }
53    }
54
55    /**
56     * Get a variable from the current object
57     *
58     * @param VariableHolder $holder
59     * @param string $varName The variable name
60     * @param int $mode One of the self::GET_* constants, determines how to behave when the variable is unset:
61     *  - GET_STRICT -> In the future, this will throw an exception. For now it returns a DUNDEFINED and logs a warning
62     *  - GET_LAX -> Return a DUNDEFINED AFPData
63     *  - GET_BC -> Return a DNULL AFPData (this should only be used for BC, see T230256)
64     * @return AFPData
65     */
66    public function getVar(
67        VariableHolder $holder,
68        string $varName,
69        $mode = self::GET_STRICT
70    ): AFPData {
71        $varName = strtolower( $varName );
72        if ( $holder->varIsSet( $varName ) ) {
73            /** @var $variable LazyLoadedVariable|AFPData */
74            $variable = $holder->getVarThrow( $varName );
75            if ( $variable instanceof LazyLoadedVariable ) {
76                $getVarCB = function ( string $varName ) use ( $holder ): AFPData {
77                    return $this->getVar( $holder, $varName );
78                };
79                $value = $this->lazyComputer->compute( $variable, $holder, $getVarCB );
80                $holder->setVar( $varName, $value );
81                return $value;
82            } elseif ( $variable instanceof AFPData ) {
83                return $variable;
84            } else {
85                // @codeCoverageIgnoreStart
86                throw new \UnexpectedValueException(
87                    "Variable $varName has unexpected type " . gettype( $variable )
88                );
89                // @codeCoverageIgnoreEnd
90            }
91        }
92
93        // The variable is not set.
94        switch ( $mode ) {
95            case self::GET_STRICT:
96                throw new UnsetVariableException( $varName );
97            case self::GET_LAX:
98                return new AFPData( AFPData::DUNDEFINED );
99            case self::GET_BC:
100                // Old behaviour, which can sometimes lead to unexpected results (e.g.
101                // `edit_delta < -5000` will match any non-edit action).
102                return new AFPData( AFPData::DNULL );
103            default:
104                // @codeCoverageIgnoreStart
105                throw new LogicException( "Mode '$mode' not recognized." );
106                // @codeCoverageIgnoreEnd
107        }
108    }
109
110    /**
111     * Dump all variables stored in the holder in their native types.
112     * If you want a not yet set variable to be included in the results you can
113     * either set $compute to an array with the name of the variable or set
114     * $compute to true to compute all not yet set variables.
115     *
116     * @param VariableHolder $holder
117     * @param array|bool $compute Variables we should compute if not yet set
118     * @param bool $includeUserVars Include user set variables
119     * @return array
120     */
121    public function dumpAllVars(
122        VariableHolder $holder,
123        $compute = [],
124        bool $includeUserVars = false
125    ): array {
126        $coreVariables = [];
127
128        if ( !$includeUserVars ) {
129            // Compile a list of all variables set by the extension to be able
130            // to filter user set ones by name
131            $activeVariables = array_keys( $this->keywordsManager->getVarsMappings() );
132            $deprecatedVariables = array_keys( $this->keywordsManager->getDeprecatedVariables() );
133            $disabledVariables = array_keys( $this->keywordsManager->getDisabledVariables() );
134            $coreVariables = array_merge( $activeVariables, $deprecatedVariables, $disabledVariables );
135            $coreVariables = array_map( 'strtolower', $coreVariables );
136        }
137
138        $exported = [];
139        foreach ( array_keys( $holder->getVars() ) as $varName ) {
140            $computeThis = ( is_array( $compute ) && in_array( $varName, $compute ) ) || $compute === true;
141            if (
142                ( $includeUserVars || in_array( strtolower( $varName ), $coreVariables ) ) &&
143                // Only include variables set in the extension in case $includeUserVars is false
144                ( $computeThis || $holder->getVarThrow( $varName ) instanceof AFPData )
145            ) {
146                $exported[$varName] = $this->getVar( $holder, $varName )->toNative();
147            }
148        }
149
150        return $exported;
151    }
152
153    /**
154     * Compute all vars which need DB access. Useful for vars which are going to be saved
155     * cross-wiki or used for offline analysis.
156     *
157     * @param VariableHolder $holder
158     */
159    public function computeDBVars( VariableHolder $holder ): void {
160        static $dbTypes = [
161            'links-from-database',
162            'links-from-update',
163            'links-from-wikitext-or-database',
164            'load-recent-authors',
165            'page-age',
166            'get-page-restrictions',
167            'user-editcount',
168            'user-emailconfirm',
169            'user-groups',
170            'user-rights',
171            'user-age',
172            'user-block',
173            'revision-text-by-id',
174            'content-model-by-id',
175        ];
176
177        /** @var LazyLoadedVariable[] $missingVars */
178        $missingVars = array_filter( $holder->getVars(), static function ( $el ) {
179            return ( $el instanceof LazyLoadedVariable );
180        } );
181        foreach ( $missingVars as $name => $var ) {
182            if ( in_array( $var->getMethod(), $dbTypes ) ) {
183                $holder->setVar( $name, $this->getVar( $holder, $name ) );
184            }
185        }
186    }
187
188    /**
189     * Export all variables stored in this object with their native (PHP) types.
190     *
191     * @param VariableHolder $holder
192     * @return array
193     */
194    public function exportAllVars( VariableHolder $holder ): array {
195        $exported = [];
196        foreach ( array_keys( $holder->getVars() ) as $varName ) {
197            $exported[ $varName ] = $this->getVar( $holder, $varName )->toNative();
198        }
199
200        return $exported;
201    }
202
203    /**
204     * Export all non-lazy variables stored in this object as string
205     *
206     * @param VariableHolder $holder
207     * @return string[]
208     */
209    public function exportNonLazyVars( VariableHolder $holder ): array {
210        $exported = [];
211        foreach ( $holder->getVars() as $varName => $data ) {
212            if ( !( $data instanceof LazyLoadedVariable ) ) {
213                $exported[$varName] = $holder->getComputedVariable( $varName )->toString();
214            }
215        }
216
217        return $exported;
218    }
219}