From: Brad Jorsch Date: Thu, 14 Nov 2013 21:07:52 +0000 (-0500) Subject: API: Add prop=contributors X-Git-Tag: 1.31.0-rc.0~17356 X-Git-Url: http://git.cyclocoop.org/%22%20.%20generer_url_ecrire%28%22suivi_revisions%22%29%20.%20%22?a=commitdiff_plain;h=e06ad6841901df582990718f885a5a655d8c7748;p=lhc%2Fweb%2Fwiklou.git API: Add prop=contributors Certain applications, such as the generation of PDFs, could use a list of all non-anonymous contributors to the page (as well as a count of anonymous contributors) without crawling the output of prop=revisions. This patch adds a prop module to retrieve this information. Including the IP addresses of anonymous contributors is not realistically possible without further schema changes, so that is not done here. Additionally, revisions with DELETED_USER will be skipped entirely. Change-Id: Iaff50dfb09016154901a5197aa14eb9f8febcbc5 --- diff --git a/RELEASE-NOTES-1.23 b/RELEASE-NOTES-1.23 index afa7efd111..33fb88b18d 100644 --- a/RELEASE-NOTES-1.23 +++ b/RELEASE-NOTES-1.23 @@ -110,6 +110,7 @@ production. * (bug 57874) action=feedcontributions no longer has one item more than limit. * All API modules now support an assert parameter. See the new features section for more details. +* Added prop=contributors to fetch the list of contributors to the page. === Languages updated in 1.23 === diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index e6ce693ca1..4b8fec2362 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -333,6 +333,7 @@ $wgAutoloadLocalClasses = array( 'ApiQueryCategoryInfo' => 'includes/api/ApiQueryCategoryInfo.php', 'ApiQueryCategoryMembers' => 'includes/api/ApiQueryCategoryMembers.php', 'ApiQueryContributions' => 'includes/api/ApiQueryUserContributions.php', + 'ApiQueryContributors' => 'includes/api/ApiQueryContributors.php', 'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php', 'ApiQueryDisabled' => 'includes/api/ApiQueryDisabled.php', 'ApiQueryDuplicateFiles' => 'includes/api/ApiQueryDuplicateFiles.php', diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index cec1ca86f9..c054bc1e90 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -44,6 +44,7 @@ class ApiQuery extends ApiBase { private static $QueryPropModules = array( 'categories' => 'ApiQueryCategories', 'categoryinfo' => 'ApiQueryCategoryInfo', + 'contributors' => 'ApiQueryContributors', 'duplicatefiles' => 'ApiQueryDuplicateFiles', 'extlinks' => 'ApiQueryExternalLinks', 'images' => 'ApiQueryImages', diff --git a/includes/api/ApiQueryContributors.php b/includes/api/ApiQueryContributors.php new file mode 100644 index 0000000000..6b896e3290 --- /dev/null +++ b/includes/api/ApiQueryContributors.php @@ -0,0 +1,287 @@ +getDB(); + $params = $this->extractRequestParams(); + $this->requireMaxOneParameter( $params, 'group', 'excludegroup', 'rights', 'excluderights' ); + + // Only operate on existing pages + $pages = array_keys( $this->getPageSet()->getGoodTitles() ); + + // Filter out already-processed pages + if ( $params['continue'] !== null ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != 2 ); + $cont_page = (int)$cont[0]; + $pages = array_filter( $pages, function ( $v ) use ( $cont_page ) { + return $v >= $cont_page; + } ); + } + if ( !count( $pages ) ) { + // Nothing to do + return; + } + + // Apply MAX_PAGES, leaving any over the limit for a continue. + sort( $pages ); + $continuePages = null; + if ( count( $pages ) > self::MAX_PAGES ) { + $continuePages = $pages[self::MAX_PAGES] . '|0'; + $pages = array_slice( $pages, 0, self::MAX_PAGES ); + } + + $result = $this->getResult(); + + // First, count anons + $this->addTables( 'revision' ); + $this->addFields( array( + 'page' => 'rev_page', + 'anons' => 'COUNT(DISTINCT rev_user_text)', + ) ); + $this->addWhereFld( 'rev_page', $pages ); + $this->addWhere( 'rev_user = 0' ); + $this->addWhere( $db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ); + $this->addOption( 'GROUP BY', 'rev_page' ); + $res = $this->select( __METHOD__ ); + foreach ( $res as $row ) { + $fit = $result->addValue( array( 'query', 'pages', $row->page ), + 'anoncontributors', $row->anons + ); + if ( !$fit ) { + // This not fitting isn't reasonable, so it probably means that + // some other module used up all the space. Just set a dummy + // continue and hope it works next time. + $this->setContinueEnumParameter( 'continue', + $params['continue'] !== null ? $params['continue'] : '0|0' + ); + return; + } + } + + // Next, add logged-in users + $this->resetQueryParams(); + $this->addTables( 'revision' ); + $this->addFields( array( + 'page' => 'rev_page', + 'user' => 'rev_user', + 'username' => 'MAX(rev_user_text)', // Non-MySQL databases don't like partial group-by + ) ); + $this->addWhereFld( 'rev_page', $pages ); + $this->addWhere( 'rev_user != 0' ); + $this->addWhere( $db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ); + $this->addOption( 'GROUP BY', 'rev_page, rev_user' ); + $this->addOption( 'LIMIT', $params['limit'] + 1 ); + + // Force a sort order to ensure that properties are grouped by page + // But only if pp_page is not constant in the WHERE clause. + if ( count( $pages ) > 1 ) { + $this->addOption( 'ORDER BY', 'rev_page, rev_user' ); + } else { + $this->addOption( 'ORDER BY', 'rev_user' ); + } + + $limitGroups = array(); + if ( $params['group'] ) { + $excludeGroups = false; + $limitGroups = $params['group']; + } elseif ( $params['excludegroup'] ) { + $excludeGroups = true; + $limitGroups = $params['excludegroup']; + } elseif ( $params['rights'] ) { + $excludeGroups = false; + foreach ( $params['rights'] as $r ) { + $limitGroups = array_merge( $limitGroups, User::getGroupsWithPermission( $r ) ); + } + + // If no group has the rights requested, no need to query + if ( !$limitGroups ) { + if ( $continuePages !== null ) { + // But we still need to continue for the next page's worth + // of anoncontributors + $this->setContinueEnumParameter( 'continue', $continuePages ); + } + return; + } + } elseif ( $params['excluderights'] ) { + $excludeGroups = true; + foreach ( $params['excluderights'] as $r ) { + $limitGroups = array_merge( $limitGroups, User::getGroupsWithPermission( $r ) ); + } + } + + if ( $limitGroups ) { + $limitGroups = array_unique( $limitGroups ); + $this->addTables( 'user_groups' ); + $this->addJoinConds( array( 'user_groups' => array( + $excludeGroups ? 'LEFT OUTER JOIN' : 'INNER JOIN', + array( 'ug_user=rev_user', 'ug_group' => $limitGroups ) + ) ) ); + $this->addWhereIf( 'ug_user IS NULL', $excludeGroups ); + } + + if ( $params['continue'] !== null ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != 2 ); + $cont_page = (int)$cont[0]; + $cont_user = (int)$cont[1]; + $this->addWhere( + "rev_page > $cont_page OR " . + "(rev_page = $cont_page AND " . + "rev_user >= $cont_user)" + ); + } + + $res = $this->select( __METHOD__ ); + $count = 0; + 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->page . '|' . $row->user ); + return; + } + + $fit = $this->addPageSubItem( $row->page, + array( 'userid' => $row->user, 'name' => $row->username ), + 'user' + ); + if ( !$fit ) { + $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->user ); + return; + } + } + + if ( $continuePages !== null ) { + $this->setContinueEnumParameter( 'continue', $continuePages ); + } + } + + public function getCacheMode( $params ) { + return 'public'; + } + + public function getAllowedParams() { + $userGroups = User::getAllGroups(); + $userRights = User::getAllRights(); + + return array( + 'group' => array( + ApiBase::PARAM_TYPE => $userGroups, + ApiBase::PARAM_ISMULTI => true, + ), + 'excludegroup' => array( + ApiBase::PARAM_TYPE => $userGroups, + ApiBase::PARAM_ISMULTI => true, + ), + 'rights' => array( + ApiBase::PARAM_TYPE => $userRights, + ApiBase::PARAM_ISMULTI => true, + ), + 'excluderights' => array( + ApiBase::PARAM_TYPE => $userRights, + ApiBase::PARAM_ISMULTI => true, + ), + 'limit' => array( + ApiBase::PARAM_DFLT => 10, + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MIN => 1, + ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, + ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 + ), + 'continue' => null, + ); + } + + public function getParamDescription() { + return array( + 'group' => array( + 'Limit users to given group name(s)', + 'Does not include implicit or auto-promoted groups like *, user, or autoconfirmed' + ), + 'excludegroup' => array( + 'Exclude users in given group name(s)', + 'Does not include implicit or auto-promoted groups like *, user, or autoconfirmed' + ), + 'rights' => array( + 'Limit users to those having given right(s)', + 'Does not include rights granted by implicit or auto-promoted groups ' . + 'like *, user, or autoconfirmed' + ), + 'excluderights' => array( + 'Limit users to those not having given right(s)', + 'Does not include rights granted by implicit or auto-promoted groups ' . + 'like *, user, or autoconfirmed' + ), + 'limit' => 'How many contributors to return', + 'continue' => 'When more results are available, use this to continue', + ); + } + + public function getPossibleErrors() { + return array_merge( parent::getPossibleErrors(), + $this->getRequireMaxOneParameterErrorMessages( + array( 'group', 'excludegroup', 'rights', 'excluderights' ) + ) + ); + } + + + public function getDescription() { + return 'Get the list of logged-in contributors and ' . + 'the count of anonymous contributors to a page'; + } + + public function getExamples() { + return array( + 'api.php?action=query&prop=contributors&titles=Main_Page', + ); + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Properties#contributors_.2F_pc'; + } +}