From a514341e365cecbc6b7e75dbc31242583d026fb1 Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Wed, 14 Sep 2016 14:22:35 -0400 Subject: [PATCH] API: Add hooks for ApiQueryBase's query and row-processing This will allow extensions to inject into the query to implement "xxshow" parameters, and to add additional properties to the output data for "xxprop" parameters. Also, call these new hooks from ApiQueryRevisions, ApiQueryAllRevisions, ApiQueryUserContributions, and ApiQueryRecentChanges since I701e8e19 is going to be wanting them. Bug: T143614 Bug: T143616 Change-Id: Id6b42c7f2eb53a6f659d0d61383287f41d96ca00 --- RELEASE-NOTES-1.28 | 9 ++++++ docs/hooks.txt | 35 ++++++++++++++++++++++ includes/api/ApiQueryAllRevisions.php | 11 +++---- includes/api/ApiQueryBase.php | 32 +++++++++++++++++++- includes/api/ApiQueryGeneratorBase.php | 9 ++++++ includes/api/ApiQueryRecentChanges.php | 6 ++-- includes/api/ApiQueryRevisions.php | 6 ++-- includes/api/ApiQueryUserContributions.php | 6 ++-- 8 files changed, 102 insertions(+), 12 deletions(-) diff --git a/RELEASE-NOTES-1.28 b/RELEASE-NOTES-1.28 index 8c479e627b..06aab84301 100644 --- a/RELEASE-NOTES-1.28 +++ b/RELEASE-NOTES-1.28 @@ -61,6 +61,9 @@ production. on the wiki farm with a different domain, MediaWiki will instead alter the redirect URL to include a ?cpPosTime parameter that triggers the database synchronization when the URL is followed by the client. The same-domain case uses a new cpPosTime cookie. +* Added new hooks, 'ApiQueryBaseBeforeQuery', 'ApiQueryBaseAfterQuery', and + 'ApiQueryBaseProcessRow', to make it easier for extensions to add 'prop' and + 'show' parameters to existing API query modules. === External library changes in 1.28 === @@ -151,6 +154,12 @@ production. * ApiResult::setParsedLimit() was removed (deprecated since 1.25) * ApiResult::setRawMode() was removed (deprecated since 1.25) * ApiResult::size() was removed (deprecated since 1.25) +* Added new hooks, 'ApiQueryBaseBeforeQuery', 'ApiQueryBaseAfterQuery', and + 'ApiQueryBaseProcessRow', to make it easier for extensions to add 'prop' and + 'show' parameters to existing API query modules. A query module can enable + these hooks by passing an array for $hookData to ApiQueryBase::select() and + by calling ApiQueryBase->processRow() before adding a row's data to the + result. === Languages updated in 1.28 === diff --git a/docs/hooks.txt b/docs/hooks.txt index 2dc1270a91..cccd13d4ce 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -464,6 +464,41 @@ $moduleManager: ApiModuleManager Module manager instance action=query submodule. Use this to extend core API modules. &$module: Module object +'ApiQueryBaseAfterQuery': Called for (some) API query modules after the +database query has returned. An API query module wanting to use this hook +should see the ApiQueryBase::select() and ApiQueryBase::processRow() +documentation. +$module: ApiQueryBase module in question +$result: ResultWrapper|bool returned from the IDatabase::select() +&$hookData: array that was passed to the 'ApiQueryBaseBeforeQuery' hook and + will be passed to the 'ApiQueryBaseProcessRow' hook, intended for inter-hook + communication. + +'ApiQueryBaseBeforeQuery': Called for (some) API query modules before a +database query is made. WARNING: It would be very easy to misuse this hook and +break the module! Any joins added *must* join on a unique key of the target +table unless you really know what you're doing. An API query module wanting to +use this hook should see the ApiQueryBase::select() and +ApiQueryBase::processRow() documentation. +$module: ApiQueryBase module in question +&$tables: array of tables to be queried +&$fields: array of columns to select +&$conds: array of WHERE conditionals for query +&$query_options: array of options for the database request +&$join_conds: join conditions for the tables +&$hookData: array that will be passed to the 'ApiQueryBaseAfterQuery' and + 'ApiQueryBaseProcessRow' hooks, intended for inter-hook communication. + +'ApiQueryBaseProcessRow': Called for (some) API query modules as each row of +the database result is processed. Return false to stop processing the result +set. An API query module wanting to use this hook should see the +ApiQueryBase::select() and ApiQueryBase::processRow() documentation. +$module: ApiQueryBase module in question +$row: stdClass Database result row +&$data: array to be included in the ApiResult. +&$hookData: array that was be passed to the 'ApiQueryBaseBeforeQuery' and + 'ApiQueryBaseAfterQuery' hooks, intended for inter-hook communication. + 'APIQueryGeneratorAfterExecute': After calling the executeGenerator() method of an action=query submodule. Use this to extend core API modules. &$module: Module object diff --git a/includes/api/ApiQueryAllRevisions.php b/includes/api/ApiQueryAllRevisions.php index d548c46ce0..b64b2c8401 100644 --- a/includes/api/ApiQueryAllRevisions.php +++ b/includes/api/ApiQueryAllRevisions.php @@ -166,7 +166,8 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { $orderby[] = "rev_id $sort"; $this->addOption( 'ORDER BY', $orderby ); - $res = $this->select( __METHOD__ ); + $hookData = []; + $res = $this->select( __METHOD__, [], $hookData ); $pageMap = []; // Maps rev_page to array index $count = 0; $nextIndex = 0; @@ -210,12 +211,12 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { ]; ApiResult::setIndexedTagName( $a['revisions'], 'rev' ); ApiQueryBase::addTitleInfo( $a, $title ); - $fit = $result->addValue( [ 'query', $this->getModuleName() ], $index, $a ); + $fit = $this->processRow( $row, $a['revisions'][0], $hookData ) && + $result->addValue( [ 'query', $this->getModuleName() ], $index, $a ); } else { $index = $pageMap[$row->rev_page]; - $fit = $result->addValue( - [ 'query', $this->getModuleName(), $index, 'revisions' ], - null, $rev ); + $fit = $this->processRow( $row, $rev, $hookData ) && + $result->addValue( [ 'query', $this->getModuleName(), $index, 'revisions' ], null, $rev ); } if ( !$fit ) { $this->setContinueEnumParameter( 'continue', "$row->rev_timestamp|$row->rev_id" ); diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index 36ad3a4be7..bba53755c1 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -347,9 +347,12 @@ abstract class ApiQueryBase extends ApiBase { * 'options' => ..., * 'join_conds' => ... * ] + * @param array|null &$hookData If set, the ApiQueryBaseBeforeQuery and + * ApiQueryBaseAfterQuery hooks will be called, and the + * ApiQueryBaseProcessRow hook will be expected. * @return ResultWrapper */ - protected function select( $method, $extraQuery = [] ) { + protected function select( $method, $extraQuery = [], array &$hookData = null ) { $tables = array_merge( $this->tables, @@ -372,11 +375,38 @@ abstract class ApiQueryBase extends ApiBase { isset( $extraQuery['join_conds'] ) ? (array)$extraQuery['join_conds'] : [] ); + if ( $hookData !== null ) { + Hooks::run( 'ApiQueryBaseBeforeQuery', + [ $this, &$tables, &$fields, &$where, &$options, &$join_conds, &$hookData ] + ); + } + $res = $this->getDB()->select( $tables, $fields, $where, $method, $options, $join_conds ); + if ( $hookData !== null ) { + Hooks::run( 'ApiQueryBaseAfterQuery', [ $this, $res, &$hookData ] ); + } + return $res; } + /** + * Call the ApiQueryBaseProcessRow hook + * + * Generally, a module that passed $hookData to self::select() will call + * this just before calling ApiResult::addValue(), and treat a false return + * here in the same way it treats a false return from addValue(). + * + * @since 1.28 + * @param object $row Database row + * @param array &$data Data to be added to the result + * @param array &$hookData Hook data from ApiQueryBase::select() + * @return bool Return false if row processing should end with continuation + */ + protected function processRow( $row, array &$data, array &$hookData ) { + return Hooks::run( 'ApiQueryBaseProcessRow', [ $this, $row, &$data, &$hookData ] ); + } + /** * @param string $query * @param string $protocol diff --git a/includes/api/ApiQueryGeneratorBase.php b/includes/api/ApiQueryGeneratorBase.php index 67fe0d61a7..f7b94c7c01 100644 --- a/includes/api/ApiQueryGeneratorBase.php +++ b/includes/api/ApiQueryGeneratorBase.php @@ -45,6 +45,15 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { $this->mGeneratorPageSet = $generatorPageSet; } + /** + * Indicate whether the module is in generator mode + * @since 1.28 + * @return bool + */ + public function isInGeneratorMode() { + return $this->mGeneratorPageSet !== null; + } + /** * Get the PageSet object to work on. * If this module is generator, the pageSet object is different from other module's diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index c4c8afbdbf..8b11dc2a47 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -361,9 +361,10 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $this->token = $params['token']; $this->addOption( 'LIMIT', $params['limit'] + 1 ); + $hookData = []; $count = 0; /* Perform the actual query. */ - $res = $this->select( __METHOD__ ); + $res = $this->select( __METHOD__, [], $hookData ); $revids = []; $titles = []; @@ -391,7 +392,8 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $vals = $this->extractRowInfo( $row ); /* Add that row's data to our final output. */ - $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals ); + $fit = $this->processRow( $row, $vals, $hookData ) && + $result->addValue( [ 'query', $this->getModuleName() ], null, $vals ); if ( !$fit ) { $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" ); break; diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index b816f43842..3259927a23 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -313,7 +313,8 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { $count = 0; $generated = []; - $res = $this->select( __METHOD__ ); + $hookData = []; + $res = $this->select( __METHOD__, [], $hookData ); foreach ( $res as $row ) { if ( ++$count > $this->limit ) { @@ -350,7 +351,8 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { } } - $fit = $this->addPageSubItem( $row->rev_page, $rev, 'rev' ); + $fit = $this->processRow( $row, $rev, $hookData ) && + $this->addPageSubItem( $row->rev_page, $rev, 'rev' ); if ( !$fit ) { if ( $enumRevMode ) { $this->setContinueEnumParameter( 'continue', diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index f92a916f6c..b85bec4899 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -111,8 +111,9 @@ class ApiQueryContributions extends ApiQueryBase { $this->prepareQuery(); + $hookData = []; // Do the actual query. - $res = $this->select( __METHOD__ ); + $res = $this->select( __METHOD__, [], $hookData ); if ( $this->fld_sizediff ) { $revIds = []; @@ -139,7 +140,8 @@ class ApiQueryContributions extends ApiQueryBase { } $vals = $this->extractRowInfo( $row ); - $fit = $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals ); + $fit = $this->processRow( $row, $vals, $hookData ) && + $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals ); if ( !$fit ) { $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) ); break; -- 2.20.1