Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 179
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryGlobalAllUsers
0.00% covered (danger)
0.00%
0 / 179
0.00% covered (danger)
0.00%
0 / 7
992
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getDB
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getCanonicalUserName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 1
552
 getGlobalGroups
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
6
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\CentralAuth\Api;
4
5use MediaWiki\Api\ApiBase;
6use MediaWiki\Api\ApiQuery;
7use MediaWiki\Api\ApiQueryBase;
8use MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager;
9use MediaWiki\Extension\CentralAuth\GlobalGroup\GlobalGroupLookup;
10use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
11use MediaWiki\User\TempUser\TempUserConfig;
12use MediaWiki\WikiMap\WikiMap;
13use Wikimedia\ParamValidator\ParamValidator;
14use Wikimedia\ParamValidator\TypeDef\IntegerDef;
15use Wikimedia\Rdbms\IExpression;
16use Wikimedia\Rdbms\IReadableDatabase;
17use Wikimedia\Rdbms\IResultWrapper;
18use Wikimedia\Rdbms\LikeValue;
19
20/**
21 * Api module for CentralAuth extension to list all global users.
22 * Partly based on ApiQueryAllUsers.
23 *
24 * This program is free software; you can redistribute it and/or modify
25 * it under the terms of the GNU General Public License as published by
26 * the Free Software Foundation; either version 2 of the License, or
27 * (at your option) any later version.
28 *
29 * This program is distributed in the hope that it will be useful,
30 * but WITHOUT ANY WARRANTY; without even the implied warranty of
31 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32 * GNU General Public License for more details.
33 *
34 * You should have received a copy of the GNU General Public License along
35 * with this program; if not, write to the Free Software Foundation, Inc.,
36 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
37 * http://www.gnu.org/copyleft/gpl.html
38 *
39 * @ingroup API
40 * @ingroup Extensions
41 *
42 * @license GPL-2.0-or-later
43 * @author Marius Hoch < hoo@online.de >
44 */
45class ApiQueryGlobalAllUsers extends ApiQueryBase {
46
47    private TempUserConfig $tempUserConfig;
48    private CentralAuthDatabaseManager $databaseManager;
49    private GlobalGroupLookup $globalGroupLookup;
50
51    public function __construct(
52        ApiQuery $query,
53        string $moduleName,
54        TempUserConfig $tempUserConfig,
55        CentralAuthDatabaseManager $databaseManager,
56        GlobalGroupLookup $globalGroupLookup
57    ) {
58        parent::__construct( $query, $moduleName, 'agu' );
59        $this->tempUserConfig = $tempUserConfig;
60        $this->databaseManager = $databaseManager;
61        $this->globalGroupLookup = $globalGroupLookup;
62    }
63
64    /**
65     * Get the Query database connection (read-only)
66     *
67     * @see ApiQueryBase::getDB
68     * @return IReadableDatabase
69     */
70    protected function getDB() {
71        static $db = null;
72
73        if ( $db === null ) {
74            $db = $this->databaseManager->getCentralReplicaDB();
75        }
76        return $db;
77    }
78
79    /**
80     * This function converts the user name to a canonical form
81     * which is stored in the database.
82     * @param string $name
83     * @return string
84     */
85    private function getCanonicalUserName( $name ) {
86        return str_replace( '_', ' ', $name );
87    }
88
89    public function execute() {
90        $params = $this->extractRequestParams();
91
92        $prop = array_flip( (array)$params['prop'] );
93
94        $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
95
96        $db = $this->getDB();
97        $this->addTables( 'globaluser' );
98        $this->addFields( [ 'gu_id', 'gu_name' ] );
99        $limit = intval( $params['limit'] ) + 1;
100
101        $this->addWhereRange(
102            'gu_name',
103            $dir,
104            isset( $params['from'] ) ? $this->getCanonicalUserName( $params['from'] ) : null,
105            isset( $params['to'] ) ? $this->getCanonicalUserName( $params['to'] ) : null
106        );
107
108        if ( $params['prefix'] !== null ) {
109            $this->addWhere(
110                $db->expr( 'gu_name', IExpression::LIKE,
111                    new LikeValue( $this->getCanonicalUserName( $params['prefix'] ), $db->anyString() ) )
112            );
113        }
114
115        $this->requireMaxOneParameter( $params, 'group', 'excludegroup' );
116
117        if ( !empty( $params['group'] ) ) {
118            $this->addTables( 'global_user_groups' );
119            // Request more rows than needed to avoid not getting all rows
120            // that belong to one user, because a user might be in multiple groups
121            $limit += count( $params['group'] ) + 1;
122
123            $this->addJoinConds( [
124                'global_user_groups' =>
125                [ 'INNER JOIN', 'gug_user = gu_id' ]
126            ] );
127
128            $this->addWhere( [ 'gug_group' => $params['group'] ] );
129            $this->addWhere( [
130                $this->getDB()->expr( 'gug_expiry', '=', null )
131                    ->or( 'gug_expiry', '>=', $this->getDB()->timestamp() )
132            ] );
133        }
134
135        if ( !empty( $params['excludegroup'] ) ) {
136            $this->addTables( 'global_user_groups', 'gug2' );
137
138            $this->addJoinConds( [
139                'gug2' =>
140                [ 'LEFT OUTER JOIN', [ 'gug2.gug_user = gu_id', 'gug2.gug_group' => $params['excludegroup'] ] ]
141            ] );
142
143            $this->addWhere( [ 'gug2.gug_user' => null ] );
144            $this->addWhere( [
145                $this->getDB()->expr( 'gug2.gug_expiry', '=', null )
146                    ->or( 'gug2.gug_expiry', '>=', $this->getDB()->timestamp() )
147            ] );
148        }
149
150        $this->addWhere( [ 'gu_hidden_level' => CentralAuthUser::HIDDEN_LEVEL_NONE ] );
151
152        if ( isset( $prop['lockinfo'] ) ) {
153            $this->addFields( 'gu_locked' );
154        }
155
156        if ( isset( $prop['existslocally'] ) ) {
157            $this->addTables( 'localuser' );
158            $this->addFields( 'lu_wiki' );
159            $this->addJoinConds( [
160                'localuser' =>
161                [ 'LEFT OUTER JOIN', [ 'gu_name=lu_name', 'lu_wiki' => WikiMap::getCurrentWikiId() ] ]
162            ] );
163        }
164
165        $excludeNamed = $params['excludenamed'];
166        $excludeTemp = $params['excludetemp'];
167
168        if ( $this->tempUserConfig->isKnown() ) {
169            if ( $excludeTemp ) {
170                $this->addWhere(
171                    $this->tempUserConfig->getMatchCondition( $db, 'gu_name', IExpression::NOT_LIKE )
172                );
173            }
174            if ( $excludeNamed ) {
175                $this->addWhere(
176                    $this->tempUserConfig->getMatchCondition( $db, 'gu_name', IExpression::LIKE )
177                );
178            }
179        }
180
181        $this->addOption( 'LIMIT', $limit );
182
183        $result = $this->select( __METHOD__ );
184
185        $groupsOfUser = [];
186        if ( isset( $prop['groups'] ) && $result->numRows() ) {
187            $groupsOfUser = $this->getGlobalGroups( $result, $dir );
188        }
189
190        $data = [];
191        $previousName = '';
192        $i = 1;
193        foreach ( $result as $row ) {
194            if ( $row->gu_name === $previousName ) {
195                continue;
196            }
197            $previousName = $row->gu_name;
198            if ( $i > $params['limit'] ) {
199                $this->setContinueEnumParameter( 'from', $row->gu_name );
200                break;
201            }
202            $i++;
203
204            $entry = [];
205            $entry['id'] = (int)$row->gu_id;
206            $entry['name'] = $row->gu_name;
207
208            if ( isset( $prop['groups'] ) ) {
209                if ( !empty( $groupsOfUser[$row->gu_id] ) ) {
210                    $entry['groups'] = $groupsOfUser[$row->gu_id];
211                } else {
212                    $entry['groups'] = [];
213                }
214                $this->getResult()->setIndexedTagName( $entry['groups'], 'group' );
215            }
216
217            if ( isset( $prop['existslocally'] ) && $row->lu_wiki != null ) {
218                $entry['existslocally'] = '';
219            }
220
221            if ( isset( $prop['lockinfo'] ) && $row->gu_locked ) {
222                $entry['locked'] = '';
223            }
224
225            $data[] = $entry;
226        }
227
228        $this->getResult()->setIndexedTagName( $data, 'globaluser' );
229
230        $this->getResult()->addValue( 'query', $this->getModuleName(), $data );
231    }
232
233    /**
234     * Get the global groups for the given global user result set.
235     *
236     * @param IResultWrapper $result Result of a globaluser table select
237     * @param string $dir Sorting directory
238     *
239     * @return string[][]
240     */
241    protected function getGlobalGroups( IResultWrapper $result, $dir ) {
242        $this->resetQueryParams();
243
244        // Get all global groups now. We do this by using a WHERE
245        // range build from the given results
246        $groupsOfUser = [];
247
248        $this->addTables( [ 'globaluser', 'global_user_groups' ] );
249        $this->addFields( [ 'gug_user', 'gug_group' ] );
250
251        $result->seek( 0 );
252        $firstUser = $result->fetchObject()->gu_name;
253        $result->seek( $result->numRows() - 1 );
254        $lastUser = $result->fetchObject()->gu_name;
255
256        $this->addWhereRange(
257            'gu_name',
258            $dir,
259            $firstUser,
260            $lastUser,
261            false
262        );
263
264        // Use an INNER JOIN to only get users with global groups
265        $this->addJoinConds( [
266            'global_user_groups' =>
267            [ 'INNER JOIN', 'gug_user = gu_id' ]
268        ] );
269
270        $this->addWhere( [
271            $this->getDB()->expr( 'gug_expiry', '=', null )
272                ->or( 'gug_expiry', '>=', $this->getDB()->timestamp() )
273        ] );
274
275        $groupResult = $this->select( __METHOD__ );
276
277        foreach ( $groupResult as $groupRow ) {
278            $groupsOfUser[$groupRow->gug_user][] = $groupRow->gug_group;
279        }
280
281        return $groupsOfUser;
282    }
283
284    /** @inheritDoc */
285    public function getAllowedParams() {
286        $globalGroups = $this->globalGroupLookup->getDefinedGroups();
287        return [
288            'from' => null,
289            'to' => null,
290            'prefix' => null,
291            'dir' => [
292                ParamValidator::PARAM_DEFAULT => 'ascending',
293                ParamValidator::PARAM_TYPE => [
294                    'ascending',
295                    'descending'
296                ],
297            ],
298            'group' => [
299                ParamValidator::PARAM_TYPE => $globalGroups,
300                ParamValidator::PARAM_ISMULTI => true,
301            ],
302            'excludegroup' => [
303                ParamValidator::PARAM_TYPE => $globalGroups,
304                ParamValidator::PARAM_ISMULTI => true,
305            ],
306            'prop' => [
307                ParamValidator::PARAM_ISMULTI => true,
308                ParamValidator::PARAM_TYPE => [
309                    'lockinfo',
310                    'groups',
311                    'existslocally'
312                ],
313                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
314            ],
315            'limit' => [
316                ParamValidator::PARAM_DEFAULT => 10,
317                ParamValidator::PARAM_TYPE => 'limit',
318                IntegerDef::PARAM_MIN => 1,
319                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
320                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
321            ],
322            'excludenamed' => [
323                ParamValidator::PARAM_TYPE => 'boolean',
324            ],
325            'excludetemp' => [
326                ParamValidator::PARAM_TYPE => 'boolean',
327            ],
328        ];
329    }
330
331    /** @inheritDoc */
332    protected function getExamplesMessages() {
333        return [
334            'action=query&list=globalallusers'
335                => 'apihelp-query+globalallusers-example-1',
336            'action=query&list=globalallusers&agufrom=ABC&aguprop=lockinfo|groups|existslocally'
337                => 'apihelp-query+globalallusers-example-2',
338        ];
339    }
340}