Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
17.65% |
9 / 51 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
GenderCache | |
18.00% |
9 / 50 |
|
0.00% |
0 / 7 |
341.59 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getDefault | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getGenderOf | |
75.00% |
9 / 12 |
|
0.00% |
0 / 1 |
6.56 | |||
doLinkBatch | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
doTitlesArray | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
doQuery | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
72 | |||
normalizeUsername | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
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 | * @author Niklas Laxström |
20 | */ |
21 | |
22 | namespace MediaWiki\Cache; |
23 | |
24 | use MediaWiki\Context\RequestContext; |
25 | use MediaWiki\Linker\LinkTarget; |
26 | use MediaWiki\MediaWikiServices; |
27 | use MediaWiki\Title\NamespaceInfo; |
28 | use MediaWiki\User\Options\UserOptionsLookup; |
29 | use MediaWiki\User\UserIdentity; |
30 | use Wikimedia\Rdbms\IConnectionProvider; |
31 | |
32 | /** |
33 | * Look up "gender" user preference. |
34 | * |
35 | * This primarily used in MediaWiki\Title\MediaWikiTitleCodec for title formatting |
36 | * of pages in gendered namespace aliases, and in CoreParserFunctions for the |
37 | * `{{gender:}}` parser function. |
38 | * |
39 | * @since 1.18 |
40 | * @ingroup Cache |
41 | */ |
42 | class GenderCache { |
43 | /** @var string[] */ |
44 | protected $cache = []; |
45 | /** @var string|null */ |
46 | protected $default = null; |
47 | /** @var int */ |
48 | protected $misses = 0; |
49 | /** |
50 | * @internal Exposed for MediaWiki core unit tests. |
51 | * @var int |
52 | */ |
53 | protected $missLimit = 1000; |
54 | |
55 | private NamespaceInfo $nsInfo; |
56 | private ?IConnectionProvider $dbProvider; |
57 | private UserOptionsLookup $userOptionsLookup; |
58 | |
59 | public function __construct( |
60 | ?NamespaceInfo $nsInfo = null, |
61 | ?IConnectionProvider $dbProvider = null, |
62 | ?UserOptionsLookup $userOptionsLookup = null |
63 | ) { |
64 | $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo(); |
65 | $this->dbProvider = $dbProvider; |
66 | $this->userOptionsLookup = $userOptionsLookup ?? MediaWikiServices::getInstance()->getUserOptionsLookup(); |
67 | } |
68 | |
69 | /** |
70 | * Get the default gender option on this wiki. |
71 | * |
72 | * @return string |
73 | */ |
74 | protected function getDefault() { |
75 | $this->default ??= $this->userOptionsLookup->getDefaultOption( 'gender' ); |
76 | return $this->default; |
77 | } |
78 | |
79 | /** |
80 | * Get the gender option for given username. |
81 | * |
82 | * @param string|UserIdentity $username |
83 | * @param string|null $caller Calling method for database profiling |
84 | * @return string |
85 | */ |
86 | public function getGenderOf( $username, $caller = '' ) { |
87 | if ( $username instanceof UserIdentity ) { |
88 | $username = $username->getName(); |
89 | } |
90 | |
91 | $username = self::normalizeUsername( $username ); |
92 | if ( !isset( $this->cache[$username] ) ) { |
93 | if ( $this->misses < $this->missLimit || |
94 | RequestContext::getMain()->getUser()->getName() === $username |
95 | ) { |
96 | $this->misses++; |
97 | $this->doQuery( $username, $caller ); |
98 | } |
99 | if ( $this->misses === $this->missLimit ) { |
100 | // Log only once and don't bother incrementing beyond limit+1 |
101 | $this->misses++; |
102 | wfDebug( __METHOD__ . ': too many misses, returning default onwards' ); |
103 | } |
104 | } |
105 | |
106 | return $this->cache[$username] ?? $this->getDefault(); |
107 | } |
108 | |
109 | /** |
110 | * Wrapper for doQuery that processes raw LinkBatch data. |
111 | * |
112 | * @param array<int,array<string,mixed>> $data |
113 | * @param string|null $caller |
114 | */ |
115 | public function doLinkBatch( array $data, $caller = '' ) { |
116 | $users = []; |
117 | foreach ( $data as $ns => $pagenames ) { |
118 | if ( $this->nsInfo->hasGenderDistinction( $ns ) ) { |
119 | $users += $pagenames; |
120 | } |
121 | } |
122 | $this->doQuery( array_keys( $users ), $caller ); |
123 | } |
124 | |
125 | /** |
126 | * Wrapper for doQuery that processes a title array. |
127 | * |
128 | * @since 1.20 |
129 | * @param LinkTarget[] $titles |
130 | * @param string|null $caller Calling method for database profiling |
131 | */ |
132 | public function doTitlesArray( $titles, $caller = '' ) { |
133 | $users = []; |
134 | foreach ( $titles as $titleObj ) { |
135 | if ( $this->nsInfo->hasGenderDistinction( $titleObj->getNamespace() ) ) { |
136 | $users[] = $titleObj->getText(); |
137 | } |
138 | } |
139 | $this->doQuery( $users, $caller ); |
140 | } |
141 | |
142 | /** |
143 | * Preload gender option for multiple user names. |
144 | * |
145 | * @param string[]|string $users Usernames |
146 | * @param string|null $caller Calling method for database profiling |
147 | */ |
148 | public function doQuery( $users, $caller = '' ) { |
149 | $default = $this->getDefault(); |
150 | |
151 | $usersToFetch = []; |
152 | foreach ( (array)$users as $value ) { |
153 | $name = self::normalizeUsername( $value ); |
154 | if ( !isset( $this->cache[$name] ) ) { |
155 | // This may be overwritten below by a fetched value |
156 | $this->cache[$name] = $default; |
157 | // T267054: We don't need to fetch data for invalid usernames, but filtering breaks DI |
158 | $usersToFetch[] = $name; |
159 | } |
160 | } |
161 | |
162 | // Skip query when database is unavailable (e.g. via the installer) |
163 | if ( !$usersToFetch || !$this->dbProvider ) { |
164 | return; |
165 | } |
166 | |
167 | $caller = __METHOD__ . ( $caller ? "/$caller" : '' ); |
168 | |
169 | $res = $queryBuilder = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder() |
170 | ->select( [ 'user_name', 'up_value' ] ) |
171 | ->from( 'user' ) |
172 | ->leftJoin( 'user_properties', null, [ 'user_id = up_user', 'up_property' => 'gender' ] ) |
173 | ->where( [ 'user_name' => $usersToFetch ] ) |
174 | ->caller( $caller ) |
175 | ->fetchResultSet(); |
176 | |
177 | foreach ( $res as $row ) { |
178 | $this->cache[$row->user_name] = $row->up_value ?: $default; |
179 | } |
180 | } |
181 | |
182 | private static function normalizeUsername( $username ) { |
183 | // Strip off subpages |
184 | $indexSlash = strpos( $username, '/' ); |
185 | if ( $indexSlash !== false ) { |
186 | $username = substr( $username, 0, $indexSlash ); |
187 | } |
188 | |
189 | // normalize underscore/spaces |
190 | return strtr( $username, '_', ' ' ); |
191 | } |
192 | } |
193 | |
194 | /** @deprecated class alias since 1.42 */ |
195 | class_alias( GenderCache::class, 'GenderCache' ); |