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