Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryLinkRecommendations
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 8
272
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
72
 incrementCounter
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getTaskUrl
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 tryLoadingMoreLinkRecommendations
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 isInternal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace GrowthExperiments\Api;
4
5use GrowthExperiments\NewcomerTasks\AddLink\LinkRecommendation;
6use GrowthExperiments\NewcomerTasks\AddLink\LinkRecommendationStore;
7use GrowthExperiments\NewcomerTasks\AddLink\LinkRecommendationUpdater;
8use GrowthExperiments\NewcomerTasks\TaskType\LinkRecommendationTaskTypeHandler;
9use MediaWiki\Api\ApiBase;
10use MediaWiki\Api\ApiQuery;
11use MediaWiki\Api\ApiQueryBase;
12use MediaWiki\Config\Config;
13use MediaWiki\SpecialPage\SpecialPage;
14use MediaWiki\Title\Title;
15use MediaWiki\Title\TitleFactory;
16use MediaWiki\WikiMap\WikiMap;
17use Wikimedia\ParamValidator\ParamValidator;
18use Wikimedia\Rdbms\IDBAccessObject;
19use Wikimedia\Stats\StatsFactory;
20
21/**
22 * API module for retrieving link recommendations for a specific page.
23 */
24class ApiQueryLinkRecommendations extends ApiQueryBase {
25
26    private LinkRecommendationStore $linkRecommendationStore;
27    private LinkRecommendationUpdater $linkRecommendationUpdater;
28    private TitleFactory $titleFactory;
29    private StatsFactory $statsFactory;
30    private Config $wikiConfig;
31
32    /**
33     * Constructor for ApiQueryLinkRecommendations
34     *
35     * @param ApiQuery $query The API query object
36     * @param string $moduleName The name of this module
37     * @param LinkRecommendationStore $linkRecommendationStore The store for link recommendations
38     * @param LinkRecommendationUpdater $linkRecommendationUpdater
39     * @param TitleFactory $titleFactory Factory for creating Title objects
40     * @param StatsFactory $statsFactory
41     * @param Config $wikiConfig Configuration object
42     */
43    public function __construct(
44        ApiQuery $query,
45        string $moduleName,
46        LinkRecommendationStore $linkRecommendationStore,
47        LinkRecommendationUpdater $linkRecommendationUpdater,
48        TitleFactory $titleFactory,
49        StatsFactory $statsFactory,
50        Config $wikiConfig
51    ) {
52        parent::__construct( $query, $moduleName, 'lr' );
53        $this->linkRecommendationStore = $linkRecommendationStore;
54        $this->linkRecommendationUpdater = $linkRecommendationUpdater;
55        $this->titleFactory = $titleFactory;
56        $this->statsFactory = $statsFactory;
57        $this->wikiConfig = $wikiConfig;
58    }
59
60    /**
61     * Main execution function for the API module.
62     * Retrieves and returns link recommendations for a given page ID.
63     */
64    public function execute() {
65        if ( !$this->wikiConfig->get( 'GESurfacingStructuredTasksEnabled' ) ) {
66            return;
67        }
68
69        $params = $this->extractRequestParams();
70        /**
71         * @var int $pageId
72         */
73        $pageId = $params[ 'pageid' ];
74
75        $user = $this->getUser();
76        if ( !$user->isNamed() ) {
77            $this->dieWithError( 'apierror-mustbeloggedin-generic' );
78        }
79
80        $title = $this->titleFactory->newFromID( $pageId );
81        if ( !$title ) {
82            $this->dieWithError( [ 'apierror-invalidtitle', $pageId ] );
83        }
84
85        $linkRecommendation = $this->linkRecommendationStore->getByPageId( $pageId );
86        if ( !$linkRecommendation ) {
87            $this->incrementCounter( 'no_preexisting_recommendation_found' );
88
89            // This is not yet production ready, see T382251
90            if ( $this->wikiConfig->get( 'GESurfacingStructuredTasksReadModeUpdateEnabled' ) ) {
91                $linkRecommendation = $this->tryLoadingMoreLinkRecommendations( $title );
92            }
93        } else {
94            $this->incrementCounter( 'preexisting_recommendations_found' );
95        }
96
97        $result = $this->getResult();
98        $path = [ 'query', $this->getModuleName() ];
99        $recommendations = [];
100
101        if ( !$linkRecommendation ) {
102            $result->addValue( $path, 'recommendations', $recommendations );
103            return;
104        }
105
106        $links = $linkRecommendation->getLinks();
107        foreach ( $links as $link ) {
108            $recommendations[] = [
109                'context_before' => $link->getContextBefore(),
110                'context_after' => $link->getContextAfter(),
111                'link_text' => $link->getText(),
112                'link_target' => $link->getLinkTarget(),
113                'link_index' => $link->getLinkIndex(),
114                'score' => $link->getScore(),
115                'wikitext_offset' => $link->getWikitextOffset(),
116            ];
117        }
118
119        $result->addValue( $path, 'recommendations', $recommendations );
120        $result->addValue( $path, 'taskURL', $this->getTaskUrl( $pageId ) );
121    }
122
123    private function incrementCounter( string $labelForEventToBeCounted ): void {
124        $wiki = WikiMap::getCurrentWikiId();
125        $this->statsFactory->withComponent( 'GrowthExperiments' )
126            ->getCounter( 'surfacing_link_recommendation_api_total' )
127            ->setLabel( 'wiki', $wiki )
128            ->setLabel( 'event', $labelForEventToBeCounted )
129            ->increment();
130    }
131
132    /**
133     * Generates a task URL for starting the Add Link session in VisualEditor.
134     *
135     * @param int $pageId The ID of the page for which to generate the task URL
136     * @return string The generated task URL
137     */
138    private function getTaskUrl( $pageId ) {
139        return SpecialPage::getTitleFor( 'Homepage', 'newcomertask/' . $pageId )->getLocalURL( [
140            'gesuggestededit' => 1,
141            'getasktype' => LinkRecommendationTaskTypeHandler::TASK_TYPE_ID,
142        ] );
143    }
144
145    private function tryLoadingMoreLinkRecommendations( Title $title ): ?LinkRecommendation {
146        $processingCandidateStartTimeSeconds = microtime( true );
147        $updateStatus = $this->linkRecommendationUpdater->processCandidate( $title, false );
148        if ( $updateStatus->isOK() ) {
149            $this->incrementCounter( 'new_recommendations_added' );
150            $linkRecommendation = $this->linkRecommendationStore->getByPageId(
151                $title->getArticleID(),
152                IDBAccessObject::READ_LATEST
153            );
154        } else {
155            $linkRecommendation = null;
156            $this->incrementCounter( 'no_recommendations_added' );
157            $this->addMessagesFromStatus( $updateStatus );
158        }
159        $this->statsFactory->withComponent( 'GrowthExperiments' )
160            ->getTiming( 'surfacing_link_recommendation_api_processing_candidate_seconds' )
161            ->setLabel( 'wiki', WikiMap::getCurrentWikiId() )
162            ->observe( microtime( true ) - $processingCandidateStartTimeSeconds );
163        return $linkRecommendation;
164    }
165
166    /**
167     * Defines the allowed parameters for this API module.
168     *
169     * @return array An array of allowed parameters and their properties
170     */
171    protected function getAllowedParams() {
172        return [
173            'pageid' => [
174                ParamValidator::PARAM_TYPE => 'integer',
175                ParamValidator::PARAM_REQUIRED => true,
176                ApiBase::PARAM_HELP_MSG => 'apihelp-query+linkrecommendations-param-pageid',
177            ],
178        ];
179    }
180
181    /**
182     * This API module is for internal use only.
183     *
184     * @return bool Always returns true
185     */
186    public function isInternal() {
187        return true;
188    }
189
190    /**
191     * Provides example queries for this API module.
192     *
193     * @return array An array of example queries and their descriptions
194     */
195    public function getExamplesMessages() {
196        return [
197            'action=query&list=linkrecommendations&lrpageid=123'
198                => 'apihelp-query+linkrecommendations-example-1',
199            'action=query&list=linkrecommendations&lrpageid=456&lrlimit=5'
200                => 'apihelp-query+linkrecommendations-example-2',
201        ];
202    }
203}