Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
50.00% covered (danger)
50.00%
32 / 64
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
MvpImageRecommendationApiHandler
50.00% covered (danger)
50.00%
32 / 64
60.00% covered (warning)
60.00%
3 / 5
34.12
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getApiRequest
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
12
 getSuggestionDataFromApiResponse
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
5.00
 getConfidence
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 sortSuggestions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace GrowthExperiments\NewcomerTasks\AddImage;
4
5use GrowthExperiments\NewcomerTasks\TaskType\ImageRecommendationTaskTypeHandler;
6use GrowthExperiments\NewcomerTasks\TaskType\TaskType;
7use MediaWiki\Context\RequestContext;
8use MediaWiki\Http\HttpRequestFactory;
9use MediaWiki\Title\Title;
10use StatusValue;
11
12/**
13 * Handler for MVP image suggestion API.
14 * Documentation: https://image-suggestion-api.wmcloud.org/?doc#/Image%20Suggestions
15 * This API should not be further used in production.
16 * Configuration of constructor parameters:
17 * - $url: GEImageRecommendationServiceUrl
18 * - $proxyUrl: GEImageRecommendationServiceHttpProxy
19 * - $useTitles: GEImageRecommendationServiceUseTitles
20 */
21class MvpImageRecommendationApiHandler implements ImageRecommendationApiHandler {
22
23    /** @var HttpRequestFactory */
24    private $httpRequestFactory;
25
26    /** @var string */
27    private $url;
28
29    /** @var string|null */
30    private $proxyUrl;
31
32    /** @var string */
33    private $wikiProject;
34
35    /** @var string */
36    private $wikiLanguage;
37
38    /** @var int|null */
39    private $requestTimeout;
40
41    /** @var bool */
42    private $useTitles;
43
44    private const CONFIDENCE_RATING_TO_NUMBER = [
45        'high' => 3,
46        'medium' => 2,
47        'low' => 1
48    ];
49
50    /**
51     * @param HttpRequestFactory $httpRequestFactory
52     * @param string $url Image recommendation service root URL
53     * @param string $wikiProject Wiki project (e.g. 'wikipedia')
54     * @param string $wikiLanguage Wiki language code
55     * @param string|null $proxyUrl HTTP proxy to use for $url
56     * @param int|null $requestTimeout Service request timeout in seconds
57     * @param bool $useTitles Use titles (the /:wiki/:lang/pages/:title API endpoint)
58     *   instead of IDs (the /:wiki/:lang/pages endpoint)?
59     */
60    public function __construct(
61        HttpRequestFactory $httpRequestFactory,
62        string $url,
63        string $wikiProject,
64        string $wikiLanguage,
65        ?string $proxyUrl,
66        ?int $requestTimeout,
67        bool $useTitles = false
68    ) {
69        $this->httpRequestFactory = $httpRequestFactory;
70        $this->url = $url;
71        $this->proxyUrl = $proxyUrl;
72        $this->wikiProject = $wikiProject;
73        $this->wikiLanguage = $wikiLanguage;
74        $this->requestTimeout = $requestTimeout;
75        $this->useTitles = $useTitles;
76    }
77
78    /** @inheritDoc */
79    public function getApiRequest( Title $title, TaskType $taskType ) {
80        if ( !$this->url ) {
81            return StatusValue::newFatal( 'rawmessage',
82                'Image Suggestions API URL is not configured' );
83        }
84
85        $pathArgs = [
86            'image-suggestions',
87            'v0',
88            $this->wikiProject,
89            $this->wikiLanguage,
90            'pages',
91        ];
92        $queryArgs = [
93            'source' => 'ima',
94        ];
95        if ( $this->useTitles ) {
96            $pathArgs[] = $title->getPrefixedDBkey();
97        } else {
98            $queryArgs['id'] = $title->getArticleID();
99        }
100        $request = $this->httpRequestFactory->create(
101            wfAppendQuery(
102                $this->url . '/' . implode( '/', array_map( 'rawurlencode', $pathArgs ) ),
103                $queryArgs
104            ),
105            [
106                'method' => 'GET',
107                'proxy' => $this->proxyUrl,
108                'originalRequest' => RequestContext::getMain()->getRequest(),
109                'timeout' => $this->requestTimeout,
110            ],
111            __METHOD__
112        );
113        $request->setHeader( 'Accept', 'application/json' );
114        return $request;
115    }
116
117    /** @inheritDoc */
118    public function getSuggestionDataFromApiResponse( array $apiResponse, TaskType $taskType ): array {
119        if ( $taskType->getId() !== ImageRecommendationTaskTypeHandler::TASK_TYPE_ID ) {
120            // The MVP API does not provide section-level recommendations.
121            return [];
122        }
123
124        if ( !$apiResponse['pages'] || !$apiResponse['pages'][0]['suggestions'] ) {
125            return [];
126        }
127        $imageData = [];
128        $sortedSuggestions = $this->sortSuggestions( $apiResponse['pages'][0]['suggestions'] );
129        foreach ( $sortedSuggestions as $suggestion ) {
130            $filename = $suggestion['filename'] ?? null;
131            $source = $suggestion['source']['details']['from'] ?? null;
132            $projects = $suggestion['source']['details']['found_on'] ?? null;
133            $datasetId = $suggestion['source']['details']['dataset_id'] ?? null;
134            $imageData[] = new ImageRecommendationData(
135                $filename,
136                $source,
137                $projects,
138                $datasetId
139            );
140        }
141        return $imageData;
142    }
143
144    /**
145     * Get numeric value of the suggestion's confidence rating
146     *
147     * @param array $suggestion
148     * @return int
149     */
150    private function getConfidence( array $suggestion ): int {
151        if ( array_key_exists( 'confidence_rating', $suggestion ) ) {
152            return self::CONFIDENCE_RATING_TO_NUMBER[$suggestion['confidence_rating']] ?? 0;
153        }
154        return 0;
155    }
156
157    /**
158     * Sort the suggestions in decreasing order based on confidence rating
159     *
160     * @param array $suggestions
161     * @return array
162     */
163    private function sortSuggestions( array $suggestions ): array {
164        $compare = function ( array $a, array $b ) {
165            return $this->getConfidence( $a ) < $this->getConfidence( $b ) ? 1 : -1;
166        };
167        usort( $suggestions, $compare );
168        return $suggestions;
169    }
170}