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.
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.
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
19 * @author Niklas Laxström
22 namespace MediaWiki\Cache
;
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
;
33 * Look up "gender" user preference.
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.
44 protected $cache = [];
45 /** @var string|null */
46 protected $default = null;
48 protected $misses = 0;
50 * @internal Exposed for MediaWiki core unit tests.
53 protected $missLimit = 1000;
55 private NamespaceInfo
$nsInfo;
56 private ?IConnectionProvider
$dbProvider;
57 private UserOptionsLookup
$userOptionsLookup;
59 public function __construct(
60 ?NamespaceInfo
$nsInfo = null,
61 ?IConnectionProvider
$dbProvider = null,
62 ?UserOptionsLookup
$userOptionsLookup = null
64 $this->nsInfo
= $nsInfo ?? MediaWikiServices
::getInstance()->getNamespaceInfo();
65 $this->dbProvider
= $dbProvider;
66 $this->userOptionsLookup
= $userOptionsLookup ?? MediaWikiServices
::getInstance()->getUserOptionsLookup();
70 * Get the default gender option on this wiki.
74 protected function getDefault() {
75 $this->default ??
= $this->userOptionsLookup
->getDefaultOption( 'gender' );
76 return $this->default;
80 * Get the gender option for given username.
82 * @param string|UserIdentity $username
83 * @param string|null $caller Calling method for database profiling
86 public function getGenderOf( $username, $caller = '' ) {
87 if ( $username instanceof UserIdentity
) {
88 $username = $username->getName();
91 $username = self
::normalizeUsername( $username );
92 if ( !isset( $this->cache
[$username] ) ) {
93 if ( $this->misses
< $this->missLimit ||
94 RequestContext
::getMain()->getUser()->getName() === $username
97 $this->doQuery( $username, $caller );
99 if ( $this->misses
=== $this->missLimit
) {
100 // Log only once and don't bother incrementing beyond limit+1
102 wfDebug( __METHOD__
. ': too many misses, returning default onwards' );
106 return $this->cache
[$username] ??
$this->getDefault();
110 * Wrapper for doQuery that processes raw LinkBatch data.
112 * @param array<int,array<string,mixed>> $data
113 * @param string|null $caller
115 public function doLinkBatch( array $data, $caller = '' ) {
117 foreach ( $data as $ns => $pagenames ) {
118 if ( $this->nsInfo
->hasGenderDistinction( $ns ) ) {
119 $users +
= $pagenames;
122 $this->doQuery( array_keys( $users ), $caller );
126 * Wrapper for doQuery that processes a title array.
129 * @param LinkTarget[] $titles
130 * @param string|null $caller Calling method for database profiling
132 public function doTitlesArray( $titles, $caller = '' ) {
134 foreach ( $titles as $titleObj ) {
135 if ( $this->nsInfo
->hasGenderDistinction( $titleObj->getNamespace() ) ) {
136 $users[] = $titleObj->getText();
139 $this->doQuery( $users, $caller );
143 * Preload gender option for multiple user names.
145 * @param string[]|string $users Usernames
146 * @param string|null $caller Calling method for database profiling
148 public function doQuery( $users, $caller = '' ) {
149 $default = $this->getDefault();
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;
162 // Skip query when database is unavailable (e.g. via the installer)
163 if ( !$usersToFetch ||
!$this->dbProvider
) {
167 $caller = __METHOD__
. ( $caller ?
"/$caller" : '' );
169 $res = $queryBuilder = $this->dbProvider
->getReplicaDatabase()->newSelectQueryBuilder()
170 ->select( [ 'user_name', 'up_value' ] )
172 ->leftJoin( 'user_properties', null, [ 'user_id = up_user', 'up_property' => 'gender' ] )
173 ->where( [ 'user_name' => $usersToFetch ] )
177 foreach ( $res as $row ) {
178 $this->cache
[$row->user_name
] = $row->up_value ?
: $default;
182 private static function normalizeUsername( $username ) {
183 // Strip off subpages
184 $indexSlash = strpos( $username, '/' );
185 if ( $indexSlash !== false ) {
186 $username = substr( $username, 0, $indexSlash );
189 // normalize underscore/spaces
190 return strtr( $username, '_', ' ' );
194 /** @deprecated class alias since 1.42 */
195 class_alias( GenderCache
::class, 'GenderCache' );