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