Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.35% covered (warning)
54.35%
25 / 46
45.45% covered (danger)
45.45%
5 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
CentralAuthDatabaseManager
54.35% covered (warning)
54.35%
25 / 46
45.45% covered (danger)
45.45%
5 / 11
62.96
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getLoadBalancer
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 assertNotReadOnly
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 isReadOnly
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getCentralReadOnlyReason
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 waitForReplication
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCentralPrimaryDB
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getCentralReplicaDB
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getCentralDB
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 getLocalDB
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 centralLBHasRecentPrimaryChanges
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\CentralAuth;
4
5use InvalidArgumentException;
6use MediaWiki\Config\ServiceOptions;
7use MediaWiki\WikiMap\WikiMap;
8use ReadOnlyError;
9use Wikimedia\Rdbms\IDatabase;
10use Wikimedia\Rdbms\ILoadBalancer;
11use Wikimedia\Rdbms\IReadableDatabase;
12use Wikimedia\Rdbms\LBFactory;
13use Wikimedia\Rdbms\ReadOnlyMode;
14
15/**
16 * Service providing access to the CentralAuth internal database.
17 *
18 * @since 1.37
19 * @author Taavi "Majavah" Väänänen <hi@taavi.wtf>
20 */
21class CentralAuthDatabaseManager {
22    /** @internal Only public for service wiring use */
23    public const CONSTRUCTOR_OPTIONS = [
24        'CentralAuthDatabase',
25        'CentralAuthReadOnly',
26    ];
27
28    /** @var ServiceOptions */
29    private $options;
30
31    /** @var LBFactory */
32    private $lbFactory;
33
34    /** @var ReadOnlyMode */
35    private $readOnlyMode;
36
37    /**
38     * @param ServiceOptions $options
39     * @param LBFactory $lbFactory
40     * @param ReadOnlyMode $readOnlyMode
41     */
42    public function __construct( ServiceOptions $options, LBFactory $lbFactory, ReadOnlyMode $readOnlyMode ) {
43        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
44        $this->options = $options;
45        $this->lbFactory = $lbFactory;
46        $this->readOnlyMode = $readOnlyMode;
47    }
48
49    /**
50     * Returns a database load balancer that can be used to access the shared CentralAuth database.
51     * @return ILoadBalancer
52     */
53    public function getLoadBalancer(): ILoadBalancer {
54        $database = $this->options->get( 'CentralAuthDatabase' );
55        return $this->lbFactory->getMainLB( $database );
56    }
57
58    /**
59     * Throw an exception if the database is read-only.
60     *
61     * @throws CentralAuthReadOnlyError
62     */
63    public function assertNotReadOnly() {
64        if ( $this->readOnlyMode->isReadOnly() ) {
65            // ReadOnlyError gets its reason text from the global ReadOnlyMode
66            throw new ReadOnlyError;
67        }
68        $reason = $this->getCentralReadOnlyReason();
69        if ( $reason ) {
70            throw new CentralAuthReadOnlyError( $reason );
71        }
72    }
73
74    /**
75     * Determine if either the local or the shared CentralAuth database is
76     * read only. This should determine whether assertNotReadOnly() would
77     * throw.
78     *
79     * @return bool
80     */
81    public function isReadOnly(): bool {
82        return $this->readOnlyMode->isReadOnly()
83            || ( $this->getCentralReadOnlyReason() !== false );
84    }
85
86    /**
87     * Return the reason why either the shared CentralAuth database is read
88     * only, false otherwise.
89     *
90     * @return bool|string
91     */
92    private function getCentralReadOnlyReason() {
93        $configReason = $this->options->get( 'CentralAuthReadOnly' );
94        if ( $configReason === true ) {
95            return '(no reason given)';
96        } elseif ( $configReason ) {
97            return $configReason;
98        }
99
100        $database = $this->options->get( 'CentralAuthDatabase' );
101        $lb = $this->getLoadBalancer();
102
103        return $lb->getReadOnlyReason( $database );
104    }
105
106    /**
107     * Wait for the CentralAuth DB replicas to catch up
108     */
109    public function waitForReplication(): void {
110        $this->lbFactory->waitForReplication( [ 'domain' => $this->options->get( 'CentralAuthDatabase' ) ] );
111    }
112
113    /**
114     * @return IDatabase a connection to the CentralAuth database primary.
115     */
116    public function getCentralPrimaryDB(): IDatabase {
117        $this->assertNotReadOnly();
118        return $this->lbFactory->getPrimaryDatabase(
119            $this->options->get( 'CentralAuthDatabase' )
120        );
121    }
122
123    /**
124     * @return IReadableDatabase a connection to a CentralAuth database replica
125     */
126    public function getCentralReplicaDB(): IReadableDatabase {
127        return $this->lbFactory->getReplicaDatabase(
128            $this->options->get( 'CentralAuthDatabase' )
129        );
130    }
131
132    /**
133     * Gets a database connection to the CentralAuth database.
134     *
135     * @param int $index DB_PRIMARY or DB_REPLICA
136     * @deprecated use {@link ::getCentralPrimaryDB}
137     *                or {@link ::getCentralReplicaDB} instead
138     *
139     * @return IDatabase
140     * @throws CentralAuthReadOnlyError
141     * @throws InvalidArgumentException
142     */
143    public function getCentralDB( int $index ): IDatabase {
144        if ( $index === DB_PRIMARY ) {
145            return $this->getCentralPrimaryDB();
146        }
147
148        if ( $index === DB_REPLICA ) {
149            return $this->getLoadBalancer()
150                ->getConnection(
151                    DB_REPLICA,
152                    [],
153                    $this->options->get( 'CentralAuthDatabase' )
154                );
155        }
156
157        throw new InvalidArgumentException( "Unknown index $index, expected DB_PRIMARY or DB_REPLICA" );
158    }
159
160    /**
161     * Gets a database connection to the local database based on a wikiId
162     *
163     * @param int $index DB_PRIMARY or DB_REPLICA
164     * @param string $wikiId
165     *
166     * @todo Split to two for IReadableDatabase support or drop entirely
167     *
168     * @return IDatabase
169     * @throws CentralAuthReadOnlyError
170     * @throws InvalidArgumentException
171     */
172    public function getLocalDB( int $index, string $wikiId ): IDatabase {
173        if ( $index !== DB_PRIMARY && $index !== DB_REPLICA ) {
174            throw new InvalidArgumentException( "Unknown index $index, expected DB_PRIMARY or DB_REPLICA" );
175        }
176
177        if ( WikiMap::isCurrentWikiId( $wikiId ) ) {
178            $wikiId = false;
179        }
180
181        return $this->lbFactory->getMainLB( $wikiId )
182            ->getConnection( $index, [], $wikiId );
183    }
184
185    /**
186     * Check hasOrMadeRecentPrimaryChanges() on the CentralAuth load balancer
187     *
188     * @return bool
189     */
190    public function centralLBHasRecentPrimaryChanges() {
191        return $this->getLoadBalancer()->hasOrMadeRecentPrimaryChanges();
192    }
193}