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