1<?php 2/** 3 * Caches current user names and other info based on user IDs. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup Cache 22 */ 23 24use MediaWiki\Cache\LinkBatchFactory; 25use MediaWiki\MediaWikiServices; 26use Psr\Log\LoggerInterface; 27use Wikimedia\Rdbms\ILoadBalancer; 28 29/** 30 * @since 1.20 31 */ 32class UserCache { 33 protected $cache = []; // (uid => property => value) 34 protected $typesCached = []; // (uid => cache type => 1) 35 36 /** @var LoggerInterface */ 37 private $logger; 38 39 /** @var LinkBatchFactory */ 40 private $linkBatchFactory; 41 42 /** @var ILoadBalancer */ 43 private $loadBalancer; 44 45 /** 46 * @return UserCache 47 */ 48 public static function singleton() { 49 return MediaWikiServices::getInstance()->getUserCache(); 50 } 51 52 /** 53 * Uses dependency injection since 1.36 54 * 55 * @param LoggerInterface $logger 56 * @param ILoadBalancer $loadBalancer 57 * @param LinkBatchFactory $linkBatchFactory 58 */ 59 public function __construct( 60 LoggerInterface $logger, 61 ILoadBalancer $loadBalancer, 62 LinkBatchFactory $linkBatchFactory 63 ) { 64 $this->logger = $logger; 65 $this->loadBalancer = $loadBalancer; 66 $this->linkBatchFactory = $linkBatchFactory; 67 } 68 69 /** 70 * Get a property of a user based on their user ID 71 * 72 * @param int $userId User ID 73 * @param string $prop User property 74 * @return mixed|bool The property or false if the user does not exist 75 */ 76 public function getProp( $userId, $prop ) { 77 if ( !isset( $this->cache[$userId][$prop] ) ) { 78 $this->logger->debug( 79 'Querying DB for prop {prop} for user ID {userId}', 80 [ 81 'prop' => $prop, 82 'userId' => $userId, 83 ] 84 ); 85 $this->doQuery( [ $userId ] ); // cache miss 86 } 87 88 return $this->cache[$userId][$prop] ?? false; // user does not exist? 89 } 90 91 /** 92 * Get the name of a user or return $ip if the user ID is 0 93 * 94 * @param int $userId 95 * @param string $ip 96 * @return string 97 * @since 1.22 98 */ 99 public function getUserName( $userId, $ip ) { 100 return $userId > 0 ? $this->getProp( $userId, 'name' ) : $ip; 101 } 102 103 /** 104 * Preloads user names for given list of users. 105 * @param array $userIds List of user IDs 106 * @param array $options Option flags; include 'userpage' and 'usertalk' 107 * @param string $caller The calling method 108 */ 109 public function doQuery( array $userIds, $options = [], $caller = '' ) { 110 $usersToCheck = []; 111 $usersToQuery = []; 112 113 $userIds = array_unique( $userIds ); 114 115 foreach ( $userIds as $userId ) { 116 $userId = (int)$userId; 117 if ( $userId <= 0 ) { 118 continue; // skip anons 119 } 120 if ( isset( $this->cache[$userId]['name'] ) ) { 121 $usersToCheck[$userId] = $this->cache[$userId]['name']; // already have name 122 } else { 123 $usersToQuery[] = $userId; // we need to get the name 124 } 125 } 126 127 // Lookup basic info for users not yet loaded... 128 if ( count( $usersToQuery ) ) { 129 $dbr = $this->loadBalancer->getConnection( DB_REPLICA ); 130 $tables = [ 'user', 'actor' ]; 131 $conds = [ 'user_id' => $usersToQuery ]; 132 $fields = [ 'user_name', 'user_real_name', 'user_registration', 'user_id', 'actor_id' ]; 133 $joinConds = [ 134 'actor' => [ 'JOIN', 'actor_user = user_id' ], 135 ]; 136 137 $comment = __METHOD__; 138 if ( strval( $caller ) !== '' ) { 139 $comment .= "/$caller"; 140 } 141 142 $res = $dbr->select( $tables, $fields, $conds, $comment, [], $joinConds ); 143 foreach ( $res as $row ) { // load each user into cache 144 $userId = (int)$row->user_id; 145 $this->cache[$userId]['name'] = $row->user_name; 146 $this->cache[$userId]['real_name'] = $row->user_real_name; 147 $this->cache[$userId]['registration'] = $row->user_registration; 148 $this->cache[$userId]['actor'] = $row->actor_id; 149 $usersToCheck[$userId] = $row->user_name; 150 } 151 } 152 153 $lb = $this->linkBatchFactory->newLinkBatch(); 154 foreach ( $usersToCheck as $userId => $name ) { 155 if ( $this->queryNeeded( $userId, 'userpage', $options ) ) { 156 $lb->add( NS_USER, $name ); 157 $this->typesCached[$userId]['userpage'] = 1; 158 } 159 if ( $this->queryNeeded( $userId, 'usertalk', $options ) ) { 160 $lb->add( NS_USER_TALK, $name ); 161 $this->typesCached[$userId]['usertalk'] = 1; 162 } 163 } 164 $lb->execute(); 165 } 166 167 /** 168 * Check if a cache type is in $options and was not loaded for this user 169 * 170 * @param int $uid User ID 171 * @param string $type Cache type 172 * @param array $options Requested cache types 173 * @return bool 174 */ 175 protected function queryNeeded( $uid, $type, array $options ) { 176 return ( in_array( $type, $options ) && !isset( $this->typesCached[$uid][$type] ) ); 177 } 178} 179