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