Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
74.39% |
61 / 82 |
|
50.00% |
2 / 4 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryImageSuggestionData | |
74.39% |
61 / 82 |
|
50.00% |
2 / 4 |
25.06 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
72.41% |
42 / 58 |
|
0.00% |
0 / 1 |
16.55 | |||
getAllowedParams | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
1 | |||
hasErrorCode | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\Api; |
4 | |
5 | use ApiBase; |
6 | use ApiQuery; |
7 | use ApiQueryBase; |
8 | use ApiResult; |
9 | use GrowthExperiments\NewcomerTasks\AddImage\ImageRecommendation; |
10 | use GrowthExperiments\NewcomerTasks\AddImage\ImageRecommendationProvider; |
11 | use GrowthExperiments\NewcomerTasks\ConfigurationLoader\ConfigurationLoader; |
12 | use GrowthExperiments\NewcomerTasks\TaskType\ImageRecommendationBaseTaskType; |
13 | use GrowthExperiments\NewcomerTasks\TaskType\ImageRecommendationTaskTypeHandler; |
14 | use GrowthExperiments\NewcomerTasks\TaskType\SectionImageRecommendationTaskTypeHandler; |
15 | use IApiMessage; |
16 | use MediaWiki\Config\Config; |
17 | use MediaWiki\Logger\LoggerFactory; |
18 | use MediaWiki\Title\Title; |
19 | use MediaWiki\Title\TitleFactory; |
20 | use Psr\Log\LoggerAwareTrait; |
21 | use StatusValue; |
22 | use Wikimedia\Assert\Assert; |
23 | use 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 | */ |
32 | class 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 | } |