Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.54% covered (warning)
82.54%
52 / 63
72.22% covered (warning)
72.22%
13 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
UserSelectQueryBuilder
82.54% covered (warning)
82.54%
52 / 63
72.22% covered (warning)
72.22%
13 / 18
29.60
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
 whereUserIds
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 userIds
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 whereUserNames
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 userNames
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 whereUserNamePrefix
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 userNamePrefix
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 whereRegisteredTimestamp
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 orderByName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 orderByUserId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 registered
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 anon
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 named
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 temp
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 hidden
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 fetchUserIdentity
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 fetchUserIdentities
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 fetchUserNames
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\User;
8
9use Iterator;
10use MediaWiki\Block\HideUserUtils;
11use MediaWiki\User\TempUser\TempUserConfig;
12use Wikimedia\Assert\Assert;
13use Wikimedia\Assert\PreconditionException;
14use Wikimedia\Rdbms\IExpression;
15use Wikimedia\Rdbms\IReadableDatabase;
16use Wikimedia\Rdbms\LikeValue;
17use Wikimedia\Rdbms\SelectQueryBuilder;
18
19/**
20 * @ingroup User
21 */
22class UserSelectQueryBuilder extends SelectQueryBuilder {
23
24    private bool $userJoined = false;
25
26    /**
27     * @internal
28     */
29    public function __construct(
30        IReadableDatabase $db,
31        private readonly ActorStore $actorStore,
32        private readonly TempUserConfig $tempUserConfig,
33        private readonly HideUserUtils $hideUserUtils,
34    ) {
35        parent::__construct( $db );
36
37        $this->table( 'actor' );
38    }
39
40    /**
41     * Find by provided user ids.
42     *
43     * @param int|int[] $userIds
44     * @return UserSelectQueryBuilder
45     */
46    public function whereUserIds( $userIds ): self {
47        Assert::parameterType( [ 'integer', 'array' ], $userIds, '$userIds' );
48        $this->conds( [ 'actor_user' => $userIds ] );
49        return $this;
50    }
51
52    /**
53     * Find by provided user ids.
54     * @deprecated since 1.37, use whereUserIds instead
55     * @param int|int[] $userIds
56     * @return UserSelectQueryBuilder
57     */
58    public function userIds( $userIds ): self {
59        return $this->whereUserIds( $userIds );
60    }
61
62    /**
63     * Find by provided usernames.
64     *
65     * @param string|string[] $userNames
66     * @return UserSelectQueryBuilder
67     */
68    public function whereUserNames( $userNames ): self {
69        Assert::parameterType( [ 'string', 'array' ], $userNames, '$userIds' );
70        $userNames = array_map( function ( $name ) {
71            return $this->actorStore->normalizeUserName( (string)$name );
72        }, (array)$userNames );
73        $this->conds( [ 'actor_name' => $userNames ] );
74        return $this;
75    }
76
77    /**
78     * Find by provided usernames.
79     * @deprecated since 1.37, use whereUserNames instead
80     * @param string|string[] $userNames
81     * @return UserSelectQueryBuilder
82     */
83    public function userNames( $userNames ): self {
84        return $this->whereUserNames( $userNames );
85    }
86
87    /**
88     * Find users with names starting from the provided prefix.
89     *
90     * @note this could produce a huge number of results, like User00000 ... User99999,
91     * so you must set a limit when using this condition.
92     *
93     * @param string $prefix
94     * @return UserSelectQueryBuilder
95     */
96    public function whereUserNamePrefix( string $prefix ): self {
97        if ( !isset( $this->options['LIMIT'] ) ) {
98            throw new PreconditionException( 'Must set a limit when using a user name prefix' );
99        }
100        $this->conds(
101            $this->db->expr( 'actor_name', IExpression::LIKE, new LikeValue( $prefix, $this->db->anyString() ) )
102        );
103        return $this;
104    }
105
106    /**
107     * Find users with names starting from the provided prefix.
108     *
109     * @note this could produce a huge number of results, like User00000 ... User99999,
110     * so you must set a limit when using this condition.
111     * @deprecated since 1.37 use whereUserNamePrefix instead
112     * @param string $prefix
113     * @return UserSelectQueryBuilder
114     */
115    public function userNamePrefix( string $prefix ): self {
116        return $this->whereUserNamePrefix( $prefix );
117    }
118
119    /**
120     * Find registered users who registered
121     *
122     * @param string $timestamp
123     * @param bool $direction Direction flag (if true, user_registration must be before $timestamp)
124     * @since 1.42
125     * @return UserSelectQueryBuilder
126     */
127    public function whereRegisteredTimestamp( string $timestamp, bool $direction ): self {
128        if ( !$this->userJoined ) {
129            $this->join( 'user', null, [ "actor_user=user_id" ] );
130            $this->userJoined = true;
131        }
132
133        $this->conds(
134            $this->db->expr( 'user_registration', ( $direction ? '<' : '>' ), $this->db->timestamp( $timestamp ) )
135        );
136        return $this;
137    }
138
139    /**
140     * Order results by name in $direction
141     *
142     * @param string $dir one of self::SORT_ASC or self::SORT_DESC
143     * @return UserSelectQueryBuilder
144     */
145    public function orderByName( string $dir = self::SORT_ASC ): self {
146        $this->orderBy( 'actor_name', $dir );
147        return $this;
148    }
149
150    /**
151     * Order results by user id.
152     *
153     * @param string $dir one of self::SORT_ASC or self::SORT_DESC
154     * @return UserSelectQueryBuilder
155     */
156    public function orderByUserId( string $dir = self::SORT_ASC ): self {
157        $this->orderBy( 'actor_user', $dir );
158        return $this;
159    }
160
161    /**
162     * Only return registered users.
163     */
164    public function registered(): self {
165        $this->conds( $this->db->expr( 'actor_user', '!=', null ) );
166        return $this;
167    }
168
169    /**
170     * Only return anonymous users.
171     */
172    public function anon(): self {
173        $this->conds( [ 'actor_user' => null ] );
174        return $this;
175    }
176
177    /**
178     * Only return named users.
179     */
180    public function named(): self {
181        // All named accounts must be registered
182        $this->registered();
183
184        if ( !$this->tempUserConfig->isKnown() ) {
185            // nothing to do: getMatchCondition throws if temp accounts aren't known
186            return $this;
187        }
188        $this->conds( $this->tempUserConfig->getMatchCondition( $this->db, 'actor_name', IExpression::NOT_LIKE ) );
189        return $this;
190    }
191
192    /**
193     * Only return temp users
194     */
195    public function temp(): self {
196        if ( !$this->tempUserConfig->isKnown() ) {
197            $this->conds( '1=0' );
198            return $this;
199        }
200        $this->conds( $this->tempUserConfig->getMatchCondition( $this->db, 'actor_name', IExpression::LIKE ) );
201        return $this;
202    }
203
204    /**
205     * Filter based on user hidden status
206     *
207     * @since 1.38
208     * @param bool $hidden True - only hidden users, false - no hidden users
209     * @return $this
210     */
211    public function hidden( bool $hidden ): self {
212        $this->conds( $this->hideUserUtils->getExpression(
213            $this->db,
214            'actor_user',
215            $hidden ? HideUserUtils::HIDDEN_USERS : HideUserUtils::SHOWN_USERS
216        ) );
217        return $this;
218    }
219
220    /**
221     * Fetch a single UserIdentity that matches specified criteria.
222     */
223    public function fetchUserIdentity(): ?UserIdentity {
224        $this->fields( [ 'actor_id', 'actor_name', 'actor_user' ] );
225        $row = $this->fetchRow();
226        if ( !$row ) {
227            return null;
228        }
229        return $this->actorStore->newActorFromRow( $row );
230    }
231
232    /**
233     * Fetch UserIdentities for the specified query.
234     *
235     * @return Iterator<UserIdentity>
236     */
237    public function fetchUserIdentities(): Iterator {
238        $this->fields( [ 'actor_id', 'actor_name', 'actor_user' ] );
239
240        $result = $this->fetchResultSet();
241        foreach ( $result as $row ) {
242            yield $this->actorStore->newActorFromRow( $row );
243        }
244        $result->free();
245    }
246
247    /**
248     * Returns an array of usernames matching the query.
249     *
250     * @return string[]
251     */
252    public function fetchUserNames(): array {
253        $this->field( 'actor_name' );
254        return $this->fetchFieldValues();
255    }
256}