Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.19% covered (warning)
72.19%
109 / 151
71.43% covered (warning)
71.43%
10 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
AddForm
72.19% covered (warning)
72.19%
109 / 151
71.43% covered (warning)
71.43%
10 / 14
38.45
0.00% covered (danger)
0.00%
0 / 1
 factory
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 getAllowedParams
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
1
 isWriteMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isInternal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 needsToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mustBePosted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
2
 getFormWithMaxId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getBaseLexemeRevisionFromRequest
55.56% covered (warning)
55.56%
10 / 18
0.00% covered (danger)
0.00%
0 / 1
5.40
 buildSaveFlags
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 saveNewLexemeRevision
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
2
 fillApiResultFromStatus
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare( strict_types = 1 );
4
5namespace Wikibase\Lexeme\MediaWiki\Api;
6
7use ApiBase;
8use ApiCreateTempUserTrait;
9use ApiMain;
10use LogicException;
11use RuntimeException;
12use Wikibase\DataModel\Entity\EntityDocument;
13use Wikibase\DataModel\Entity\EntityIdParser;
14use Wikibase\DataModel\Serializers\SerializerFactory;
15use Wikibase\Lexeme\Domain\Model\Exceptions\ConflictException;
16use Wikibase\Lexeme\Domain\Model\Form;
17use Wikibase\Lexeme\Domain\Model\FormId;
18use Wikibase\Lexeme\Domain\Model\Lexeme;
19use Wikibase\Lexeme\MediaWiki\Api\Error\LexemeNotFound;
20use Wikibase\Lexeme\Serialization\FormSerializer;
21use Wikibase\Lexeme\WikibaseLexemeServices;
22use Wikibase\Lib\FormatableSummary;
23use Wikibase\Lib\Store\EntityRevision;
24use Wikibase\Lib\Store\EntityRevisionLookup;
25use Wikibase\Lib\Store\LookupConstants;
26use Wikibase\Lib\Store\StorageException;
27use Wikibase\Lib\Summary;
28use Wikibase\Repo\Api\ApiErrorReporter;
29use Wikibase\Repo\Api\ApiHelperFactory;
30use Wikibase\Repo\Api\ResultBuilder;
31use Wikibase\Repo\ChangeOp\ChangeOpException;
32use Wikibase\Repo\EditEntity\EditEntityStatus;
33use Wikibase\Repo\EditEntity\MediaWikiEditEntityFactory;
34use Wikibase\Repo\Store\Store;
35use Wikibase\Repo\SummaryFormatter;
36use Wikimedia\ParamValidator\ParamValidator;
37
38/**
39 * @license GPL-2.0-or-later
40 */
41class 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}