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