Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
CentralAuthAntiSpoofManager
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 4
272
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getSpoofUser
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 testNewAccount
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
110
 getOldRenamedUserName
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace MediaWiki\Extension\CentralAuth\User;
4
5use MediaWiki\Config\ServiceOptions;
6use MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager;
7use MediaWiki\Extension\CentralAuth\Config\CAMainConfigNames;
8use MediaWiki\Language\RawMessage;
9use MediaWiki\Message\Message;
10use MediaWiki\User\User;
11use Psr\Log\LoggerInterface;
12use StatusValue;
13use Wikimedia\Rdbms\IConnectionProvider;
14
15class CentralAuthAntiSpoofManager {
16
17    /** @internal Only public for service wiring use. */
18    public const CONSTRUCTOR_OPTIONS = [
19        CAMainConfigNames::CentralAuthOldNameAntiSpoofWiki,
20    ];
21
22    private ServiceOptions $options;
23    private LoggerInterface $logger;
24    private IConnectionProvider $connectionProvider;
25    private CentralAuthDatabaseManager $databaseManager;
26
27    /**
28     * @param ServiceOptions $options
29     * @param LoggerInterface $logger
30     * @param IConnectionProvider $connectionProvider
31     * @param CentralAuthDatabaseManager $databaseManager
32     */
33    public function __construct(
34        ServiceOptions $options,
35        LoggerInterface $logger,
36        IConnectionProvider $connectionProvider,
37        CentralAuthDatabaseManager $databaseManager
38    ) {
39        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
40        $this->options = $options;
41        $this->logger = $logger;
42        $this->connectionProvider = $connectionProvider;
43        $this->databaseManager = $databaseManager;
44    }
45
46    /**
47     * @param string $name
48     * @return CentralAuthSpoofUser
49     */
50    public function getSpoofUser( string $name ): CentralAuthSpoofUser {
51        return new CentralAuthSpoofUser(
52            $name,
53            $this->databaseManager
54        );
55    }
56
57    /**
58     * Test if an account is acceptable
59     *
60     * @param User $user
61     * @param User $creator
62     * @param bool $enable
63     * @param bool $override
64     * @param LoggerInterface|null $logger
65     *
66     * @return StatusValue
67     */
68    public function testNewAccount( $user, $creator, $enable, $override, $logger = null ) {
69        $logger ??= $this->logger;
70
71        if ( !$enable ) {
72            $mode = 'LOGGING ';
73            $active = false;
74        } elseif ( $override && $creator->isAllowed( 'override-antispoof' ) ) {
75            $mode = 'OVERRIDE ';
76            $active = false;
77        } else {
78            $mode = '';
79            $active = true;
80        }
81
82        $name = $user->getName();
83        $spoof = $this->getSpoofUser( $name );
84        if ( $spoof->isLegal() ) {
85            $normalized = $spoof->getNormalized();
86            $conflicts = $spoof->getConflicts();
87            $oldUserName = $this->getOldRenamedUserName( $name );
88            if ( $oldUserName !== null ) {
89                $conflicts[] = $oldUserName;
90            }
91            if ( !$conflicts ) {
92                $logger->info( "{$mode}PASS new account '$name' [$normalized]" );
93            } else {
94                $logger->info( "{$mode}CONFLICT new account '$name' [$normalized] spoofs " .
95                    implode( ',', $conflicts ) );
96
97                if ( $active ) {
98                    $list = [];
99                    foreach ( $conflicts as $simUser ) {
100                        $list[] = "* " . wfMessage( 'antispoof-conflict-item', $simUser )->plain();
101                    }
102                    $list = implode( "\n", $list );
103
104                    return StatusValue::newFatal(
105                        'antispoof-conflict',
106                        $name,
107                        Message::numParam( count( $conflicts ) ),
108                        // Avoid forced wikitext escaping for params in the Status class
109                        new RawMessage( $list )
110                    );
111                }
112            }
113        } else {
114            $error = $spoof->getErrorStatus();
115            $logger->info( "{mode}ILLEGAL new account '{name}' {error}", [
116                'mode' => $mode,
117                'name' => $name,
118                'error' => $error->getMessage( false, false, 'en' )->text(),
119            ] );
120            if ( $active ) {
121                return StatusValue::newFatal( 'antispoof-name-illegal', $name, $error->getMessage() );
122            }
123        }
124        return StatusValue::newGood();
125    }
126
127    /**
128     * Given a username, find the old name
129     *
130     * @param string $name Name to lookup
131     *
132     * @return null|string Old username, or null
133     */
134    public function getOldRenamedUserName( $name ) {
135        $dbrLogWiki = $this->connectionProvider->getReplicaDatabase(
136            // If nobody has set this variable, it will be false,
137            // which will mean the current wiki, which sounds like as
138            // good a default as we can get.
139            $this->options->get( CAMainConfigNames::CentralAuthOldNameAntiSpoofWiki )
140        );
141
142        $newNameOfUser = $dbrLogWiki->newSelectQueryBuilder()
143            ->select( 'log_title' )
144            ->from( 'logging' )
145            ->join( 'log_search', null, 'ls_log_id=log_id' )
146            ->where( [
147                'ls_field' => 'oldname',
148                'ls_value' => $name,
149                'log_type' => 'gblrename',
150                'log_namespace' => NS_SPECIAL
151            ] )
152            ->caller( __METHOD__ )
153            ->fetchField();
154        $slashPos = strpos( $newNameOfUser ?: '', '/' );
155        if ( $newNameOfUser && $slashPos ) {
156            // We have to remove the Special:CentralAuth prefix.
157            return substr( $newNameOfUser, $slashPos + 1 );
158        }
159        return null;
160    }
161}