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