Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.23% covered (warning)
87.23%
41 / 47
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockedDomainFilter
87.23% covered (warning)
87.23%
41 / 47
33.33% covered (danger)
33.33%
1 / 3
13.35
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 filter
85.29% covered (warning)
85.29%
29 / 34
0.00% covered (danger)
0.00%
0 / 1
9.26
 logFilterHit
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
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;
21
22use LogPage;
23use ManualLogEntry;
24use MediaWiki\CheckUser\Hooks as CUHooks;
25use MediaWiki\Extension\AbuseFilter\Variables\UnsetVariableException;
26use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
27use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager;
28use MediaWiki\Message\Message;
29use MediaWiki\Registration\ExtensionRegistry;
30use MediaWiki\Status\Status;
31use MediaWiki\Title\Title;
32use MediaWiki\User\User;
33
34/**
35 * Filters blocked domains
36 *
37 * @ingroup SpecialPage
38 */
39class BlockedDomainFilter {
40    public const SERVICE_NAME = 'AbuseFilterBlockedDomainFilter';
41    private VariablesManager $variablesManager;
42    private BlockedDomainStorage $blockedDomainStorage;
43
44    /**
45     * @param VariablesManager $variablesManager
46     * @param BlockedDomainStorage $blockedDomainStorage
47     */
48    public function __construct(
49        VariablesManager $variablesManager,
50        BlockedDomainStorage $blockedDomainStorage
51    ) {
52        $this->variablesManager = $variablesManager;
53        $this->blockedDomainStorage = $blockedDomainStorage;
54    }
55
56    /**
57     * @param VariableHolder $vars variables by the action
58     * @param User $user User that tried to add the domain, used for logging
59     * @param Title $title Title of the page that was attempted on, used for logging
60     * @return Status Error status if it's a match, good status if not
61     */
62    public function filter( VariableHolder $vars, User $user, Title $title ) {
63        global $wgAbuseFilterEnableBlockedExternalDomain;
64        $status = Status::newGood();
65        if ( !$wgAbuseFilterEnableBlockedExternalDomain ) {
66            return $status;
67        }
68        try {
69            $urls = $this->variablesManager->getVar( $vars, 'added_links', VariablesManager::GET_STRICT );
70        } catch ( UnsetVariableException $_ ) {
71            return $status;
72        }
73
74        $addedDomains = [];
75        foreach ( $urls->toArray() as $addedUrl ) {
76            $parsedHost = parse_url( (string)$addedUrl->getData(), PHP_URL_HOST );
77            if ( !is_string( $parsedHost ) ) {
78                continue;
79            }
80            // Given that we block subdomains of blocked domains too
81            // pretend that all the higher-level domains are added as well
82            // so for foo.bar.com, you will have three domains to check:
83            // foo.bar.com, bar.com, and com
84            // This saves string search in the large list of blocked domains
85            // making it much faster.
86            $domainString = '';
87            $domainPieces = array_reverse( explode( '.', strtolower( $parsedHost ) ) );
88            foreach ( $domainPieces as $domainPiece ) {
89                if ( !$domainString ) {
90                    $domainString = $domainPiece;
91                } else {
92                    $domainString = $domainPiece . '.' . $domainString;
93                }
94                // It should be a map, benchmark at https://phabricator.wikimedia.org/P48956
95                $addedDomains[$domainString] = true;
96            }
97        }
98        if ( !$addedDomains ) {
99            return $status;
100        }
101        $blockedDomains = $this->blockedDomainStorage->loadComputed();
102        $blockedDomainsAdded = array_intersect_key( $addedDomains, $blockedDomains );
103        if ( !$blockedDomainsAdded ) {
104            return $status;
105        }
106        $blockedDomainsAdded = array_keys( $blockedDomainsAdded );
107        $error = Message::newFromSpecifier( 'abusefilter-blocked-domains-attempted' );
108        $error->params( Message::listParam( $blockedDomainsAdded ) );
109        $status->fatal( $error );
110        $this->logFilterHit(
111            $user,
112            $title,
113            implode( ' ', $blockedDomainsAdded )
114        );
115        return $status;
116    }
117
118    /**
119     * Logs the filter hit to Special:Log
120     *
121     * @param User $user
122     * @param Title $title
123     * @param string $blockedDomain The blocked domain the user attempted to add
124     */
125    private function logFilterHit( User $user, Title $title, string $blockedDomain ) {
126        $logEntry = new ManualLogEntry( 'abusefilterblockeddomainhit', 'hit' );
127        $logEntry->setPerformer( $user );
128        $logEntry->setTarget( $title );
129        $logEntry->setParameters( [ '4::blocked' => $blockedDomain ] );
130        $logid = $logEntry->insert();
131        $log = new LogPage( 'abusefilterblockeddomainhit' );
132        if ( $log->isRestricted() ) {
133            // Make sure checkusers can see this action if the log is restricted
134            // (which is the default)
135            if ( ExtensionRegistry::getInstance()->isLoaded( 'CheckUser' ) ) {
136                $rc = $logEntry->getRecentChange( $logid );
137                CUHooks::updateCheckUserData( $rc );
138            }
139        } else {
140            // If the log is unrestricted, publish normally to RC,
141            // which will also update checkuser
142            $logEntry->publish( $logid, "rc" );
143        }
144    }
145}