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