Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
SpecialAbuseFilter
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
7 / 7
32
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
 getRestriction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doesWrites
n/a
0 / 0
n/a
0 / 0
1
 getGroupName
n/a
0 / 0
n/a
0 / 0
1
 execute
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
2
 instantiateView
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getViewClassAndPageType
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
21
 isPositiveInteger
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getTitleForSubpage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\Special;
4
5use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
6use MediaWiki\Extension\AbuseFilter\AbuseLoggerFactory;
7use MediaWiki\Extension\AbuseFilter\CentralDBManager;
8use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesFactory;
9use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesRegistry;
10use MediaWiki\Extension\AbuseFilter\EditBox\EditBoxBuilderFactory;
11use MediaWiki\Extension\AbuseFilter\FilterImporter;
12use MediaWiki\Extension\AbuseFilter\FilterLookup;
13use MediaWiki\Extension\AbuseFilter\FilterProfiler;
14use MediaWiki\Extension\AbuseFilter\FilterStore;
15use MediaWiki\Extension\AbuseFilter\Parser\RuleCheckerFactory;
16use MediaWiki\Extension\AbuseFilter\SpecsFormatter;
17use MediaWiki\Extension\AbuseFilter\VariableGenerator\VariableGeneratorFactory;
18use MediaWiki\Extension\AbuseFilter\Variables\VariablesBlobStore;
19use MediaWiki\Extension\AbuseFilter\Variables\VariablesFormatter;
20use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager;
21use MediaWiki\Extension\AbuseFilter\View\AbuseFilterView;
22use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewDiff;
23use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewEdit;
24use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewExamine;
25use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewHistory;
26use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewImport;
27use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewList;
28use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewRevert;
29use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewTestBatch;
30use MediaWiki\Extension\AbuseFilter\View\AbuseFilterViewTools;
31use MediaWiki\Html\Html;
32use MediaWiki\Title\Title;
33use Wikimedia\ObjectFactory\ObjectFactory;
34
35class SpecialAbuseFilter extends AbuseFilterSpecialPage {
36
37    private const PAGE_NAME = 'AbuseFilter';
38
39    private const SERVICES_PER_VIEW = [
40        AbuseFilterViewDiff::class => [
41            AbuseFilterPermissionManager::SERVICE_NAME,
42            SpecsFormatter::SERVICE_NAME,
43            FilterLookup::SERVICE_NAME,
44        ],
45        AbuseFilterViewEdit::class => [
46            'DBLoadBalancerFactory',
47            AbuseFilterPermissionManager::SERVICE_NAME,
48            FilterProfiler::SERVICE_NAME,
49            FilterLookup::SERVICE_NAME,
50            FilterImporter::SERVICE_NAME,
51            FilterStore::SERVICE_NAME,
52            EditBoxBuilderFactory::SERVICE_NAME,
53            ConsequencesRegistry::SERVICE_NAME,
54            SpecsFormatter::SERVICE_NAME,
55        ],
56        AbuseFilterViewExamine::class => [
57            'DBLoadBalancerFactory',
58            AbuseFilterPermissionManager::SERVICE_NAME,
59            FilterLookup::SERVICE_NAME,
60            EditBoxBuilderFactory::SERVICE_NAME,
61            VariablesBlobStore::SERVICE_NAME,
62            VariablesFormatter::SERVICE_NAME,
63            VariablesManager::SERVICE_NAME,
64            VariableGeneratorFactory::SERVICE_NAME,
65            AbuseLoggerFactory::SERVICE_NAME,
66            'RecentChangeStore',
67            'ReadOnlyMode',
68        ],
69        AbuseFilterViewHistory::class => [
70            'UserNameUtils',
71            'LinkBatchFactory',
72            AbuseFilterPermissionManager::SERVICE_NAME,
73            FilterLookup::SERVICE_NAME,
74            SpecsFormatter::SERVICE_NAME,
75        ],
76        AbuseFilterViewImport::class => [
77            AbuseFilterPermissionManager::SERVICE_NAME,
78        ],
79        AbuseFilterViewList::class => [
80            'LinkBatchFactory',
81            'ConnectionProvider',
82            AbuseFilterPermissionManager::SERVICE_NAME,
83            FilterProfiler::SERVICE_NAME,
84            SpecsFormatter::SERVICE_NAME,
85            CentralDBManager::SERVICE_NAME,
86            FilterLookup::SERVICE_NAME,
87        ],
88        AbuseFilterViewRevert::class => [
89            'DBLoadBalancerFactory',
90            'UserFactory',
91            AbuseFilterPermissionManager::SERVICE_NAME,
92            FilterLookup::SERVICE_NAME,
93            ConsequencesFactory::SERVICE_NAME,
94            VariablesBlobStore::SERVICE_NAME,
95            SpecsFormatter::SERVICE_NAME,
96        ],
97        AbuseFilterViewTestBatch::class => [
98            'DBLoadBalancerFactory',
99            AbuseFilterPermissionManager::SERVICE_NAME,
100            EditBoxBuilderFactory::SERVICE_NAME,
101            RuleCheckerFactory::SERVICE_NAME,
102            VariableGeneratorFactory::SERVICE_NAME,
103            AbuseLoggerFactory::SERVICE_NAME,
104            'RecentChangeFactory',
105            'ReadOnlyMode',
106        ],
107        AbuseFilterViewTools::class => [
108            AbuseFilterPermissionManager::SERVICE_NAME,
109            EditBoxBuilderFactory::SERVICE_NAME,
110        ],
111    ];
112
113    public function __construct(
114        AbuseFilterPermissionManager $afPermissionManager,
115        private readonly ObjectFactory $objectFactory
116    ) {
117        parent::__construct( self::PAGE_NAME, $afPermissionManager );
118    }
119
120    /** @inheritDoc */
121    public function getRestriction(): string {
122        return 'abusefilter-view';
123    }
124
125    /**
126     * @codeCoverageIgnore Merely declarative
127     * @inheritDoc
128     */
129    public function doesWrites() {
130        return true;
131    }
132
133    /**
134     * @codeCoverageIgnore Merely declarative
135     * @inheritDoc
136     */
137    protected function getGroupName() {
138        return 'wiki';
139    }
140
141    /**
142     * @param string|null $subpage
143     */
144    public function execute( $subpage ) {
145        $out = $this->getOutput();
146
147        $out->addModuleStyles( 'ext.abuseFilter' );
148        $this->setHeaders();
149        $this->addHelpLink( 'Extension:AbuseFilter' );
150
151        $this->checkPermissions();
152
153        $session = $this->getContext()->getRequest()->getSession();
154        $successData = $session->get( AbuseFilterViewEdit::EDIT_SUCCESS_SESSION_KEY );
155        if ( $successData !== null ) {
156            // Remove session data for the success message
157            $session->remove( AbuseFilterViewEdit::EDIT_SUCCESS_SESSION_KEY );
158
159            $changedFilter = $successData['changedFilter'];
160            $changeId = $successData['changeId'];
161
162            $out->setSubtitle( $this->msg( 'abusefilter-edit-done-subtitle' ) );
163            $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
164            $out->addHTML( Html::successBox(
165                $this->msg(
166                    'abusefilter-edit-done',
167                    $changedFilter,
168                    $changeId,
169                    $this->getLanguage()->formatNum( $changedFilter )
170                )->parse()
171            ) );
172        }
173
174        [ $view, $pageType, $params ] = $this->getViewClassAndPageType( $subpage );
175
176        // Links at the top
177        $this->addNavigationLinks( $pageType );
178
179        $view = $this->instantiateView( $view, $params );
180        $view->show();
181    }
182
183    /**
184     * Instantiate the view class
185     *
186     * @param class-string<AbuseFilterView> $viewClass
187     * @param array $params
188     * @return AbuseFilterView
189     */
190    public function instantiateView( string $viewClass, array $params ): AbuseFilterView {
191        return $this->objectFactory->createObject( [
192            'class' => $viewClass,
193            'services' => self::SERVICES_PER_VIEW[$viewClass],
194            'args' => [ $this->getContext(), $this->getLinkRenderer(), self::PAGE_NAME, $params ]
195        ] );
196    }
197
198    /**
199     * Determine the view class to instantiate
200     *
201     * @param string|null $subpage
202     * @return array{0:class-string,1:string,2:array} A tuple of three elements:
203     *      - a subclass of AbuseFilterView
204     *      - type of page for addNavigationLinks
205     *      - array of parameters for the class
206     */
207    public function getViewClassAndPageType( ?string $subpage ): array {
208        // Filter by removing blanks.
209        $params = preg_split( '{/+}', $subpage ?: '' );
210
211        if ( $subpage === 'tools' ) {
212            return [ AbuseFilterViewTools::class, 'tools', [] ];
213        }
214
215        if ( $subpage === 'import' ) {
216            return [ AbuseFilterViewImport::class, 'import', [] ];
217        }
218
219        if ( is_numeric( $subpage ) || $subpage === 'new' ) {
220            return [
221                AbuseFilterViewEdit::class,
222                'edit',
223                [ 'filter' => is_numeric( $subpage ) ? (int)$subpage : null ]
224            ];
225        }
226
227        if ( $params ) {
228            if (
229                count( $params ) === 2 && $params[0] === 'revert' &&
230                self::isPositiveInteger( $params[1] )
231            ) {
232                return [ AbuseFilterViewRevert::class, 'revert', [ 'filter' => (int)$params[1] ] ];
233            }
234
235            if ( $params[0] === 'test' ) {
236                return [ AbuseFilterViewTestBatch::class, 'test', $params ];
237            }
238
239            if ( $params[0] === 'examine' ) {
240                return [ AbuseFilterViewExamine::class, 'examine', $params ];
241            }
242
243            if ( $params[0] === 'history' || $params[0] === 'log' ) {
244                if ( count( $params ) <= 2 ) {
245                    $params = isset( $params[1] ) ? [ 'filter' => (int)$params[1] ] : [];
246                    return [ AbuseFilterViewHistory::class, 'recentchanges', $params ];
247                }
248                if ( count( $params ) === 4 && $params[2] === 'item' ) {
249                    return [
250                        AbuseFilterViewEdit::class,
251                        '',
252                        [ 'filter' => (int)$params[1], 'history' => (int)$params[3] ]
253                    ];
254                }
255                if ( count( $params ) === 5 && $params[2] === 'diff' ) {
256                    // Special:AbuseFilter/history/<filter>/diff/<oldid>/<newid>
257                    return [ AbuseFilterViewDiff::class, '', $params ];
258                }
259            }
260        }
261
262        return [ AbuseFilterViewList::class, 'home', [] ];
263    }
264
265    /**
266     * Checks whether the given value represents a positive integer.
267     *
268     * @param string|int $num
269     * @return bool
270     * @todo Use this method throughout the module.
271     */
272    private static function isPositiveInteger( string|int $num ): bool {
273        if ( is_string( $num ) && !ctype_digit( $num ) ) {
274            return false;
275        }
276        return (int)$num > 0;
277    }
278
279    /**
280     * Static variant to get the associated Title.
281     *
282     * @param string|int $subpage
283     * @return Title
284     */
285    public static function getTitleForSubpage( $subpage ): Title {
286        return self::getTitleFor( self::PAGE_NAME, $subpage );
287    }
288}