x-tools /
xtools
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
| 1 | <?php |
||
| 2 | |||
| 3 | declare(strict_types = 1); |
||
| 4 | |||
| 5 | namespace App\Repository; |
||
| 6 | |||
| 7 | use App\Model\Project; |
||
| 8 | use App\Model\User; |
||
| 9 | |||
| 10 | /** |
||
| 11 | * An UserRightsRepository is responsible for retrieving information around a user's |
||
| 12 | * rights on a given wiki. It doesn't do any post-processing of that information. |
||
| 13 | * @codeCoverageIgnore |
||
| 14 | */ |
||
| 15 | class UserRightsRepository extends Repository |
||
| 16 | { |
||
| 17 | /** |
||
| 18 | * Get user rights changes of the given user, including those made on Meta. |
||
| 19 | * @param Project $project |
||
| 20 | * @param User $user |
||
| 21 | * @return array |
||
| 22 | */ |
||
| 23 | public function getRightsChanges(Project $project, User $user): array |
||
| 24 | { |
||
| 25 | $changes = $this->queryRightsChanges($project, $user); |
||
| 26 | |||
| 27 | if ($this->isWMF) { |
||
| 28 | $changes = array_merge( |
||
| 29 | $changes, |
||
| 30 | $this->queryRightsChanges($project, $user, 'meta') |
||
| 31 | ); |
||
| 32 | } |
||
| 33 | |||
| 34 | return $changes; |
||
| 35 | } |
||
| 36 | |||
| 37 | /** |
||
| 38 | * Get global user rights changes of the given user. |
||
| 39 | * @param Project $project Global rights are always on Meta, so this |
||
| 40 | * Project instance is re-used if it is already Meta, otherwise |
||
| 41 | * a new Project instance is created. |
||
| 42 | * @param User $user |
||
| 43 | * @return array |
||
| 44 | */ |
||
| 45 | public function getGlobalRightsChanges(Project $project, User $user): array |
||
| 46 | { |
||
| 47 | return $this->queryRightsChanges($project, $user, 'global'); |
||
| 48 | } |
||
| 49 | |||
| 50 | /** |
||
| 51 | * User rights changes for given project, optionally fetched from Meta. |
||
| 52 | * @param Project $project Global rights and Meta-changed rights will |
||
| 53 | * automatically use the Meta Project. This Project instance is re-used |
||
| 54 | * if it is already Meta, otherwise a new Project instance is created. |
||
| 55 | * @param User $user |
||
| 56 | * @param string $type One of 'local' - query the local rights log, |
||
| 57 | * 'meta' - query for username@dbname for local rights changes made on Meta, or |
||
| 58 | * 'global' - query for global rights changes. |
||
| 59 | * @return array |
||
| 60 | */ |
||
| 61 | private function queryRightsChanges(Project $project, User $user, string $type = 'local'): array |
||
| 62 | { |
||
| 63 | $dbName = $project->getDatabaseName(); |
||
| 64 | |||
| 65 | // Global rights and Meta-changed rights should use a Meta Project. |
||
| 66 | if ('local' !== $type) { |
||
| 67 | $dbName = 'metawiki'; |
||
| 68 | } |
||
| 69 | |||
| 70 | $loggingTable = $this->getTableName($dbName, 'logging', 'logindex'); |
||
| 71 | $commentTable = $this->getTableName($dbName, 'comment', 'logging'); |
||
| 72 | $actorTable = $this->getTableName($dbName, 'actor', 'logging'); |
||
| 73 | $username = str_replace(' ', '_', $user->getUsername()); |
||
| 74 | |||
| 75 | if ('meta' === $type) { |
||
| 76 | // Reference the original Project. |
||
| 77 | $username .= '@'.$project->getDatabaseName(); |
||
| 78 | } |
||
| 79 | |||
| 80 | // Way back when it was possible to have usernames with lowercase characters. |
||
| 81 | // Some log entries aren't caught unless we look for both variations. |
||
| 82 | $usernameLower = lcfirst($username); |
||
| 83 | |||
| 84 | $logType = 'global' == $type ? 'gblrights' : 'rights'; |
||
| 85 | |||
| 86 | $sql = "SELECT log_id, log_timestamp, log_params, log_action, actor_name AS `performer`, |
||
| 87 | IFNULL(comment_text, '') AS `log_comment`, '$type' AS type |
||
| 88 | FROM $loggingTable |
||
| 89 | JOIN $actorTable ON log_actor = actor_id |
||
| 90 | LEFT OUTER JOIN $commentTable ON comment_id = log_comment_id |
||
| 91 | WHERE log_type = '$logType' |
||
| 92 | AND log_namespace = 2 |
||
| 93 | AND log_title IN (:username, :username2)"; |
||
| 94 | |||
| 95 | return $this->executeProjectsQuery($dbName, $sql, [ |
||
| 96 | 'username' => $username, |
||
| 97 | 'username2' => $usernameLower, |
||
| 98 | ])->fetchAllAssociative(); |
||
| 99 | } |
||
| 100 | |||
| 101 | /** |
||
| 102 | * Get the localized names for all user groups on given Project (and global), |
||
| 103 | * fetched from on-wiki system messages. |
||
| 104 | * @param Project $project |
||
| 105 | * @param string $lang Language code to pass in. |
||
| 106 | * @return string[] Localized names keyed by database value. |
||
| 107 | */ |
||
| 108 | public function getRightsNames(Project $project, string $lang): array |
||
| 109 | { |
||
| 110 | $cacheKey = $this->getCacheKey(func_get_args(), 'project_rights_names'); |
||
| 111 | if ($this->cache->hasItem($cacheKey)) { |
||
| 112 | return $this->cache->getItem($cacheKey)->get(); |
||
| 113 | } |
||
| 114 | |||
| 115 | $rightsPaths = array_map(function ($right) { |
||
| 116 | return "Group-$right-member"; |
||
| 117 | }, $this->getRawRightsNames($project)); |
||
| 118 | |||
| 119 | $rightsNames = []; |
||
| 120 | |||
| 121 | for ($i = 0; $i < count($rightsPaths); $i += 50) { |
||
|
0 ignored issues
–
show
|
|||
| 122 | $rightsSlice = array_slice($rightsPaths, $i, 50); |
||
| 123 | $params = [ |
||
| 124 | 'action' => 'query', |
||
| 125 | 'meta' => 'allmessages', |
||
| 126 | 'ammessages' => implode('|', $rightsSlice), |
||
| 127 | 'amlang' => $lang, |
||
| 128 | 'amenableparser' => 1, |
||
| 129 | 'formatversion' => 2, |
||
| 130 | ]; |
||
| 131 | $result = $this->executeApiRequest($project, $params)['query']['allmessages']; |
||
| 132 | |||
| 133 | foreach ($result as $msg) { |
||
| 134 | $normalized = preg_replace('/^group-|-member$/', '', $msg['normalizedname']); |
||
| 135 | $rightsNames[$normalized] = $msg['content'] ?? $normalized; |
||
| 136 | } |
||
| 137 | } |
||
| 138 | |||
| 139 | // Cache for one day and return. |
||
| 140 | return $this->setCache($cacheKey, $rightsNames, 'P1D'); |
||
| 141 | } |
||
| 142 | |||
| 143 | /** |
||
| 144 | * Get the names of all the possible local and global user groups. |
||
| 145 | * @param Project $project |
||
| 146 | * @return string[] |
||
| 147 | */ |
||
| 148 | private function getRawRightsNames(Project $project): array |
||
| 149 | { |
||
| 150 | $ugTable = $project->getTableName('user_groups'); |
||
| 151 | $ufgTable = $project->getTableName('user_former_groups'); |
||
| 152 | $sql = "SELECT DISTINCT(ug_group) |
||
| 153 | FROM $ugTable |
||
| 154 | UNION |
||
| 155 | SELECT DISTINCT(ufg_group) |
||
| 156 | FROM $ufgTable"; |
||
| 157 | |||
| 158 | $groups = $this->executeProjectsQuery($project, $sql)->fetchFirstColumn(); |
||
| 159 | |||
| 160 | if ($this->isWMF) { |
||
| 161 | $sql = "SELECT DISTINCT(gug_group) FROM centralauth_p.global_user_groups"; |
||
| 162 | $groups = array_merge( |
||
| 163 | $groups, |
||
| 164 | $this->executeProjectsQuery('centralauth', $sql)->fetchFirstColumn(), |
||
| 165 | // WMF installations have a special 'autoconfirmed' user group. |
||
| 166 | ['autoconfirmed'] |
||
| 167 | ); |
||
| 168 | } |
||
| 169 | |||
| 170 | return array_unique($groups); |
||
| 171 | } |
||
| 172 | |||
| 173 | /** |
||
| 174 | * Get the threshold values to become autoconfirmed for the given Project. |
||
| 175 | * Yes, eval is bad, but here we're validating only mathematical expressions are ran. |
||
| 176 | * @param Project $project |
||
| 177 | * @return array|null With keys 'wgAutoConfirmAge' and 'wgAutoConfirmCount'. Null if not found/not applicable. |
||
| 178 | */ |
||
| 179 | public function getAutoconfirmedAgeAndCount(Project $project): ?array |
||
| 180 | { |
||
| 181 | if (!$this->isWMF) { |
||
| 182 | return null; |
||
| 183 | } |
||
| 184 | |||
| 185 | // Set up cache. |
||
| 186 | $cacheKey = $this->getCacheKey(func_get_args(), 'ec_rightschanges_autoconfirmed'); |
||
| 187 | if ($this->cache->hasItem($cacheKey)) { |
||
| 188 | return $this->cache->getItem($cacheKey)->get(); |
||
| 189 | } |
||
| 190 | |||
| 191 | $url = 'https://noc.wikimedia.org/conf/InitialiseSettings.php.txt'; |
||
| 192 | $contents = $this->guzzle->request('GET', $url) |
||
| 193 | ->getBody() |
||
| 194 | ->getContents(); |
||
| 195 | |||
| 196 | $dbname = $project->getDatabaseName(); |
||
| 197 | if ('wikidatawiki' === $dbname) { |
||
| 198 | // Edge-case: 'wikidata' is an alias. |
||
| 199 | $dbname = 'wikidatawiki|wikidata'; |
||
| 200 | } |
||
| 201 | $dbNameRegex = "/'$dbname'\s*=>\s*([\d*\s]+)/s"; |
||
| 202 | $defaultRegex = "/'default'\s*=>\s*([\d*\s]+)/s"; |
||
| 203 | $out = []; |
||
| 204 | |||
| 205 | foreach (['wgAutoConfirmAge', 'wgAutoConfirmCount'] as $type) { |
||
| 206 | // Extract the text of the file that contains the rules we're looking for. |
||
| 207 | $typeRegex = "/\'$type.*?\]/s"; |
||
| 208 | $matches = []; |
||
| 209 | if (1 === preg_match($typeRegex, $contents, $matches)) { |
||
| 210 | $group = $matches[0]; |
||
| 211 | |||
| 212 | // Find the autoconfirmed expression for the $type and $dbname. |
||
| 213 | $matches = []; |
||
| 214 | if (1 === preg_match($dbNameRegex, $group, $matches)) { |
||
| 215 | $out[$type] = (int)eval('return('.$matches[1].');'); |
||
|
0 ignored issues
–
show
|
|||
| 216 | continue; |
||
| 217 | } |
||
| 218 | |||
| 219 | // Find the autoconfirmed expression for the 'default' and $dbname. |
||
| 220 | $matches = []; |
||
| 221 | if (1 === preg_match($defaultRegex, $group, $matches)) { |
||
| 222 | $out[$type] = (int)eval('return('.$matches[1].');'); |
||
|
0 ignored issues
–
show
|
|||
| 223 | continue; |
||
| 224 | } |
||
| 225 | } else { |
||
| 226 | return null; |
||
| 227 | } |
||
| 228 | } |
||
| 229 | |||
| 230 | // Cache for one day and return. |
||
| 231 | return $this->setCache($cacheKey, $out, 'P1D'); |
||
| 232 | } |
||
| 233 | |||
| 234 | /** |
||
| 235 | * Get the timestamp of the nth edit made by the given user. |
||
| 236 | * @param Project $project |
||
| 237 | * @param User $user |
||
| 238 | * @param string $offset Date to start at, in YYYYMMDDHHSS format. |
||
| 239 | * @param int $edits Offset of rows to look for (edit threshold for autoconfirmed). |
||
| 240 | * @return string|false Timestamp in YYYYMMDDHHSS format. False if not found. |
||
| 241 | */ |
||
| 242 | public function getNthEditTimestamp(Project $project, User $user, string $offset, int $edits) |
||
| 243 | { |
||
| 244 | $cacheKey = $this->getCacheKey(func_get_args(), 'ec_rightschanges_nthtimestamp'); |
||
| 245 | if ($this->cache->hasItem($cacheKey)) { |
||
| 246 | return $this->cache->getItem($cacheKey)->get(); |
||
| 247 | } |
||
| 248 | |||
| 249 | $revisionTable = $project->getTableName('revision'); |
||
| 250 | $sql = "SELECT rev_timestamp |
||
| 251 | FROM $revisionTable |
||
| 252 | WHERE rev_actor = :actorId |
||
| 253 | AND rev_timestamp >= $offset |
||
| 254 | LIMIT 1 OFFSET ".($edits - 1); |
||
| 255 | |||
| 256 | $ret = $this->executeProjectsQuery($project, $sql, [ |
||
| 257 | 'actorId' => $user->getActorId($project), |
||
| 258 | ])->fetchOne(); |
||
| 259 | |||
| 260 | // Cache and return. |
||
| 261 | return $this->setCache($cacheKey, $ret); |
||
| 262 | } |
||
| 263 | |||
| 264 | /** |
||
| 265 | * Get the number of edits the user has made as of the given timestamp. |
||
| 266 | * @param Project $project |
||
| 267 | * @param User $user |
||
| 268 | * @param string $timestamp In YYYYMMDDHHSS format. |
||
| 269 | * @return int |
||
| 270 | */ |
||
| 271 | public function getNumEditsByTimestamp(Project $project, User $user, string $timestamp): int |
||
| 272 | { |
||
| 273 | $cacheKey = $this->getCacheKey(func_get_args(), 'ec_rightschanges_editstimestamp'); |
||
| 274 | if ($this->cache->hasItem($cacheKey)) { |
||
| 275 | return $this->cache->getItem($cacheKey)->get(); |
||
| 276 | } |
||
| 277 | |||
| 278 | $revisionTable = $project->getTableName('revision'); |
||
| 279 | $sql = "SELECT COUNT(rev_id) |
||
| 280 | FROM $revisionTable |
||
| 281 | WHERE rev_actor = :actorId |
||
| 282 | AND rev_timestamp <= $timestamp"; |
||
| 283 | |||
| 284 | $ret = (int)$this->executeProjectsQuery($project, $sql, [ |
||
| 285 | 'actorId' => $user->getActorId($project), |
||
| 286 | ])->fetchOne(); |
||
| 287 | |||
| 288 | // Cache and return. |
||
| 289 | return $this->setCache($cacheKey, $ret); |
||
| 290 | } |
||
| 291 | } |
||
| 292 |
If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration: