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