Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 192
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockedExternalDomains
0.00% covered (danger)
0.00%
0 / 192
0.00% covered (danger)
0.00%
0 / 10
756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 showList
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
42
 doDomainRow
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 showRemoveForm
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
6
 processRemoveForm
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 showAddForm
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
6
 processAddForm
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isListed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20namespace MediaWiki\Extension\AbuseFilter\Special;
21
22use MediaWiki\Exception\ErrorPageError;
23use MediaWiki\Exception\PermissionsError;
24use MediaWiki\Extension\AbuseFilter\BlockedDomains\BlockedDomainValidator;
25use MediaWiki\Extension\AbuseFilter\BlockedDomains\IBlockedDomainStorage;
26use MediaWiki\Html\Html;
27use MediaWiki\HTMLForm\HTMLForm;
28use MediaWiki\SpecialPage\SpecialPage;
29use MediaWiki\Title\TitleValue;
30use Wikimedia\ObjectCache\WANObjectCache;
31use Wikimedia\Rdbms\IDBAccessObject;
32
33/**
34 * List and manage blocked external domains
35 *
36 * @ingroup SpecialPage
37 */
38class BlockedExternalDomains extends SpecialPage {
39    private IBlockedDomainStorage $blockedDomainStorage;
40    private BlockedDomainValidator $blockedDomainValidator;
41    private WANObjectCache $wanCache;
42
43    public function __construct(
44        IBlockedDomainStorage $blockedDomainStorage,
45        BlockedDomainValidator $blockedDomainValidator,
46        WANObjectCache $wanCache
47    ) {
48        parent::__construct( 'BlockedExternalDomains' );
49        $this->blockedDomainStorage = $blockedDomainStorage;
50        $this->blockedDomainValidator = $blockedDomainValidator;
51        $this->wanCache = $wanCache;
52    }
53
54    /** @inheritDoc */
55    public function execute( $par ) {
56        if ( !$this->getConfig()->get( 'AbuseFilterEnableBlockedExternalDomain' ) ) {
57            throw new ErrorPageError( 'abusefilter-disabled', 'disabledspecialpage-disabled' );
58        }
59        $this->setHeaders();
60        $this->outputHeader();
61        $this->addHelpLink( 'Manual:BlockedExternalDomains' );
62
63        $request = $this->getRequest();
64        switch ( $par ) {
65            case 'remove':
66                $this->showRemoveForm( $request->getVal( 'domain' ) );
67                break;
68            case 'add':
69                $this->showAddForm( $request->getVal( 'domain' ) );
70                break;
71            default:
72                $this->showList();
73                break;
74        }
75    }
76
77    private function showList() {
78        $out = $this->getOutput();
79        $out->setPageTitleMsg( $this->msg( 'abusefilter-blocked-domains-title' ) );
80        $out->wrapWikiMsg( "$1", 'abusefilter-blocked-domains-intro' );
81
82        // Direct editing of this page is blocked via EditPermissionHandler
83        $userCanManage = $this->getAuthority()->isAllowed( 'abusefilter-modify-blocked-external-domains' );
84
85        // Show form to add a blocked domain
86        if ( $userCanManage ) {
87            $fields = [
88                'Domain' => [
89                    'type' => 'text',
90                    'label' => $this->msg( 'abusefilter-blocked-domains-domain' )->plain(),
91                    'required' => true,
92                ],
93                'Notes' => [
94                    'type' => 'text',
95                    'maxlength' => 255,
96                    'label' => $this->msg( 'abusefilter-blocked-domains-notes' )->plain(),
97                    'size' => 250,
98                ],
99            ];
100
101            HTMLForm::factory( 'ooui', $fields, $this->getContext() )
102                ->setAction( $this->getPageTitle( 'add' )->getLocalURL() )
103                ->setWrapperLegendMsg( 'abusefilter-blocked-domains-add-heading' )
104                ->setHeaderHtml( $this->msg( 'abusefilter-blocked-domains-add-explanation' )->parseAsBlock() )
105                ->setSubmitCallback( [ $this, 'processAddForm' ] )
106                ->setSubmitTextMsg( 'abusefilter-blocked-domains-add-submit' )
107                ->show();
108
109            if ( $out->getRedirect() !== '' ) {
110                return;
111            }
112        }
113
114        $res = $this->blockedDomainStorage->loadConfig( IDBAccessObject::READ_LATEST );
115        if ( !$res->isGood() ) {
116            return;
117        }
118
119        $content = Html::element( 'th', [], $this->msg( 'abusefilter-blocked-domains-domain-header' )->text() ) .
120            Html::element( 'th', [], $this->msg( 'abusefilter-blocked-domains-notes-header' )->text() );
121        if ( $userCanManage ) {
122            $content .= Html::element(
123                'th',
124                [],
125                $this->msg( 'abusefilter-blocked-domains-addedby-header' )->text()
126            );
127            $content .= Html::element(
128                'th',
129                [ 'class' => 'unsortable' ],
130                $this->msg( 'abusefilter-blocked-domains-actions-header' )->text()
131            );
132        }
133        $thead = Html::rawElement( 'tr', [], $content );
134
135        // Parsing each row is expensive, put it behind WAN cache
136        // with md5 checksum, we make sure changes to the domain list
137        // invalidate the cache
138        $cacheKey = $this->wanCache->makeKey(
139            'abuse-filter-special-blocked-external-domains-rows',
140            md5( json_encode( $res->getValue() ) ),
141            (int)$userCanManage
142        );
143        $tbody = $this->wanCache->getWithSetCallback(
144            $cacheKey,
145            WANObjectCache::TTL_DAY,
146            function () use ( $res, $userCanManage ) {
147                $tbody = '';
148                foreach ( $res->getValue() as $domain ) {
149                    $tbody .= $this->doDomainRow( $domain, $userCanManage );
150                }
151                return $tbody;
152            }
153        );
154
155        $out->addModuleStyles( [ 'jquery.tablesorter.styles', 'mediawiki.pager.styles' ] );
156        $out->addModules( 'jquery.tablesorter' );
157        $out->addHTML( Html::rawElement(
158            'table',
159            [ 'class' => 'mw-datatable sortable' ],
160            Html::rawElement( 'thead', [], $thead ) .
161            Html::rawElement( 'tbody', [], $tbody )
162        ) );
163    }
164
165    /**
166     * Show the row in the table
167     *
168     * @param array $domain domain data
169     * @param bool $showManageActions whether to add manage actions
170     * @return string HTML for the row
171     */
172    private function doDomainRow( $domain, $showManageActions ) {
173        $newRow = Html::rawElement( 'td', [], Html::element( 'code', [], $domain['domain'] ) );
174
175        $newRow .= Html::rawElement( 'td', [], $this->getOutput()->parseInlineAsInterface( $domain['notes'] ) );
176
177        if ( $showManageActions ) {
178            if ( isset( $domain['addedBy'] ) ) {
179                $addedBy = $this->getLinkRenderer()->makeLink(
180                    new TitleValue( 3, $domain['addedBy'] ),
181                    $domain['addedBy']
182                );
183            } else {
184                $addedBy = '';
185            }
186            $newRow .= Html::rawElement( 'td', [], $addedBy );
187
188            $actionLink = $this->getLinkRenderer()->makeKnownLink(
189                $this->getPageTitle( 'remove' ),
190                $this->msg( 'abusefilter-blocked-domains-remove' )->text(),
191                [],
192                [ 'domain' => $domain['domain'] ]
193            );
194            $newRow .= Html::rawElement( 'td', [], $actionLink );
195        }
196
197        return Html::rawElement( 'tr', [], $newRow ) . "\n";
198    }
199
200    /**
201     * Show form for removing a domain from the blocked list
202     *
203     * @param string $domain
204     * @return void
205     */
206    private function showRemoveForm( $domain ) {
207        if ( !$this->getAuthority()->isAllowed( 'editsitejson' ) ) {
208            throw new PermissionsError( 'editsitejson' );
209        }
210
211        $out = $this->getOutput();
212        $out->setPageTitleMsg( $this->msg( 'abusefilter-blocked-domains-remove-title' ) );
213        $out->addBacklinkSubtitle( $this->getPageTitle() );
214
215        $preText = $this->msg( 'abusefilter-blocked-domains-remove-explanation-initial', $domain )->parseAsBlock();
216
217        $fields = [
218            'Domain' => [
219                'type' => 'text',
220                'label' => $this->msg( 'abusefilter-blocked-domains-domain' )->plain(),
221                'required' => true,
222                'default' => $domain,
223            ],
224            'Notes' => [
225                'type' => 'text',
226                'maxlength' => 255,
227                'label' => $this->msg( 'abusefilter-blocked-domains-notes' )->plain(),
228                'size' => 250,
229            ],
230        ];
231
232        HTMLForm::factory( 'ooui', $fields, $this->getContext() )
233            ->setAction( $this->getPageTitle( 'remove' )->getLocalURL() )
234            ->setSubmitCallback( function ( $data, $form ) {
235                return $this->processRemoveForm( $data, $form );
236            } )
237            ->setSubmitTextMsg( 'abusefilter-blocked-domains-remove-submit' )
238            ->setSubmitDestructive()
239            ->addPreHtml( $preText )
240            ->show();
241    }
242
243    /**
244     * Process the form for removing a domain from the blocked list
245     *
246     * @param array $data request data
247     * @param HTMLForm $form
248     * @return bool whether the action was successful or not
249     */
250    public function processRemoveForm( array $data, HTMLForm $form ) {
251        $out = $form->getContext()->getOutput();
252        $domain = $this->blockedDomainValidator->validateDomain( $data['Domain'] );
253        if ( $domain === false ) {
254            $out->wrapWikiTextAsInterface( 'error', 'Invalid URL' );
255            return false;
256        }
257
258        $status = $this->blockedDomainStorage->removeDomain(
259            $domain,
260            $data['Notes'] ?? '',
261            $this->getAuthority()
262        );
263
264        if ( !$status->isGood() ) {
265            $out->wrapWikiTextAsInterface( 'error', 'Save failed' );
266            return false;
267        }
268
269        $out->redirect( $this->getPageTitle()->getLocalURL() );
270        return true;
271    }
272
273    /**
274     * Show form for adding a domain to the blocked list
275     *
276     * @param string $domain
277     * @return void
278     */
279    private function showAddForm( $domain ) {
280        if ( !$this->getAuthority()->isAllowed( 'editsitejson' ) ) {
281            throw new PermissionsError( 'editsitejson' );
282        }
283
284        $out = $this->getOutput();
285        $out->setPageTitleMsg( $this->msg( "abusefilter-blocked-domains-add-heading" ) );
286        $out->addBacklinkSubtitle( $this->getPageTitle() );
287
288        $preText = $this->msg( "abusefilter-blocked-domains-add-explanation", $domain )->parseAsBlock();
289
290        $fields = [
291            'Domain' => [
292                'type' => 'text',
293                'label' => $this->msg( 'abusefilter-blocked-domains-domain' )->plain(),
294                'required' => true,
295                'default' => $domain,
296            ],
297            'Notes' => [
298                'type' => 'text',
299                'maxlength' => 255,
300                'label' => $this->msg( 'abusefilter-blocked-domains-notes' )->plain(),
301                'size' => 250,
302            ],
303        ];
304
305        HTMLForm::factory( 'ooui', $fields, $this->getContext() )
306            ->setAction( $this->getPageTitle( 'add' )->getLocalURL() )
307            ->setSubmitCallback( function ( $data, $form ) {
308                return $this->processAddForm( $data, $form );
309            } )
310            ->setSubmitTextMsg( "abusefilter-blocked-domains-add-submit" )
311            ->addPreHtml( $preText )
312            ->show();
313    }
314
315    /**
316     * Process the form for adding a domain to the blocked list
317     *
318     * @param array $data request data
319     * @param HTMLForm $form
320     * @return bool whether the action was successful or not
321     */
322    private function processAddForm( array $data, HTMLForm $form ) {
323        $out = $form->getContext()->getOutput();
324
325        $domain = $this->blockedDomainValidator->validateDomain( $data['Domain'] );
326        if ( $domain === false ) {
327            $out->wrapWikiTextAsInterface( 'error', 'Invalid URL' );
328            return false;
329        }
330        $status = $this->blockedDomainStorage->addDomain(
331            $domain,
332            $data['Notes'] ?? '',
333            $this->getAuthority()
334        );
335
336        if ( !$status->isGood() ) {
337            $out->wrapWikiTextAsInterface( 'error', 'Save failed' );
338            return false;
339        }
340
341        $out->redirect( $this->getPageTitle()->getLocalURL() );
342        return true;
343    }
344
345    /** @inheritDoc */
346    protected function getGroupName() {
347        return 'spam';
348    }
349
350    /** @inheritDoc */
351    public function isListed() {
352        return $this->getConfig()->get( 'AbuseFilterEnableBlockedExternalDomain' );
353    }
354}