Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
72.19% |
109 / 151 |
|
71.43% |
10 / 14 |
CRAP | |
0.00% |
0 / 1 |
AddForm | |
72.19% |
109 / 151 |
|
71.43% |
10 / 14 |
38.45 | |
0.00% |
0 / 1 |
factory | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
1 | |||
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
5 | |||
getAllowedParams | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
1 | |||
isWriteMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isInternal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
needsToken | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
mustBePosted | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getExamplesMessages | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
2 | |||
getFormWithMaxId | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getBaseLexemeRevisionFromRequest | |
55.56% |
10 / 18 |
|
0.00% |
0 / 1 |
5.40 | |||
buildSaveFlags | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
saveNewLexemeRevision | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
2 | |||
fillApiResultFromStatus | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace Wikibase\Lexeme\MediaWiki\Api; |
6 | |
7 | use LogicException; |
8 | use MediaWiki\Api\ApiBase; |
9 | use MediaWiki\Api\ApiCreateTempUserTrait; |
10 | use MediaWiki\Api\ApiMain; |
11 | use MediaWiki\Api\ApiUsageException; |
12 | use MediaWiki\Message\Message; |
13 | use RuntimeException; |
14 | use Wikibase\DataModel\Entity\EntityDocument; |
15 | use Wikibase\DataModel\Entity\EntityIdParser; |
16 | use Wikibase\DataModel\Serializers\SerializerFactory; |
17 | use Wikibase\Lexeme\Domain\Model\Exceptions\ConflictException; |
18 | use Wikibase\Lexeme\Domain\Model\Form; |
19 | use Wikibase\Lexeme\Domain\Model\FormId; |
20 | use Wikibase\Lexeme\Domain\Model\Lexeme; |
21 | use Wikibase\Lexeme\MediaWiki\Api\Error\LexemeNotFound; |
22 | use Wikibase\Lexeme\Serialization\FormSerializer; |
23 | use Wikibase\Lexeme\WikibaseLexemeServices; |
24 | use Wikibase\Lib\FormatableSummary; |
25 | use Wikibase\Lib\Store\EntityRevision; |
26 | use Wikibase\Lib\Store\EntityRevisionLookup; |
27 | use Wikibase\Lib\Store\LookupConstants; |
28 | use Wikibase\Lib\Store\StorageException; |
29 | use Wikibase\Lib\Summary; |
30 | use Wikibase\Repo\Api\ApiErrorReporter; |
31 | use Wikibase\Repo\Api\ApiHelperFactory; |
32 | use Wikibase\Repo\Api\ResultBuilder; |
33 | use Wikibase\Repo\ChangeOp\ChangeOpException; |
34 | use Wikibase\Repo\EditEntity\EditEntityStatus; |
35 | use Wikibase\Repo\EditEntity\MediaWikiEditEntityFactory; |
36 | use Wikibase\Repo\Store\Store; |
37 | use Wikibase\Repo\SummaryFormatter; |
38 | use Wikimedia\ParamValidator\ParamValidator; |
39 | |
40 | /** |
41 | * @license GPL-2.0-or-later |
42 | */ |
43 | class AddForm extends ApiBase { |
44 | |
45 | use ApiCreateTempUserTrait; |
46 | |
47 | private const LATEST_REVISION = 0; |
48 | |
49 | private AddFormRequestParser $requestParser; |
50 | private ResultBuilder $resultBuilder; |
51 | private ApiErrorReporter $errorReporter; |
52 | private FormSerializer $formSerializer; |
53 | private MediaWikiEditEntityFactory $editEntityFactory; |
54 | private SummaryFormatter $summaryFormatter; |
55 | private EntityRevisionLookup $entityRevisionLookup; |
56 | |
57 | public static function factory( |
58 | ApiMain $mainModule, |
59 | string $moduleName, |
60 | ApiHelperFactory $apiHelperFactory, |
61 | SerializerFactory $baseDataModelSerializerFactory, |
62 | MediaWikiEditEntityFactory $editEntityFactory, |
63 | EntityIdParser $entityIdParser, |
64 | Store $store, |
65 | SummaryFormatter $summaryFormatter |
66 | ): self { |
67 | $formSerializer = new FormSerializer( |
68 | $baseDataModelSerializerFactory->newTermListSerializer(), |
69 | $baseDataModelSerializerFactory->newStatementListSerializer() |
70 | ); |
71 | |
72 | return new self( |
73 | $mainModule, |
74 | $moduleName, |
75 | new AddFormRequestParser( |
76 | $entityIdParser, |
77 | WikibaseLexemeServices::getEditFormChangeOpDeserializer() |
78 | ), |
79 | $formSerializer, |
80 | $store->getEntityRevisionLookup( Store::LOOKUP_CACHING_DISABLED ), |
81 | $editEntityFactory, |
82 | $summaryFormatter, |
83 | $apiHelperFactory |
84 | ); |
85 | } |
86 | |
87 | public function __construct( |
88 | ApiMain $mainModule, |
89 | string $moduleName, |
90 | AddFormRequestParser $requestParser, |
91 | FormSerializer $formSerializer, |
92 | EntityRevisionLookup $entityRevisionLookup, |
93 | MediaWikiEditEntityFactory $editEntityFactory, |
94 | SummaryFormatter $summaryFormatter, |
95 | ApiHelperFactory $apiHelperFactory |
96 | ) { |
97 | parent::__construct( $mainModule, $moduleName ); |
98 | |
99 | $this->resultBuilder = $apiHelperFactory->getResultBuilder( $this ); |
100 | $this->errorReporter = $apiHelperFactory->getErrorReporter( $this ); |
101 | $this->requestParser = $requestParser; |
102 | $this->formSerializer = $formSerializer; |
103 | $this->editEntityFactory = $editEntityFactory; |
104 | $this->entityRevisionLookup = $entityRevisionLookup; |
105 | $this->summaryFormatter = $summaryFormatter; |
106 | } |
107 | |
108 | /** |
109 | * @see ApiBase::execute() |
110 | * |
111 | * @throws ApiUsageException |
112 | */ |
113 | public function execute(): void { |
114 | /* |
115 | * { |
116 | "representations": [ |
117 | "en-gb": { |
118 | "value": "colour", |
119 | "language": "en-gb" |
120 | }, |
121 | "en-us": { |
122 | "value": "color", |
123 | "language": "en-us" |
124 | } |
125 | ], |
126 | "grammaticalFeatures": [ |
127 | "Q1", |
128 | "Q2" |
129 | ] |
130 | } |
131 | * |
132 | */ |
133 | |
134 | //FIXME: Representation text normalization |
135 | |
136 | //TODO: Documenting response structure. Is it possible? |
137 | |
138 | $params = $this->extractRequestParams(); |
139 | $request = $this->requestParser->parse( $params ); |
140 | |
141 | $lexemeRevision = $this->getBaseLexemeRevisionFromRequest( $request ); |
142 | /** @var Lexeme $lexeme */ |
143 | $lexeme = $lexemeRevision->getEntity(); |
144 | $changeOp = $request->getChangeOp(); |
145 | |
146 | $summary = new Summary(); |
147 | try { |
148 | $changeOp->apply( $lexeme, $summary ); |
149 | } catch ( ChangeOpException $exception ) { |
150 | $this->errorReporter->dieException( $exception, 'unprocessable-request' ); |
151 | } |
152 | |
153 | $baseRevId = $request->getBaseRevId() ?: $lexemeRevision->getRevisionId(); |
154 | |
155 | $flags = $this->buildSaveFlags( $params ); |
156 | $status = $this->saveNewLexemeRevision( $lexeme, $baseRevId, $summary, $flags, $params['tags'] ?: [] ); |
157 | |
158 | if ( !$status->isGood() ) { |
159 | $this->dieStatus( $status ); |
160 | } |
161 | |
162 | $this->fillApiResultFromStatus( $status, $params ); |
163 | } |
164 | |
165 | protected function getAllowedParams(): array { |
166 | return array_merge( [ |
167 | AddFormRequestParser::PARAM_LEXEME_ID => [ |
168 | ParamValidator::PARAM_TYPE => 'string', |
169 | ParamValidator::PARAM_REQUIRED => true, |
170 | ], |
171 | AddFormRequestParser::PARAM_DATA => [ |
172 | ParamValidator::PARAM_TYPE => 'text', |
173 | ParamValidator::PARAM_REQUIRED => true, |
174 | ], |
175 | AddFormRequestParser::PARAM_BASEREVID => [ |
176 | ParamValidator::PARAM_TYPE => 'integer', |
177 | ], |
178 | 'tags' => [ |
179 | ParamValidator::PARAM_TYPE => 'tags', |
180 | ParamValidator::PARAM_ISMULTI => true, |
181 | ], |
182 | 'bot' => [ |
183 | ParamValidator::PARAM_TYPE => 'boolean', |
184 | ParamValidator::PARAM_DEFAULT => false, |
185 | ], |
186 | ], $this->getCreateTempUserParams() ); |
187 | } |
188 | |
189 | public function isWriteMode(): bool { |
190 | return true; |
191 | } |
192 | |
193 | /** |
194 | * As long as this codebase is in development and APIs might change any time without notice, we |
195 | * mark all as internal. This adds an "unstable" notice, but does not hide them in any way. |
196 | */ |
197 | public function isInternal(): bool { |
198 | return true; |
199 | } |
200 | |
201 | public function needsToken(): string { |
202 | return 'csrf'; |
203 | } |
204 | |
205 | public function mustBePosted(): bool { |
206 | return true; |
207 | } |
208 | |
209 | protected function getExamplesMessages(): array { |
210 | $lexemeId = 'L12'; |
211 | $exampleData = [ |
212 | 'representations' => [ |
213 | 'en-us' => [ 'value' => 'color', 'language' => 'en-us' ], |
214 | 'en-gb' => [ 'value' => 'colour', 'language' => 'en-gb' ], |
215 | ], |
216 | 'grammaticalFeatures' => [ |
217 | 'Q1', 'Q2', |
218 | ], |
219 | ]; |
220 | |
221 | $query = http_build_query( [ |
222 | 'action' => $this->getModuleName(), |
223 | AddFormRequestParser::PARAM_LEXEME_ID => $lexemeId, |
224 | AddFormRequestParser::PARAM_DATA => json_encode( $exampleData ), |
225 | ] ); |
226 | |
227 | $languages = array_column( $exampleData['representations'], 'language' ); |
228 | $representations = array_column( $exampleData['representations'], 'value' ); |
229 | |
230 | $representationsText = $this->getLanguage()->commaList( $representations ); |
231 | $languagesText = $this->getLanguage()->commaList( $languages ); |
232 | $grammaticalFeaturesText = $this->getLanguage()->commaList( $exampleData['grammaticalFeatures'] ); |
233 | |
234 | $exampleMessage = new Message( |
235 | 'apihelp-wbladdform-example-1', |
236 | [ |
237 | $lexemeId, |
238 | $representationsText, |
239 | $languagesText, |
240 | $grammaticalFeaturesText, |
241 | ] |
242 | ); |
243 | |
244 | return [ |
245 | urldecode( $query ) => $exampleMessage, |
246 | ]; |
247 | } |
248 | |
249 | private function getFormWithMaxId( Lexeme $lexeme ): Form { |
250 | // TODO: This is all rather nasty |
251 | $maxIdNumber = $lexeme->getForms()->maxFormIdNumber(); |
252 | // TODO: Use some service to get the ID object! |
253 | $formId = new FormId( $lexeme->getId() . '-F' . $maxIdNumber ); |
254 | return $lexeme->getForm( $formId ); |
255 | } |
256 | |
257 | /** |
258 | * @throws ApiUsageException |
259 | */ |
260 | private function getBaseLexemeRevisionFromRequest( AddFormRequest $request ): EntityRevision { |
261 | $lexemeId = $request->getLexemeId(); |
262 | try { |
263 | $lexemeRevision = $this->entityRevisionLookup->getEntityRevision( |
264 | $lexemeId, |
265 | self::LATEST_REVISION, |
266 | LookupConstants::LATEST_FROM_MASTER |
267 | ); |
268 | } catch ( StorageException $e ) { |
269 | // TODO Test it |
270 | if ( $e->getStatus() ) { |
271 | $this->dieStatus( $e->getStatus() ); |
272 | } else { |
273 | throw new LogicException( |
274 | 'StorageException caught with no status', |
275 | 0, |
276 | $e |
277 | ); |
278 | } |
279 | } |
280 | |
281 | if ( !$lexemeRevision ) { |
282 | $error = new LexemeNotFound( $lexemeId ); |
283 | $this->dieWithError( $error->asApiMessage( AddFormRequestParser::PARAM_LEXEME_ID, [] ) ); |
284 | } |
285 | |
286 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable |
287 | return $lexemeRevision; |
288 | } |
289 | |
290 | private function buildSaveFlags( array $params ): int { |
291 | $flags = EDIT_UPDATE; |
292 | if ( isset( $params['bot'] ) && $params['bot'] && |
293 | $this->getPermissionManager()->userHasRight( $this->getUser(), 'bot' ) |
294 | ) { |
295 | $flags |= EDIT_FORCE_BOT; |
296 | } |
297 | return $flags; |
298 | } |
299 | |
300 | private function saveNewLexemeRevision( |
301 | EntityDocument $lexeme, |
302 | int $baseRevId, |
303 | FormatableSummary $summary, |
304 | int $flags, |
305 | array $tags |
306 | ): EditEntityStatus { |
307 | $editEntity = $this->editEntityFactory->newEditEntity( |
308 | $this->getContext(), |
309 | $lexeme->getId(), |
310 | $baseRevId |
311 | ); |
312 | $summaryString = $this->summaryFormatter->formatSummary( |
313 | $summary |
314 | ); |
315 | |
316 | $tokenThatDoesNotNeedChecking = false; |
317 | try { |
318 | $status = $editEntity->attemptSave( |
319 | $lexeme, |
320 | $summaryString, |
321 | $flags, |
322 | $tokenThatDoesNotNeedChecking, |
323 | null, |
324 | $tags |
325 | ); |
326 | } catch ( ConflictException $exception ) { |
327 | $this->dieWithException( new RuntimeException( 'Edit conflict: ' . $exception->getMessage() ) ); |
328 | } |
329 | |
330 | return $status; |
331 | } |
332 | |
333 | private function fillApiResultFromStatus( EditEntityStatus $status, array $params ): void { |
334 | $entityRevision = $status->getRevision(); |
335 | |
336 | /** @var Lexeme $editedLexeme */ |
337 | $editedLexeme = $entityRevision->getEntity(); |
338 | '@phan-var Lexeme $editedLexeme'; |
339 | $newForm = $this->getFormWithMaxId( $editedLexeme ); |
340 | $serializedForm = $this->formSerializer->serialize( $newForm ); |
341 | |
342 | $this->resultBuilder->addRevisionIdFromStatusToResult( $status, null ); |
343 | $this->resultBuilder->markSuccess(); |
344 | $this->getResult()->addValue( null, 'form', $serializedForm ); |
345 | $this->resultBuilder->addTempUser( $status, fn ( $user ) => $this->getTempUserRedirectUrl( $params, $user ) ); |
346 | } |
347 | |
348 | } |