wikimedia /
mediawiki
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.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | |||
| 3 | use MediaWiki\Linker\LinkTarget; |
||
| 4 | use Wikimedia\Assert\Assert; |
||
| 5 | |||
| 6 | /** |
||
| 7 | * Class performing complex database queries related to WatchedItems. |
||
| 8 | * |
||
| 9 | * @since 1.28 |
||
| 10 | * |
||
| 11 | * @file |
||
| 12 | * @ingroup Watchlist |
||
| 13 | * |
||
| 14 | * @license GNU GPL v2+ |
||
| 15 | */ |
||
| 16 | class WatchedItemQueryService { |
||
| 17 | |||
| 18 | const DIR_OLDER = 'older'; |
||
| 19 | const DIR_NEWER = 'newer'; |
||
| 20 | |||
| 21 | const INCLUDE_FLAGS = 'flags'; |
||
| 22 | const INCLUDE_USER = 'user'; |
||
| 23 | const INCLUDE_USER_ID = 'userid'; |
||
| 24 | const INCLUDE_COMMENT = 'comment'; |
||
| 25 | const INCLUDE_PATROL_INFO = 'patrol'; |
||
| 26 | const INCLUDE_SIZES = 'sizes'; |
||
| 27 | const INCLUDE_LOG_INFO = 'loginfo'; |
||
| 28 | |||
| 29 | // FILTER_* constants are part of public API (are used in ApiQueryWatchlist and |
||
| 30 | // ApiQueryWatchlistRaw classes) and should not be changed. |
||
| 31 | // Changing values of those constants will result in a breaking change in the API |
||
| 32 | const FILTER_MINOR = 'minor'; |
||
| 33 | const FILTER_NOT_MINOR = '!minor'; |
||
| 34 | const FILTER_BOT = 'bot'; |
||
| 35 | const FILTER_NOT_BOT = '!bot'; |
||
| 36 | const FILTER_ANON = 'anon'; |
||
| 37 | const FILTER_NOT_ANON = '!anon'; |
||
| 38 | const FILTER_PATROLLED = 'patrolled'; |
||
| 39 | const FILTER_NOT_PATROLLED = '!patrolled'; |
||
| 40 | const FILTER_UNREAD = 'unread'; |
||
| 41 | const FILTER_NOT_UNREAD = '!unread'; |
||
| 42 | const FILTER_CHANGED = 'changed'; |
||
| 43 | const FILTER_NOT_CHANGED = '!changed'; |
||
| 44 | |||
| 45 | const SORT_ASC = 'ASC'; |
||
| 46 | const SORT_DESC = 'DESC'; |
||
| 47 | |||
| 48 | /** |
||
| 49 | * @var LoadBalancer |
||
| 50 | */ |
||
| 51 | private $loadBalancer; |
||
| 52 | |||
| 53 | /** @var WatchedItemQueryServiceExtension[]|null */ |
||
| 54 | private $extensions = null; |
||
| 55 | |||
| 56 | public function __construct( LoadBalancer $loadBalancer ) { |
||
| 57 | $this->loadBalancer = $loadBalancer; |
||
| 58 | } |
||
| 59 | |||
| 60 | /** |
||
| 61 | * @return WatchedItemQueryServiceExtension[] |
||
| 62 | */ |
||
| 63 | private function getExtensions() { |
||
| 64 | if ( $this->extensions === null ) { |
||
| 65 | $this->extensions = []; |
||
| 66 | Hooks::run( 'WatchedItemQueryServiceExtensions', [ &$this->extensions, $this ] ); |
||
| 67 | } |
||
| 68 | return $this->extensions; |
||
| 69 | } |
||
| 70 | |||
| 71 | /** |
||
| 72 | * @return IDatabase |
||
| 73 | * @throws MWException |
||
| 74 | */ |
||
| 75 | private function getConnection() { |
||
| 76 | return $this->loadBalancer->getConnectionRef( DB_REPLICA, [ 'watchlist' ] ); |
||
| 77 | } |
||
| 78 | |||
| 79 | /** |
||
| 80 | * @param User $user |
||
| 81 | * @param array $options Allowed keys: |
||
| 82 | * 'includeFields' => string[] RecentChange fields to be included in the result, |
||
| 83 | * self::INCLUDE_* constants should be used |
||
| 84 | * 'filters' => string[] optional filters to narrow down resulted items |
||
| 85 | * 'namespaceIds' => int[] optional namespace IDs to filter by |
||
| 86 | * (defaults to all namespaces) |
||
| 87 | * 'allRevisions' => bool return multiple revisions of the same page if true, |
||
| 88 | * only the most recent if false (default) |
||
| 89 | * 'rcTypes' => int[] which types of RecentChanges to include |
||
| 90 | * (defaults to all types), allowed values: RC_EDIT, RC_NEW, |
||
| 91 | * RC_LOG, RC_EXTERNAL, RC_CATEGORIZE |
||
| 92 | * 'onlyByUser' => string only list changes by a specified user |
||
| 93 | * 'notByUser' => string do not incluide changes by a specified user |
||
| 94 | * 'dir' => string in which direction to enumerate, accepted values: |
||
| 95 | * - DIR_OLDER list newest first |
||
| 96 | * - DIR_NEWER list oldest first |
||
| 97 | * 'start' => string (format accepted by wfTimestamp) requires 'dir' option, |
||
| 98 | * timestamp to start enumerating from |
||
| 99 | * 'end' => string (format accepted by wfTimestamp) requires 'dir' option, |
||
| 100 | * timestamp to end enumerating |
||
| 101 | * 'watchlistOwner' => User user whose watchlist items should be listed if different |
||
| 102 | * than the one specified with $user param, |
||
| 103 | * requires 'watchlistOwnerToken' option |
||
| 104 | * 'watchlistOwnerToken' => string a watchlist token used to access another user's |
||
| 105 | * watchlist, used with 'watchlistOwnerToken' option |
||
| 106 | * 'limit' => int maximum numbers of items to return |
||
| 107 | * 'usedInGenerator' => bool include only RecentChange id field required by the |
||
| 108 | * generator ('rc_cur_id' or 'rc_this_oldid') if true, or all |
||
| 109 | * id fields ('rc_cur_id', 'rc_this_oldid', 'rc_last_oldid') |
||
| 110 | * if false (default) |
||
| 111 | * @param array|null &$startFrom Continuation value: [ string $rcTimestamp, int $rcId ] |
||
| 112 | * @return array of pairs ( WatchedItem $watchedItem, string[] $recentChangeInfo ), |
||
| 113 | * where $recentChangeInfo contains the following keys: |
||
| 114 | * - 'rc_id', |
||
| 115 | * - 'rc_namespace', |
||
| 116 | * - 'rc_title', |
||
| 117 | * - 'rc_timestamp', |
||
| 118 | * - 'rc_type', |
||
| 119 | * - 'rc_deleted', |
||
| 120 | * Additional keys could be added by specifying the 'includeFields' option |
||
| 121 | */ |
||
| 122 | public function getWatchedItemsWithRecentChangeInfo( |
||
| 123 | User $user, array $options = [], &$startFrom = null |
||
| 124 | ) { |
||
| 125 | $options += [ |
||
| 126 | 'includeFields' => [], |
||
| 127 | 'namespaceIds' => [], |
||
| 128 | 'filters' => [], |
||
| 129 | 'allRevisions' => false, |
||
| 130 | 'usedInGenerator' => false |
||
| 131 | ]; |
||
| 132 | |||
| 133 | Assert::parameter( |
||
| 134 | !isset( $options['rcTypes'] ) |
||
| 135 | || !array_diff( $options['rcTypes'], [ RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL, RC_CATEGORIZE ] ), |
||
| 136 | '$options[\'rcTypes\']', |
||
| 137 | 'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE' |
||
| 138 | ); |
||
| 139 | Assert::parameter( |
||
| 140 | !isset( $options['dir'] ) || in_array( $options['dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ), |
||
| 141 | '$options[\'dir\']', |
||
| 142 | 'must be DIR_OLDER or DIR_NEWER' |
||
| 143 | ); |
||
| 144 | Assert::parameter( |
||
| 145 | !isset( $options['start'] ) && !isset( $options['end'] ) && $startFrom === null |
||
| 146 | || isset( $options['dir'] ), |
||
| 147 | '$options[\'dir\']', |
||
| 148 | 'must be provided when providing the "start" or "end" options or the $startFrom parameter' |
||
| 149 | ); |
||
| 150 | Assert::parameter( |
||
| 151 | !isset( $options['startFrom'] ), |
||
| 152 | '$options[\'startFrom\']', |
||
| 153 | 'must not be provided, use $startFrom instead' |
||
| 154 | ); |
||
| 155 | Assert::parameter( |
||
| 156 | !isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ), |
||
| 157 | '$startFrom', |
||
| 158 | 'must be a two-element array' |
||
| 159 | ); |
||
| 160 | if ( array_key_exists( 'watchlistOwner', $options ) ) { |
||
| 161 | Assert::parameterType( |
||
| 162 | User::class, |
||
| 163 | $options['watchlistOwner'], |
||
| 164 | '$options[\'watchlistOwner\']' |
||
| 165 | ); |
||
| 166 | Assert::parameter( |
||
| 167 | isset( $options['watchlistOwnerToken'] ), |
||
| 168 | '$options[\'watchlistOwnerToken\']', |
||
| 169 | 'must be provided when providing watchlistOwner option' |
||
| 170 | ); |
||
| 171 | } |
||
| 172 | |||
| 173 | $tables = [ 'recentchanges', 'watchlist' ]; |
||
| 174 | if ( !$options['allRevisions'] ) { |
||
| 175 | $tables[] = 'page'; |
||
| 176 | } |
||
| 177 | |||
| 178 | $db = $this->getConnection(); |
||
| 179 | |||
| 180 | $fields = $this->getWatchedItemsWithRCInfoQueryFields( $options ); |
||
| 181 | $conds = $this->getWatchedItemsWithRCInfoQueryConds( $db, $user, $options ); |
||
| 182 | $dbOptions = $this->getWatchedItemsWithRCInfoQueryDbOptions( $options ); |
||
| 183 | $joinConds = $this->getWatchedItemsWithRCInfoQueryJoinConds( $options ); |
||
| 184 | |||
| 185 | if ( $startFrom !== null ) { |
||
| 186 | $conds[] = $this->getStartFromConds( $db, $options, $startFrom ); |
||
| 187 | } |
||
| 188 | |||
| 189 | foreach ( $this->getExtensions() as $extension ) { |
||
| 190 | $extension->modifyWatchedItemsWithRCInfoQuery( |
||
| 191 | $user, $options, $db, |
||
| 192 | $tables, |
||
| 193 | $fields, |
||
| 194 | $conds, |
||
| 195 | $dbOptions, |
||
| 196 | $joinConds |
||
| 197 | ); |
||
| 198 | } |
||
| 199 | |||
| 200 | $res = $db->select( |
||
| 201 | $tables, |
||
| 202 | $fields, |
||
| 203 | $conds, |
||
| 204 | __METHOD__, |
||
| 205 | $dbOptions, |
||
| 206 | $joinConds |
||
| 207 | ); |
||
| 208 | |||
| 209 | $limit = isset( $dbOptions['LIMIT'] ) ? $dbOptions['LIMIT'] : INF; |
||
| 210 | $items = []; |
||
| 211 | $startFrom = null; |
||
| 212 | foreach ( $res as $row ) { |
||
|
0 ignored issues
–
show
|
|||
| 213 | if ( --$limit <= 0 ) { |
||
| 214 | $startFrom = [ $row->rc_timestamp, $row->rc_id ]; |
||
| 215 | break; |
||
| 216 | } |
||
| 217 | |||
| 218 | $items[] = [ |
||
| 219 | new WatchedItem( |
||
| 220 | $user, |
||
| 221 | new TitleValue( (int)$row->rc_namespace, $row->rc_title ), |
||
| 222 | $row->wl_notificationtimestamp |
||
| 223 | ), |
||
| 224 | $this->getRecentChangeFieldsFromRow( $row ) |
||
| 225 | ]; |
||
| 226 | } |
||
| 227 | |||
| 228 | foreach ( $this->getExtensions() as $extension ) { |
||
| 229 | $extension->modifyWatchedItemsWithRCInfo( $user, $options, $db, $items, $res, $startFrom ); |
||
| 230 | } |
||
| 231 | |||
| 232 | return $items; |
||
| 233 | } |
||
| 234 | |||
| 235 | /** |
||
| 236 | * For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser |
||
| 237 | * |
||
| 238 | * @param User $user |
||
| 239 | * @param array $options Allowed keys: |
||
| 240 | * 'sort' => string optional sorting by namespace ID and title |
||
| 241 | * one of the self::SORT_* constants |
||
| 242 | * 'namespaceIds' => int[] optional namespace IDs to filter by (defaults to all namespaces) |
||
| 243 | * 'limit' => int maximum number of items to return |
||
| 244 | * 'filter' => string optional filter, one of the self::FILTER_* contants |
||
| 245 | * 'from' => LinkTarget requires 'sort' key, only return items starting from |
||
| 246 | * those related to the link target |
||
| 247 | * 'until' => LinkTarget requires 'sort' key, only return items until |
||
| 248 | * those related to the link target |
||
| 249 | * 'startFrom' => LinkTarget requires 'sort' key, only return items starting from |
||
| 250 | * those related to the link target, allows to skip some link targets |
||
| 251 | * specified using the form option |
||
| 252 | * @return WatchedItem[] |
||
| 253 | */ |
||
| 254 | public function getWatchedItemsForUser( User $user, array $options = [] ) { |
||
| 255 | if ( $user->isAnon() ) { |
||
| 256 | // TODO: should this just return an empty array or rather complain loud at this point |
||
| 257 | // as e.g. ApiBase::getWatchlistUser does? |
||
| 258 | return []; |
||
| 259 | } |
||
| 260 | |||
| 261 | $options += [ 'namespaceIds' => [] ]; |
||
| 262 | |||
| 263 | Assert::parameter( |
||
| 264 | !isset( $options['sort'] ) || in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ), |
||
| 265 | '$options[\'sort\']', |
||
| 266 | 'must be SORT_ASC or SORT_DESC' |
||
| 267 | ); |
||
| 268 | Assert::parameter( |
||
| 269 | !isset( $options['filter'] ) || in_array( |
||
| 270 | $options['filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ] |
||
| 271 | ), |
||
| 272 | '$options[\'filter\']', |
||
| 273 | 'must be FILTER_CHANGED or FILTER_NOT_CHANGED' |
||
| 274 | ); |
||
| 275 | Assert::parameter( |
||
| 276 | !isset( $options['from'] ) && !isset( $options['until'] ) && !isset( $options['startFrom'] ) |
||
| 277 | || isset( $options['sort'] ), |
||
| 278 | '$options[\'sort\']', |
||
| 279 | 'must be provided if any of "from", "until", "startFrom" options is provided' |
||
| 280 | ); |
||
| 281 | |||
| 282 | $db = $this->getConnection(); |
||
| 283 | |||
| 284 | $conds = $this->getWatchedItemsForUserQueryConds( $db, $user, $options ); |
||
| 285 | $dbOptions = $this->getWatchedItemsForUserQueryDbOptions( $options ); |
||
| 286 | |||
| 287 | $res = $db->select( |
||
| 288 | 'watchlist', |
||
| 289 | [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], |
||
| 290 | $conds, |
||
| 291 | __METHOD__, |
||
| 292 | $dbOptions |
||
| 293 | ); |
||
| 294 | |||
| 295 | $watchedItems = []; |
||
| 296 | View Code Duplication | foreach ( $res as $row ) { |
|
|
0 ignored issues
–
show
The expression
$res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?
There are different options of fixing this problem.
Loading history...
|
|||
| 297 | // todo these could all be cached at some point? |
||
| 298 | $watchedItems[] = new WatchedItem( |
||
| 299 | $user, |
||
| 300 | new TitleValue( (int)$row->wl_namespace, $row->wl_title ), |
||
| 301 | $row->wl_notificationtimestamp |
||
| 302 | ); |
||
| 303 | } |
||
| 304 | |||
| 305 | return $watchedItems; |
||
| 306 | } |
||
| 307 | |||
| 308 | private function getRecentChangeFieldsFromRow( stdClass $row ) { |
||
| 309 | // This can be simplified to single array_filter call filtering by key value, |
||
| 310 | // once we stop supporting PHP 5.5 |
||
| 311 | $allFields = get_object_vars( $row ); |
||
| 312 | $rcKeys = array_filter( |
||
| 313 | array_keys( $allFields ), |
||
| 314 | function( $key ) { |
||
| 315 | return substr( $key, 0, 3 ) === 'rc_'; |
||
| 316 | } |
||
| 317 | ); |
||
| 318 | return array_intersect_key( $allFields, array_flip( $rcKeys ) ); |
||
| 319 | } |
||
| 320 | |||
| 321 | private function getWatchedItemsWithRCInfoQueryFields( array $options ) { |
||
| 322 | $fields = [ |
||
| 323 | 'rc_id', |
||
| 324 | 'rc_namespace', |
||
| 325 | 'rc_title', |
||
| 326 | 'rc_timestamp', |
||
| 327 | 'rc_type', |
||
| 328 | 'rc_deleted', |
||
| 329 | 'wl_notificationtimestamp' |
||
| 330 | ]; |
||
| 331 | |||
| 332 | $rcIdFields = [ |
||
| 333 | 'rc_cur_id', |
||
| 334 | 'rc_this_oldid', |
||
| 335 | 'rc_last_oldid', |
||
| 336 | ]; |
||
| 337 | if ( $options['usedInGenerator'] ) { |
||
| 338 | if ( $options['allRevisions'] ) { |
||
| 339 | $rcIdFields = [ 'rc_this_oldid' ]; |
||
| 340 | } else { |
||
| 341 | $rcIdFields = [ 'rc_cur_id' ]; |
||
| 342 | } |
||
| 343 | } |
||
| 344 | $fields = array_merge( $fields, $rcIdFields ); |
||
| 345 | |||
| 346 | if ( in_array( self::INCLUDE_FLAGS, $options['includeFields'] ) ) { |
||
| 347 | $fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] ); |
||
| 348 | } |
||
| 349 | if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) { |
||
| 350 | $fields[] = 'rc_user_text'; |
||
| 351 | } |
||
| 352 | if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) { |
||
| 353 | $fields[] = 'rc_user'; |
||
| 354 | } |
||
| 355 | if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) { |
||
| 356 | $fields[] = 'rc_comment'; |
||
| 357 | } |
||
| 358 | if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) { |
||
| 359 | $fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] ); |
||
| 360 | } |
||
| 361 | if ( in_array( self::INCLUDE_SIZES, $options['includeFields'] ) ) { |
||
| 362 | $fields = array_merge( $fields, [ 'rc_old_len', 'rc_new_len' ] ); |
||
| 363 | } |
||
| 364 | if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) { |
||
| 365 | $fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] ); |
||
| 366 | } |
||
| 367 | |||
| 368 | return $fields; |
||
| 369 | } |
||
| 370 | |||
| 371 | private function getWatchedItemsWithRCInfoQueryConds( |
||
| 372 | IDatabase $db, |
||
| 373 | User $user, |
||
| 374 | array $options |
||
| 375 | ) { |
||
| 376 | $watchlistOwnerId = $this->getWatchlistOwnerId( $user, $options ); |
||
| 377 | $conds = [ 'wl_user' => $watchlistOwnerId ]; |
||
| 378 | |||
| 379 | if ( !$options['allRevisions'] ) { |
||
| 380 | $conds[] = $db->makeList( |
||
| 381 | [ 'rc_this_oldid=page_latest', 'rc_type=' . RC_LOG ], |
||
| 382 | LIST_OR |
||
| 383 | ); |
||
| 384 | } |
||
| 385 | |||
| 386 | if ( $options['namespaceIds'] ) { |
||
| 387 | $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] ); |
||
| 388 | } |
||
| 389 | |||
| 390 | if ( array_key_exists( 'rcTypes', $options ) ) { |
||
| 391 | $conds['rc_type'] = array_map( 'intval', $options['rcTypes'] ); |
||
| 392 | } |
||
| 393 | |||
| 394 | $conds = array_merge( |
||
| 395 | $conds, |
||
| 396 | $this->getWatchedItemsWithRCInfoQueryFilterConds( $user, $options ) |
||
| 397 | ); |
||
| 398 | |||
| 399 | $conds = array_merge( $conds, $this->getStartEndConds( $db, $options ) ); |
||
| 400 | |||
| 401 | if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) { |
||
| 402 | if ( $db->getType() === 'mysql' ) { |
||
| 403 | // This is an index optimization for mysql |
||
| 404 | $conds[] = "rc_timestamp > ''"; |
||
| 405 | } |
||
| 406 | } |
||
| 407 | |||
| 408 | $conds = array_merge( $conds, $this->getUserRelatedConds( $db, $user, $options ) ); |
||
| 409 | |||
| 410 | $deletedPageLogCond = $this->getExtraDeletedPageLogEntryRelatedCond( $db, $user ); |
||
| 411 | if ( $deletedPageLogCond ) { |
||
| 412 | $conds[] = $deletedPageLogCond; |
||
| 413 | } |
||
| 414 | |||
| 415 | return $conds; |
||
| 416 | } |
||
| 417 | |||
| 418 | private function getWatchlistOwnerId( User $user, array $options ) { |
||
| 419 | if ( array_key_exists( 'watchlistOwner', $options ) ) { |
||
| 420 | /** @var User $watchlistOwner */ |
||
| 421 | $watchlistOwner = $options['watchlistOwner']; |
||
| 422 | $ownersToken = $watchlistOwner->getOption( 'watchlisttoken' ); |
||
| 423 | $token = $options['watchlistOwnerToken']; |
||
| 424 | if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) { |
||
| 425 | throw new UsageException( |
||
| 426 | 'Incorrect watchlist token provided -- please set a correct token in Special:Preferences', |
||
| 427 | 'bad_wltoken' |
||
| 428 | ); |
||
| 429 | } |
||
| 430 | return $watchlistOwner->getId(); |
||
| 431 | } |
||
| 432 | return $user->getId(); |
||
| 433 | } |
||
| 434 | |||
| 435 | private function getWatchedItemsWithRCInfoQueryFilterConds( User $user, array $options ) { |
||
| 436 | $conds = []; |
||
| 437 | |||
| 438 | View Code Duplication | if ( in_array( self::FILTER_MINOR, $options['filters'] ) ) { |
|
| 439 | $conds[] = 'rc_minor != 0'; |
||
| 440 | } elseif ( in_array( self::FILTER_NOT_MINOR, $options['filters'] ) ) { |
||
| 441 | $conds[] = 'rc_minor = 0'; |
||
| 442 | } |
||
| 443 | |||
| 444 | View Code Duplication | if ( in_array( self::FILTER_BOT, $options['filters'] ) ) { |
|
| 445 | $conds[] = 'rc_bot != 0'; |
||
| 446 | } elseif ( in_array( self::FILTER_NOT_BOT, $options['filters'] ) ) { |
||
| 447 | $conds[] = 'rc_bot = 0'; |
||
| 448 | } |
||
| 449 | |||
| 450 | View Code Duplication | if ( in_array( self::FILTER_ANON, $options['filters'] ) ) { |
|
| 451 | $conds[] = 'rc_user = 0'; |
||
| 452 | } elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) { |
||
| 453 | $conds[] = 'rc_user != 0'; |
||
| 454 | } |
||
| 455 | |||
| 456 | if ( $user->useRCPatrol() || $user->useNPPatrol() ) { |
||
| 457 | // TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol |
||
| 458 | // right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does? |
||
| 459 | View Code Duplication | if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) { |
|
| 460 | $conds[] = 'rc_patrolled != 0'; |
||
| 461 | } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) { |
||
| 462 | $conds[] = 'rc_patrolled = 0'; |
||
| 463 | } |
||
| 464 | } |
||
| 465 | |||
| 466 | View Code Duplication | if ( in_array( self::FILTER_UNREAD, $options['filters'] ) ) { |
|
| 467 | $conds[] = 'rc_timestamp >= wl_notificationtimestamp'; |
||
| 468 | } elseif ( in_array( self::FILTER_NOT_UNREAD, $options['filters'] ) ) { |
||
| 469 | // TODO: should this be changed to use Database::makeList? |
||
| 470 | $conds[] = 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp'; |
||
| 471 | } |
||
| 472 | |||
| 473 | return $conds; |
||
| 474 | } |
||
| 475 | |||
| 476 | private function getStartEndConds( IDatabase $db, array $options ) { |
||
| 477 | if ( !isset( $options['start'] ) && ! isset( $options['end'] ) ) { |
||
| 478 | return []; |
||
| 479 | } |
||
| 480 | |||
| 481 | $conds = []; |
||
| 482 | |||
| 483 | View Code Duplication | if ( isset( $options['start'] ) ) { |
|
| 484 | $after = $options['dir'] === self::DIR_OLDER ? '<=' : '>='; |
||
| 485 | $conds[] = 'rc_timestamp ' . $after . ' ' . |
||
| 486 | $db->addQuotes( $db->timestamp( $options['start'] ) ); |
||
| 487 | } |
||
| 488 | View Code Duplication | if ( isset( $options['end'] ) ) { |
|
| 489 | $before = $options['dir'] === self::DIR_OLDER ? '>=' : '<='; |
||
| 490 | $conds[] = 'rc_timestamp ' . $before . ' ' . |
||
| 491 | $db->addQuotes( $db->timestamp( $options['end'] ) ); |
||
| 492 | } |
||
| 493 | |||
| 494 | return $conds; |
||
| 495 | } |
||
| 496 | |||
| 497 | private function getUserRelatedConds( IDatabase $db, User $user, array $options ) { |
||
| 498 | if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) { |
||
| 499 | return []; |
||
| 500 | } |
||
| 501 | |||
| 502 | $conds = []; |
||
| 503 | |||
| 504 | if ( array_key_exists( 'onlyByUser', $options ) ) { |
||
| 505 | $conds['rc_user_text'] = $options['onlyByUser']; |
||
| 506 | } elseif ( array_key_exists( 'notByUser', $options ) ) { |
||
| 507 | $conds[] = 'rc_user_text != ' . $db->addQuotes( $options['notByUser'] ); |
||
| 508 | } |
||
| 509 | |||
| 510 | // Avoid brute force searches (bug 17342) |
||
| 511 | $bitmask = 0; |
||
| 512 | if ( !$user->isAllowed( 'deletedhistory' ) ) { |
||
| 513 | $bitmask = Revision::DELETED_USER; |
||
| 514 | } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { |
||
| 515 | $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; |
||
| 516 | } |
||
| 517 | if ( $bitmask ) { |
||
| 518 | $conds[] = $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask"; |
||
| 519 | } |
||
| 520 | |||
| 521 | return $conds; |
||
| 522 | } |
||
| 523 | |||
| 524 | private function getExtraDeletedPageLogEntryRelatedCond( IDatabase $db, User $user ) { |
||
| 525 | // LogPage::DELETED_ACTION hides the affected page, too. So hide those |
||
| 526 | // entirely from the watchlist, or someone could guess the title. |
||
| 527 | $bitmask = 0; |
||
| 528 | if ( !$user->isAllowed( 'deletedhistory' ) ) { |
||
| 529 | $bitmask = LogPage::DELETED_ACTION; |
||
| 530 | } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { |
||
| 531 | $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED; |
||
| 532 | } |
||
| 533 | View Code Duplication | if ( $bitmask ) { |
|
| 534 | return $db->makeList( [ |
||
| 535 | 'rc_type != ' . RC_LOG, |
||
| 536 | $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask", |
||
| 537 | ], LIST_OR ); |
||
| 538 | } |
||
| 539 | return ''; |
||
| 540 | } |
||
| 541 | |||
| 542 | private function getStartFromConds( IDatabase $db, array $options, array $startFrom ) { |
||
| 543 | $op = $options['dir'] === self::DIR_OLDER ? '<' : '>'; |
||
| 544 | list( $rcTimestamp, $rcId ) = $startFrom; |
||
| 545 | $rcTimestamp = $db->addQuotes( $db->timestamp( $rcTimestamp ) ); |
||
| 546 | $rcId = (int)$rcId; |
||
| 547 | return $db->makeList( |
||
| 548 | [ |
||
| 549 | "rc_timestamp $op $rcTimestamp", |
||
| 550 | $db->makeList( |
||
| 551 | [ |
||
| 552 | "rc_timestamp = $rcTimestamp", |
||
| 553 | "rc_id $op= $rcId" |
||
| 554 | ], |
||
| 555 | LIST_AND |
||
| 556 | ) |
||
| 557 | ], |
||
| 558 | LIST_OR |
||
| 559 | ); |
||
| 560 | } |
||
| 561 | |||
| 562 | private function getWatchedItemsForUserQueryConds( IDatabase $db, User $user, array $options ) { |
||
| 563 | $conds = [ 'wl_user' => $user->getId() ]; |
||
| 564 | if ( $options['namespaceIds'] ) { |
||
| 565 | $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] ); |
||
| 566 | } |
||
| 567 | if ( isset( $options['filter'] ) ) { |
||
| 568 | $filter = $options['filter']; |
||
| 569 | if ( $filter === self::FILTER_CHANGED ) { |
||
| 570 | $conds[] = 'wl_notificationtimestamp IS NOT NULL'; |
||
| 571 | } else { |
||
| 572 | $conds[] = 'wl_notificationtimestamp IS NULL'; |
||
| 573 | } |
||
| 574 | } |
||
| 575 | |||
| 576 | View Code Duplication | if ( isset( $options['from'] ) ) { |
|
| 577 | $op = $options['sort'] === self::SORT_ASC ? '>' : '<'; |
||
| 578 | $conds[] = $this->getFromUntilTargetConds( $db, $options['from'], $op ); |
||
| 579 | } |
||
| 580 | View Code Duplication | if ( isset( $options['until'] ) ) { |
|
| 581 | $op = $options['sort'] === self::SORT_ASC ? '<' : '>'; |
||
| 582 | $conds[] = $this->getFromUntilTargetConds( $db, $options['until'], $op ); |
||
| 583 | } |
||
| 584 | View Code Duplication | if ( isset( $options['startFrom'] ) ) { |
|
| 585 | $op = $options['sort'] === self::SORT_ASC ? '>' : '<'; |
||
| 586 | $conds[] = $this->getFromUntilTargetConds( $db, $options['startFrom'], $op ); |
||
| 587 | } |
||
| 588 | |||
| 589 | return $conds; |
||
| 590 | } |
||
| 591 | |||
| 592 | /** |
||
| 593 | * Creates a query condition part for getting only items before or after the given link target |
||
| 594 | * (while ordering using $sort mode) |
||
| 595 | * |
||
| 596 | * @param IDatabase $db |
||
| 597 | * @param LinkTarget $target |
||
| 598 | * @param string $op comparison operator to use in the conditions |
||
| 599 | * @return string |
||
| 600 | */ |
||
| 601 | private function getFromUntilTargetConds( IDatabase $db, LinkTarget $target, $op ) { |
||
| 602 | return $db->makeList( |
||
| 603 | [ |
||
| 604 | "wl_namespace $op " . $target->getNamespace(), |
||
| 605 | $db->makeList( |
||
| 606 | [ |
||
| 607 | 'wl_namespace = ' . $target->getNamespace(), |
||
| 608 | "wl_title $op= " . $db->addQuotes( $target->getDBkey() ) |
||
| 609 | ], |
||
| 610 | LIST_AND |
||
| 611 | ) |
||
| 612 | ], |
||
| 613 | LIST_OR |
||
| 614 | ); |
||
| 615 | } |
||
| 616 | |||
| 617 | private function getWatchedItemsWithRCInfoQueryDbOptions( array $options ) { |
||
| 618 | $dbOptions = []; |
||
| 619 | |||
| 620 | if ( array_key_exists( 'dir', $options ) ) { |
||
| 621 | $sort = $options['dir'] === self::DIR_OLDER ? ' DESC' : ''; |
||
| 622 | $dbOptions['ORDER BY'] = [ 'rc_timestamp' . $sort, 'rc_id' . $sort ]; |
||
| 623 | } |
||
| 624 | |||
| 625 | if ( array_key_exists( 'limit', $options ) ) { |
||
| 626 | $dbOptions['LIMIT'] = (int)$options['limit'] + 1; |
||
| 627 | } |
||
| 628 | |||
| 629 | return $dbOptions; |
||
| 630 | } |
||
| 631 | |||
| 632 | private function getWatchedItemsForUserQueryDbOptions( array $options ) { |
||
| 633 | $dbOptions = []; |
||
| 634 | if ( array_key_exists( 'sort', $options ) ) { |
||
| 635 | $dbOptions['ORDER BY'] = [ |
||
| 636 | "wl_namespace {$options['sort']}", |
||
| 637 | "wl_title {$options['sort']}" |
||
| 638 | ]; |
||
| 639 | if ( count( $options['namespaceIds'] ) === 1 ) { |
||
| 640 | $dbOptions['ORDER BY'] = "wl_title {$options['sort']}"; |
||
| 641 | } |
||
| 642 | } |
||
| 643 | if ( array_key_exists( 'limit', $options ) ) { |
||
| 644 | $dbOptions['LIMIT'] = (int)$options['limit']; |
||
| 645 | } |
||
| 646 | return $dbOptions; |
||
| 647 | } |
||
| 648 | |||
| 649 | private function getWatchedItemsWithRCInfoQueryJoinConds( array $options ) { |
||
| 650 | $joinConds = [ |
||
| 651 | 'watchlist' => [ 'INNER JOIN', |
||
| 652 | [ |
||
| 653 | 'wl_namespace=rc_namespace', |
||
| 654 | 'wl_title=rc_title' |
||
| 655 | ] |
||
| 656 | ] |
||
| 657 | ]; |
||
| 658 | if ( !$options['allRevisions'] ) { |
||
| 659 | $joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ]; |
||
| 660 | } |
||
| 661 | return $joinConds; |
||
| 662 | } |
||
| 663 | |||
| 664 | } |
||
| 665 |
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.