/** * Generates and outputs the result of this query based upon the provided parameters. * * @param ApiPageSet $resultPageSet */ public function run($resultPageSet = null) { $user = $this->getUser(); /* Get the parameters of the request. */ $params = $this->extractRequestParams(); /* Build our basic query. Namely, something along the lines of: * SELECT * FROM recentchanges WHERE rc_timestamp > $start * AND rc_timestamp < $end AND rc_namespace = $namespace */ $this->addTables('recentchanges'); $index = array('recentchanges' => 'rc_timestamp'); // May change $this->addTimestampWhereRange('rc_timestamp', $params['dir'], $params['start'], $params['end']); if (!is_null($params['continue'])) { $cont = explode('|', $params['continue']); $this->dieContinueUsageIf(count($cont) != 2); $db = $this->getDB(); $timestamp = $db->addQuotes($db->timestamp($cont[0])); $id = intval($cont[1]); $this->dieContinueUsageIf($id != $cont[1]); $op = $params['dir'] === 'older' ? '<' : '>'; $this->addWhere("rc_timestamp {$op} {$timestamp} OR " . "(rc_timestamp = {$timestamp} AND " . "rc_id {$op}= {$id})"); } $order = $params['dir'] === 'older' ? 'DESC' : 'ASC'; $this->addOption('ORDER BY', array("rc_timestamp {$order}", "rc_id {$order}")); $this->addWhereFld('rc_namespace', $params['namespace']); if (!is_null($params['type'])) { try { $this->addWhereFld('rc_type', RecentChange::parseToRCType($params['type'])); } catch (Exception $e) { ApiBase::dieDebug(__METHOD__, $e->getMessage()); } } if (!is_null($params['show'])) { $show = array_flip($params['show']); /* Check for conflicting parameters. */ if (isset($show['minor']) && isset($show['!minor']) || isset($show['bot']) && isset($show['!bot']) || isset($show['anon']) && isset($show['!anon']) || isset($show['redirect']) && isset($show['!redirect']) || isset($show['patrolled']) && isset($show['!patrolled']) || isset($show['patrolled']) && isset($show['unpatrolled']) || isset($show['!patrolled']) && isset($show['unpatrolled'])) { $this->dieUsageMsg('show'); } // Check permissions if (isset($show['patrolled']) || isset($show['!patrolled']) || isset($show['unpatrolled'])) { if (!$user->useRCPatrol() && !$user->useNPPatrol()) { $this->dieUsage('You need the patrol right to request the patrolled flag', 'permissiondenied'); } } /* Add additional conditions to query depending upon parameters. */ $this->addWhereIf('rc_minor = 0', isset($show['!minor'])); $this->addWhereIf('rc_minor != 0', isset($show['minor'])); $this->addWhereIf('rc_bot = 0', isset($show['!bot'])); $this->addWhereIf('rc_bot != 0', isset($show['bot'])); $this->addWhereIf('rc_user = 0', isset($show['anon'])); $this->addWhereIf('rc_user != 0', isset($show['!anon'])); $this->addWhereIf('rc_patrolled = 0', isset($show['!patrolled'])); $this->addWhereIf('rc_patrolled != 0', isset($show['patrolled'])); $this->addWhereIf('page_is_redirect = 1', isset($show['redirect'])); if (isset($show['unpatrolled'])) { // See ChangesList:isUnpatrolled if ($user->useRCPatrol()) { $this->addWhere('rc_patrolled = 0'); } elseif ($user->useNPPatrol()) { $this->addWhere('rc_patrolled = 0'); $this->addWhereFld('rc_type', RC_NEW); } } // Don't throw log entries out the window here $this->addWhereIf('page_is_redirect = 0 OR page_is_redirect IS NULL', isset($show['!redirect'])); } if (!is_null($params['user']) && !is_null($params['excludeuser'])) { $this->dieUsage('user and excludeuser cannot be used together', 'user-excludeuser'); } if (!is_null($params['user'])) { $this->addWhereFld('rc_user_text', $params['user']); $index['recentchanges'] = 'rc_user_text'; } if (!is_null($params['excludeuser'])) { // We don't use the rc_user_text index here because // * it would require us to sort by rc_user_text before rc_timestamp // * the != condition doesn't throw out too many rows anyway $this->addWhere('rc_user_text != ' . $this->getDB()->addQuotes($params['excludeuser'])); } /* Add the fields we're concerned with to our query. */ $this->addFields(array('rc_id', 'rc_timestamp', 'rc_namespace', 'rc_title', 'rc_cur_id', 'rc_type', 'rc_deleted')); $showRedirects = false; /* Determine what properties we need to display. */ if (!is_null($params['prop'])) { $prop = array_flip($params['prop']); /* Set up internal members based upon params. */ $this->initProperties($prop); if ($this->fld_patrolled && !$user->useRCPatrol() && !$user->useNPPatrol()) { $this->dieUsage('You need the patrol right to request the patrolled flag', 'permissiondenied'); } /* Add fields to our query if they are specified as a needed parameter. */ $this->addFieldsIf(array('rc_this_oldid', 'rc_last_oldid'), $this->fld_ids); $this->addFieldsIf('rc_comment', $this->fld_comment || $this->fld_parsedcomment); $this->addFieldsIf('rc_user', $this->fld_user || $this->fld_userid); $this->addFieldsIf('rc_user_text', $this->fld_user); $this->addFieldsIf(array('rc_minor', 'rc_type', 'rc_bot'), $this->fld_flags); $this->addFieldsIf(array('rc_old_len', 'rc_new_len'), $this->fld_sizes); $this->addFieldsIf('rc_patrolled', $this->fld_patrolled); $this->addFieldsIf(array('rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params'), $this->fld_loginfo); $showRedirects = $this->fld_redirect || isset($show['redirect']) || isset($show['!redirect']); } if ($this->fld_tags) { $this->addTables('tag_summary'); $this->addJoinConds(array('tag_summary' => array('LEFT JOIN', array('rc_id=ts_rc_id')))); $this->addFields('ts_tags'); } if ($this->fld_sha1) { $this->addTables('revision'); $this->addJoinConds(array('revision' => array('LEFT JOIN', array('rc_this_oldid=rev_id')))); $this->addFields(array('rev_sha1', 'rev_deleted')); } if ($params['toponly'] || $showRedirects) { $this->addTables('page'); $this->addJoinConds(array('page' => array('LEFT JOIN', array('rc_namespace=page_namespace', 'rc_title=page_title')))); $this->addFields('page_is_redirect'); if ($params['toponly']) { $this->addWhere('rc_this_oldid = page_latest'); } } if (!is_null($params['tag'])) { $this->addTables('change_tag'); $this->addJoinConds(array('change_tag' => array('INNER JOIN', array('rc_id=ct_rc_id')))); $this->addWhereFld('ct_tag', $params['tag']); } // Paranoia: avoid brute force searches (bug 17342) if (!is_null($params['user']) || !is_null($params['excludeuser'])) { if (!$user->isAllowed('deletedhistory')) { $bitmask = Revision::DELETED_USER; } elseif (!$user->isAllowedAny('suppressrevision', 'viewsuppressed')) { $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; } else { $bitmask = 0; } if ($bitmask) { $this->addWhere($this->getDB()->bitAnd('rc_deleted', $bitmask) . " != {$bitmask}"); } } if ($this->getRequest()->getCheck('namespace')) { // LogPage::DELETED_ACTION hides the affected page, too. if (!$user->isAllowed('deletedhistory')) { $bitmask = LogPage::DELETED_ACTION; } elseif (!$user->isAllowedAny('suppressrevision', 'viewsuppressed')) { $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED; } else { $bitmask = 0; } if ($bitmask) { $this->addWhere($this->getDB()->makeList(array('rc_type != ' . RC_LOG, $this->getDB()->bitAnd('rc_deleted', $bitmask) . " != {$bitmask}"), LIST_OR)); } } $this->token = $params['token']; $this->addOption('LIMIT', $params['limit'] + 1); $this->addOption('USE INDEX', $index); $count = 0; /* Perform the actual query. */ $res = $this->select(__METHOD__); $titles = array(); $result = $this->getResult(); /* Iterate through the rows, adding data extracted from them to our query result. */ foreach ($res as $row) { if (++$count > $params['limit']) { // We've reached the one extra which shows that there are // additional pages to be had. Stop here... $this->setContinueEnumParameter('continue', "{$row->rc_timestamp}|{$row->rc_id}"); break; } if (is_null($resultPageSet)) { /* Extract the data from a single row. */ $vals = $this->extractRowInfo($row); /* Add that row's data to our final output. */ $fit = $result->addValue(array('query', $this->getModuleName()), null, $vals); if (!$fit) { $this->setContinueEnumParameter('continue', "{$row->rc_timestamp}|{$row->rc_id}"); break; } } else { $titles[] = Title::makeTitle($row->rc_namespace, $row->rc_title); } } if (is_null($resultPageSet)) { /* Format the result */ $result->addIndexedTagName(array('query', $this->getModuleName()), 'rc'); } else { $resultPageSet->populateFromTitles($titles); } }
/** * Parsing text to RC_* constants * @since 1.24 * @param string|array $type * @throws MWException * @return int|array RC_TYPE */ public static function parseToRCType($type) { if (is_array($type)) { $retval = array(); foreach ($type as $t) { $retval[] = RecentChange::parseToRCType($t); } return $retval; } if (!array_key_exists($type, self::$changeTypes)) { throw new MWException("Unknown type '{$type}'"); } return self::$changeTypes[$type]; }
/** * Parsing text to RC_* constants * @since 1.24 * @param string|array $type * @throws MWException * @return int|array RC_TYPE */ public static function parseToRCType($type) { if (is_array($type)) { $retval = array(); foreach ($type as $t) { $retval[] = RecentChange::parseToRCType($t); } return $retval; } switch ($type) { case 'edit': return RC_EDIT; case 'new': return RC_NEW; case 'log': return RC_LOG; case 'external': return RC_EXTERNAL; default: throw new MWException("Unknown type '{$type}'"); } }
/** * @param ApiPageSet $resultPageSet * @return void */ private function run($resultPageSet = null) { $this->selectNamedDB('watchlist', DB_SLAVE, 'watchlist'); $params = $this->extractRequestParams(); $user = $this->getUser(); $wlowner = $this->getWatchlistUser($params); if (!is_null($params['prop']) && is_null($resultPageSet)) { $prop = array_flip($params['prop']); $this->fld_ids = isset($prop['ids']); $this->fld_title = isset($prop['title']); $this->fld_flags = isset($prop['flags']); $this->fld_user = isset($prop['user']); $this->fld_userid = isset($prop['userid']); $this->fld_comment = isset($prop['comment']); $this->fld_parsedcomment = isset($prop['parsedcomment']); $this->fld_timestamp = isset($prop['timestamp']); $this->fld_sizes = isset($prop['sizes']); $this->fld_patrol = isset($prop['patrol']); $this->fld_notificationtimestamp = isset($prop['notificationtimestamp']); $this->fld_loginfo = isset($prop['loginfo']); if ($this->fld_patrol) { if (!$user->useRCPatrol() && !$user->useNPPatrol()) { $this->dieUsage('patrol property is not available', 'patrol'); } } } $this->addFields(['rc_id', 'rc_namespace', 'rc_title', 'rc_timestamp', 'rc_type', 'rc_deleted']); if (is_null($resultPageSet)) { $this->addFields(['rc_cur_id', 'rc_this_oldid', 'rc_last_oldid']); $this->addFieldsIf(['rc_type', 'rc_minor', 'rc_bot'], $this->fld_flags); $this->addFieldsIf('rc_user', $this->fld_user || $this->fld_userid); $this->addFieldsIf('rc_user_text', $this->fld_user); $this->addFieldsIf('rc_comment', $this->fld_comment || $this->fld_parsedcomment); $this->addFieldsIf(['rc_patrolled', 'rc_log_type'], $this->fld_patrol); $this->addFieldsIf(['rc_old_len', 'rc_new_len'], $this->fld_sizes); $this->addFieldsIf('wl_notificationtimestamp', $this->fld_notificationtimestamp); $this->addFieldsIf(['rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params'], $this->fld_loginfo); } elseif ($params['allrev']) { $this->addFields('rc_this_oldid'); } else { $this->addFields('rc_cur_id'); } $this->addTables(['recentchanges', 'watchlist']); $userId = $wlowner->getId(); $this->addJoinConds(['watchlist' => ['INNER JOIN', ['wl_user' => $userId, 'wl_namespace=rc_namespace', 'wl_title=rc_title']]]); $db = $this->getDB(); $this->addTimestampWhereRange('rc_timestamp', $params['dir'], $params['start'], $params['end']); // Include in ORDER BY for uniqueness $this->addWhereRange('rc_id', $params['dir'], null, null); if (!is_null($params['continue'])) { $cont = explode('|', $params['continue']); $this->dieContinueUsageIf(count($cont) != 2); $op = $params['dir'] === 'newer' ? '>' : '<'; $continueTimestamp = $db->addQuotes($db->timestamp($cont[0])); $continueId = (int) $cont[1]; $this->dieContinueUsageIf($continueId != $cont[1]); $this->addWhere("rc_timestamp {$op} {$continueTimestamp} OR " . "(rc_timestamp = {$continueTimestamp} AND " . "rc_id {$op}= {$continueId})"); } $this->addWhereFld('wl_namespace', $params['namespace']); if (!$params['allrev']) { $this->addTables('page'); $this->addJoinConds(['page' => ['LEFT JOIN', 'rc_cur_id=page_id']]); $this->addWhere('rc_this_oldid=page_latest OR rc_type=' . RC_LOG); } if (!is_null($params['show'])) { $show = array_flip($params['show']); /* Check for conflicting parameters. */ if (isset($show['minor']) && isset($show['!minor']) || isset($show['bot']) && isset($show['!bot']) || isset($show['anon']) && isset($show['!anon']) || isset($show['patrolled']) && isset($show['!patrolled']) || isset($show['unread']) && isset($show['!unread'])) { $this->dieUsageMsg('show'); } // Check permissions. if (isset($show['patrolled']) || isset($show['!patrolled'])) { if (!$user->useRCPatrol() && !$user->useNPPatrol()) { $this->dieUsage('You need the patrol right to request the patrolled flag', 'permissiondenied'); } } /* Add additional conditions to query depending upon parameters. */ $this->addWhereIf('rc_minor = 0', isset($show['!minor'])); $this->addWhereIf('rc_minor != 0', isset($show['minor'])); $this->addWhereIf('rc_bot = 0', isset($show['!bot'])); $this->addWhereIf('rc_bot != 0', isset($show['bot'])); $this->addWhereIf('rc_user = 0', isset($show['anon'])); $this->addWhereIf('rc_user != 0', isset($show['!anon'])); $this->addWhereIf('rc_patrolled = 0', isset($show['!patrolled'])); $this->addWhereIf('rc_patrolled != 0', isset($show['patrolled'])); $this->addWhereIf('rc_timestamp >= wl_notificationtimestamp', isset($show['unread'])); $this->addWhereIf('wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp', isset($show['!unread'])); } if (!is_null($params['type'])) { try { $this->addWhereFld('rc_type', RecentChange::parseToRCType($params['type'])); } catch (Exception $e) { ApiBase::dieDebug(__METHOD__, $e->getMessage()); } } if (!is_null($params['user']) && !is_null($params['excludeuser'])) { $this->dieUsage('user and excludeuser cannot be used together', 'user-excludeuser'); } if (!is_null($params['user'])) { $this->addWhereFld('rc_user_text', $params['user']); } if (!is_null($params['excludeuser'])) { $this->addWhere('rc_user_text != ' . $db->addQuotes($params['excludeuser'])); } // This is an index optimization for mysql, as done in the Special:Watchlist page $this->addWhereIf("rc_timestamp > ''", !isset($params['start']) && !isset($params['end']) && $db->getType() == 'mysql'); // Paranoia: avoid brute force searches (bug 17342) if (!is_null($params['user']) || !is_null($params['excludeuser'])) { if (!$user->isAllowed('deletedhistory')) { $bitmask = Revision::DELETED_USER; } elseif (!$user->isAllowedAny('suppressrevision', 'viewsuppressed')) { $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; } else { $bitmask = 0; } if ($bitmask) { $this->addWhere($this->getDB()->bitAnd('rc_deleted', $bitmask) . " != {$bitmask}"); } } // LogPage::DELETED_ACTION hides the affected page, too. So hide those // entirely from the watchlist, or someone could guess the title. if (!$user->isAllowed('deletedhistory')) { $bitmask = LogPage::DELETED_ACTION; } elseif (!$user->isAllowedAny('suppressrevision', 'viewsuppressed')) { $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED; } else { $bitmask = 0; } if ($bitmask) { $this->addWhere($this->getDB()->makeList(['rc_type != ' . RC_LOG, $this->getDB()->bitAnd('rc_deleted', $bitmask) . " != {$bitmask}"], LIST_OR)); } $this->addOption('LIMIT', $params['limit'] + 1); $ids = []; $count = 0; $res = $this->select(__METHOD__); foreach ($res as $row) { if (++$count > $params['limit']) { // We've reached the one extra which shows that there are // additional pages to be had. Stop here... $this->setContinueEnumParameter('continue', "{$row->rc_timestamp}|{$row->rc_id}"); break; } if (is_null($resultPageSet)) { $vals = $this->extractRowInfo($row); $fit = $this->getResult()->addValue(['query', $this->getModuleName()], null, $vals); if (!$fit) { $this->setContinueEnumParameter('continue', "{$row->rc_timestamp}|{$row->rc_id}"); break; } } else { if ($params['allrev']) { $ids[] = intval($row->rc_this_oldid); } else { $ids[] = intval($row->rc_cur_id); } } } if (is_null($resultPageSet)) { $this->getResult()->addIndexedTagName(['query', $this->getModuleName()], 'item'); } elseif ($params['allrev']) { $resultPageSet->populateFromRevisionIDs($ids); } else { $resultPageSet->populateFromPageIDs($ids); } }
/** * @dataProvider provideRCTypes * @covers RecentChange::parseToRCType */ public function testParseToRCType($rcType, $type) { $this->assertEquals($rcType, RecentChange::parseToRCType($type)); }
/** * @param ApiPageSet $resultPageSet * @return void */ private function run($resultPageSet = null) { $this->selectNamedDB('watchlist', DB_REPLICA, 'watchlist'); $params = $this->extractRequestParams(); $user = $this->getUser(); $wlowner = $this->getWatchlistUser($params); if (!is_null($params['prop']) && is_null($resultPageSet)) { $prop = array_flip($params['prop']); $this->fld_ids = isset($prop['ids']); $this->fld_title = isset($prop['title']); $this->fld_flags = isset($prop['flags']); $this->fld_user = isset($prop['user']); $this->fld_userid = isset($prop['userid']); $this->fld_comment = isset($prop['comment']); $this->fld_parsedcomment = isset($prop['parsedcomment']); $this->fld_timestamp = isset($prop['timestamp']); $this->fld_sizes = isset($prop['sizes']); $this->fld_patrol = isset($prop['patrol']); $this->fld_notificationtimestamp = isset($prop['notificationtimestamp']); $this->fld_loginfo = isset($prop['loginfo']); if ($this->fld_patrol) { if (!$user->useRCPatrol() && !$user->useNPPatrol()) { $this->dieUsage('patrol property is not available', 'patrol'); } } } $options = ['dir' => $params['dir'] === 'older' ? WatchedItemQueryService::DIR_OLDER : WatchedItemQueryService::DIR_NEWER]; if (is_null($resultPageSet)) { $options['includeFields'] = $this->getFieldsToInclude(); } else { $options['usedInGenerator'] = true; } if ($params['start']) { $options['start'] = $params['start']; } if ($params['end']) { $options['end'] = $params['end']; } if (!is_null($params['continue'])) { $cont = explode('|', $params['continue']); $this->dieContinueUsageIf(count($cont) != 2); $continueTimestamp = $cont[0]; $continueId = (int) $cont[1]; $this->dieContinueUsageIf($continueId != $cont[1]); $options['startFrom'] = [$continueTimestamp, $continueId]; } if ($wlowner !== $user) { $options['watchlistOwner'] = $wlowner; $options['watchlistOwnerToken'] = $params['token']; } if (!is_null($params['namespace'])) { $options['namespaceIds'] = $params['namespace']; } if ($params['allrev']) { $options['allRevisions'] = true; } if (!is_null($params['show'])) { $show = array_flip($params['show']); /* Check for conflicting parameters. */ if ($this->showParamsConflicting($show)) { $this->dieUsageMsg('show'); } // Check permissions. if (isset($show[WatchedItemQueryService::FILTER_PATROLLED]) || isset($show[WatchedItemQueryService::FILTER_NOT_PATROLLED])) { if (!$user->useRCPatrol() && !$user->useNPPatrol()) { $this->dieUsage('You need the patrol right to request the patrolled flag', 'permissiondenied'); } } $options['filters'] = array_keys($show); } if (!is_null($params['type'])) { try { $options['rcTypes'] = RecentChange::parseToRCType($params['type']); } catch (Exception $e) { ApiBase::dieDebug(__METHOD__, $e->getMessage()); } } if (!is_null($params['user']) && !is_null($params['excludeuser'])) { $this->dieUsage('user and excludeuser cannot be used together', 'user-excludeuser'); } if (!is_null($params['user'])) { $options['onlyByUser'] = $params['user']; } if (!is_null($params['excludeuser'])) { $options['notByUser'] = $params['excludeuser']; } $options['limit'] = $params['limit'] + 1; $ids = []; $count = 0; $watchedItemQuery = MediaWikiServices::getInstance()->getWatchedItemQueryService(); $items = $watchedItemQuery->getWatchedItemsWithRecentChangeInfo($wlowner, $options); foreach ($items as list($watchedItem, $recentChangeInfo)) { /** @var WatchedItem $watchedItem */ if (++$count > $params['limit']) { // We've reached the one extra which shows that there are // additional pages to be had. Stop here... $this->setContinueEnumParameter('continue', $recentChangeInfo['rc_timestamp'] . '|' . $recentChangeInfo['rc_id']); break; } if (is_null($resultPageSet)) { $vals = $this->extractOutputData($watchedItem, $recentChangeInfo); $fit = $this->getResult()->addValue(['query', $this->getModuleName()], null, $vals); if (!$fit) { $this->setContinueEnumParameter('continue', $recentChangeInfo['rc_timestamp'] . '|' . $recentChangeInfo['rc_id']); break; } } else { if ($params['allrev']) { $ids[] = intval($recentChangeInfo['rc_this_oldid']); } else { $ids[] = intval($recentChangeInfo['rc_cur_id']); } } } if (is_null($resultPageSet)) { $this->getResult()->addIndexedTagName(['query', $this->getModuleName()], 'item'); } elseif ($params['allrev']) { $resultPageSet->populateFromRevisionIDs($ids); } else { $resultPageSet->populateFromPageIDs($ids); } }