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