Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.50% covered (warning)
87.50%
42 / 48
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockedDomainFilter
87.50% covered (warning)
87.50%
42 / 48
33.33% covered (danger)
33.33%
1 / 3
13.33
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.71% covered (warning)
85.71%
30 / 35
0.00% covered (danger)
0.00%
0 / 1
9.24
 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 ExtensionRegistry;
23use LogPage;
24use ManualLogEntry;
25use MediaWiki\CheckUser\Hooks as CUHooks;
26use MediaWiki\Extension\AbuseFilter\Variables\UnsetVariableException;
27use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
28use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager;
29use MediaWiki\Status\Status;
30use MediaWiki\Title\Title;
31use MediaWiki\User\User;
32use Message;
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, $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 of 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
110        $status = Status::newFatal( $error, 'blockeddomain', 'blockeddomain' );
111        $status->value['blockeddomain'] = [ 'disallow' ];
112        $this->logFilterHit(
113            $user,
114            $title,
115            implode( ' ', $blockedDomainsAdded )
116        );
117        return $status;
118    }
119
120    /**
121     * Logs the filter hit to Special:Log
122     *
123     * @param User $user
124     * @param Title $title
125     * @param string $blockedDomain The blocked domain the user attempted to add
126     */
127    private function logFilterHit( User $user, $title, $blockedDomain ) {
128        $logEntry = new ManualLogEntry( 'abusefilterblockeddomainhit', 'hit' );
129        $logEntry->setPerformer( $user );
130        $logEntry->setTarget( $title );
131        $logEntry->setParameters( [ '4::blocked' => $blockedDomain ] );
132        $logid = $logEntry->insert();
133        $log = new LogPage( 'abusefilterblockeddomainhit' );
134        if ( $log->isRestricted() ) {
135            // Make sure checkusers can see this action if the log is restricted
136            // (which is the default)
137            if ( ExtensionRegistry::getInstance()->isLoaded( 'CheckUser' ) ) {
138                $rc = $logEntry->getRecentChange( $logid );
139                CUHooks::updateCheckUserData( $rc );
140            }
141        } else {
142            // If the log is unrestricted, publish normally to RC,
143            // which will also update checkuser
144            $logEntry->publish( $logid, "rc" );
145        }
146    }
147}