Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.39% covered (warning)
74.39%
61 / 82
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryImageSuggestionData
74.39% covered (warning)
74.39%
61 / 82
50.00% covered (danger)
50.00%
2 / 4
25.06
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 execute
72.41% covered (warning)
72.41%
42 / 58
0.00% covered (danger)
0.00%
0 / 1
16.55
 getAllowedParams
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 hasErrorCode
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace GrowthExperiments\Api;
4
5use ApiBase;
6use ApiQuery;
7use ApiQueryBase;
8use ApiResult;
9use GrowthExperiments\NewcomerTasks\AddImage\ImageRecommendation;
10use GrowthExperiments\NewcomerTasks\AddImage\ImageRecommendationProvider;
11use GrowthExperiments\NewcomerTasks\ConfigurationLoader\ConfigurationLoader;
12use GrowthExperiments\NewcomerTasks\TaskType\ImageRecommendationBaseTaskType;
13use GrowthExperiments\NewcomerTasks\TaskType\ImageRecommendationTaskTypeHandler;
14use GrowthExperiments\NewcomerTasks\TaskType\SectionImageRecommendationTaskTypeHandler;
15use IApiMessage;
16use MediaWiki\Config\Config;
17use MediaWiki\Logger\LoggerFactory;
18use MediaWiki\Title\Title;
19use MediaWiki\Title\TitleFactory;
20use Psr\Log\LoggerAwareTrait;
21use StatusValue;
22use Wikimedia\Assert\Assert;
23use Wikimedia\ParamValidator\ParamValidator;
24
25/**
26 * Query module to support fetching image metadata from the Image Suggestion API.
27 *
28 * - Users must be logged-in
29 * - Rate limits apply
30 * - Image suggestion metadata is cached
31 */
32class ApiQueryImageSuggestionData extends ApiQueryBase {
33    use LoggerAwareTrait;
34
35    private ImageRecommendationProvider $imageRecommendationProvider;
36    private ConfigurationLoader $configurationLoader;
37    private Config $config;
38    private TitleFactory $titleFactory;
39
40    /**
41     * @param ApiQuery $mainModule
42     * @param string $moduleName
43     * @param ImageRecommendationProvider $imageRecommendationProvider
44     * @param ConfigurationLoader $configurationLoader
45     * @param Config $config
46     * @param TitleFactory $titleFactory
47     */
48    public function __construct(
49        ApiQuery $mainModule,
50        $moduleName,
51        ImageRecommendationProvider $imageRecommendationProvider,
52        ConfigurationLoader $configurationLoader,
53        Config $config,
54        TitleFactory $titleFactory
55    ) {
56        parent::__construct( $mainModule, $moduleName, 'gisd' );
57        $this->imageRecommendationProvider = $imageRecommendationProvider;
58        $this->configurationLoader = $configurationLoader;
59        $this->config = $config;
60        $this->titleFactory = $titleFactory;
61
62        $this->setLogger( LoggerFactory::getInstance( 'GrowthExperiments' ) );
63    }
64
65    /** @inheritDoc */
66    public function execute() {
67        $user = $this->getUser();
68        if ( !$user->isNamed() ) {
69            $this->dieWithError( [ 'apierror-mustbeloggedin-generic' ] );
70        }
71
72        if ( $user->pingLimiter( 'growthexperiments-apiqueryimagesuggestiondata' ) ) {
73            $this->dieWithError( 'apierror-ratelimited' );
74        }
75        $params = $this->extractRequestParams();
76        // This API is used by external clients for their own structured task workflows so
77        // include disabled task types.
78        $allTaskTypes = $this->configurationLoader->getTaskTypes()
79            + $this->configurationLoader->getDisabledTaskTypes();
80        $taskType = $allTaskTypes[$params['tasktype']] ?? null;
81
82        if ( $taskType === null ) {
83            $this->logger->warning(
84                'Task type {tasktype} was not found in {configpage}',
85                [
86                    'tasktype' => $params['tasktype'],
87                    'configpage' => $this->getConfig()->get( 'GENewcomerTasksConfigTitle' ),
88                ]
89            );
90            $this->dieWithError(
91                [ 'growthexperiments-homepage-imagesuggestiondata-not-in-config', $params['tasktype'] ],
92                'not-in-config'
93            );
94        }
95
96        Assert::parameterType( ImageRecommendationBaseTaskType::class, $taskType, '$taskType' );
97        '@phan-var ImageRecommendationBaseTaskType $taskType';
98
99        $continueTitle = null;
100        if ( $params['continue'] !== null ) {
101            $continue = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'string' ] );
102            $continueTitle = $this->titleFactory->makeTitleSafe( $continue[0], $continue[1] );
103            $this->dieContinueUsageIf( !$continueTitle );
104        }
105        $pageSet = $this->getPageSet();
106        // Allow non-existing pages in developer setup, to facilitate local development/testing.
107        $pages = $this->config->get( 'GEDeveloperSetup' )
108            ? $pageSet->getGoodAndMissingPages()
109            : $pageSet->getGoodPages();
110        foreach ( $pages as $pageApiId => $pageIdentity ) {
111            if ( $continueTitle && !$continueTitle->equals( $pageIdentity ) ) {
112                continue;
113            }
114            $title = Title::castFromPageIdentity( $pageIdentity );
115            if ( !$title ) {
116                continue;
117            }
118            $metadata = $this->imageRecommendationProvider->get(
119                $title,
120                $taskType
121            );
122            $fit = null;
123            if ( $metadata instanceof ImageRecommendation ) {
124                $fit = $this->addPageSubItem(
125                    $pageApiId,
126                    $metadata->toArray()
127                );
128            } elseif ( !$this->hasErrorCode( $metadata, 'growthexperiments-no-recommendation-found' ) ) {
129                // like ApiQueryBase::addPageSubItems but we want to use a different path
130                $errorArray = $this->getErrorFormatter()->arrayFromStatus( $metadata );
131                $path = [ 'query', 'pages', $pageApiId ];
132                ApiResult::setIndexedTagName( $errorArray, 'growthimagesuggestiondataerrors' );
133                $fit = $this->getResult()->addValue( $path, 'growthimagesuggestiondataerrors', $errorArray );
134            }
135            if ( $fit === false ) {
136                $this->setContinueEnumParameter(
137                    'continue',
138                    $title->getNamespace() . '|' . $title->getText()
139                );
140                break;
141            }
142        }
143    }
144
145    /** @inheritDoc */
146    public function getAllowedParams() {
147        return [
148            'tasktype' => [
149                ParamValidator::PARAM_TYPE => [
150                    // Do not filter out non-existing task-types: during API structure tests
151                    // none of the task types exist and an empty list would cause test failures.
152                    ImageRecommendationTaskTypeHandler::TASK_TYPE_ID,
153                    SectionImageRecommendationTaskTypeHandler::TASK_TYPE_ID,
154                ],
155                ParamValidator::PARAM_REQUIRED => false,
156                ParamValidator::PARAM_DEFAULT => ImageRecommendationTaskTypeHandler::TASK_TYPE_ID,
157            ],
158            'continue' => [
159                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
160            ],
161        ];
162    }
163
164    private function hasErrorCode( StatusValue $status, string $errorCode ): bool {
165        foreach ( $status->getErrors() as $error ) {
166            $message = $error['message'];
167            if ( $message instanceof IApiMessage && $message->getApiCode() === $errorCode ) {
168                return true;
169            }
170        }
171        return false;
172    }
173
174}