Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
75.16% |
115 / 153 |
|
66.67% |
8 / 12 |
CRAP | |
0.00% |
0 / 1 |
EditFormElements | |
75.16% |
115 / 153 |
|
66.67% |
8 / 12 |
32.82 | |
0.00% |
0 / 1 |
factory | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
1 | |||
__construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
88.89% |
32 / 36 |
|
0.00% |
0 / 1 |
6.05 | |||
saveForm | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
5.00 | |||
generateResponse | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getAllowedParams | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
1 | |||
isWriteMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isInternal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
needsToken | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
mustBePosted | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getExamplesMessages | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
2 | |||
getRevIdForWhenUserWasLastToEdit | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace Wikibase\Lexeme\MediaWiki\Api; |
6 | |
7 | use MediaWiki\Api\ApiBase; |
8 | use MediaWiki\Api\ApiCreateTempUserTrait; |
9 | use MediaWiki\Api\ApiMain; |
10 | use MediaWiki\Message\Message; |
11 | use Wikibase\DataModel\Entity\EntityId; |
12 | use Wikibase\DataModel\Entity\EntityIdParser; |
13 | use Wikibase\DataModel\Serializers\SerializerFactory; |
14 | use Wikibase\Lexeme\Domain\Model\Form; |
15 | use Wikibase\Lexeme\MediaWiki\Api\Error\FormNotFound; |
16 | use Wikibase\Lexeme\Presentation\ChangeOp\Deserialization\FormIdDeserializer; |
17 | use Wikibase\Lexeme\Serialization\FormSerializer; |
18 | use Wikibase\Lexeme\WikibaseLexemeServices; |
19 | use Wikibase\Lib\Store\EntityRevisionLookup; |
20 | use Wikibase\Lib\Store\EntityStore; |
21 | use Wikibase\Lib\Store\LookupConstants; |
22 | use Wikibase\Lib\Summary; |
23 | use Wikibase\Repo\Api\ApiErrorReporter; |
24 | use Wikibase\Repo\Api\ApiHelperFactory; |
25 | use Wikibase\Repo\Api\ResultBuilder; |
26 | use Wikibase\Repo\ChangeOp\ChangeOpException; |
27 | use Wikibase\Repo\ChangeOp\ChangeOpValidationException; |
28 | use Wikibase\Repo\EditEntity\EditEntityStatus; |
29 | use Wikibase\Repo\EditEntity\MediaWikiEditEntityFactory; |
30 | use Wikibase\Repo\Store\Store; |
31 | use Wikibase\Repo\SummaryFormatter; |
32 | use Wikimedia\ParamValidator\ParamValidator; |
33 | |
34 | /** |
35 | * @license GPL-2.0-or-later |
36 | */ |
37 | class 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 | } |