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