Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
74.68% |
118 / 158 |
|
70.00% |
7 / 10 |
CRAP | |
0.00% |
0 / 1 |
AddSense | |
74.68% |
118 / 158 |
|
70.00% |
7 / 10 |
29.85 | |
0.00% |
0 / 1 |
factory | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
1 | |||
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
81.82% |
54 / 66 |
|
0.00% |
0 / 1 |
14.02 | |||
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 / 27 |
|
0.00% |
0 / 1 |
2 | |||
getSenseWithMaxId | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace Wikibase\Lexeme\MediaWiki\Api; |
6 | |
7 | use ApiBase; |
8 | use ApiCreateTempUserTrait; |
9 | use ApiMain; |
10 | use Deserializers\Deserializer; |
11 | use LogicException; |
12 | use RuntimeException; |
13 | use Wikibase\DataModel\Deserializers\TermDeserializer; |
14 | use Wikibase\DataModel\Entity\EntityIdParser; |
15 | use Wikibase\DataModel\Serializers\SerializerFactory; |
16 | use Wikibase\Lexeme\DataAccess\ChangeOp\Validation\LexemeTermLanguageValidator; |
17 | use Wikibase\Lexeme\DataAccess\ChangeOp\Validation\LexemeTermSerializationValidator; |
18 | use Wikibase\Lexeme\Domain\Model\Exceptions\ConflictException; |
19 | use Wikibase\Lexeme\Domain\Model\Lexeme; |
20 | use Wikibase\Lexeme\Domain\Model\Sense; |
21 | use Wikibase\Lexeme\Domain\Model\SenseId; |
22 | use Wikibase\Lexeme\MediaWiki\Api\Error\LexemeNotFound; |
23 | use Wikibase\Lexeme\Presentation\ChangeOp\Deserialization\EditSenseChangeOpDeserializer; |
24 | use Wikibase\Lexeme\Presentation\ChangeOp\Deserialization\GlossesChangeOpDeserializer; |
25 | use Wikibase\Lexeme\Serialization\SenseSerializer; |
26 | use Wikibase\Lexeme\WikibaseLexemeServices; |
27 | use Wikibase\Lib\Store\EntityRevisionLookup; |
28 | use Wikibase\Lib\Store\LookupConstants; |
29 | use Wikibase\Lib\Store\StorageException; |
30 | use Wikibase\Lib\StringNormalizer; |
31 | use Wikibase\Lib\Summary; |
32 | use Wikibase\Repo\Api\ApiErrorReporter; |
33 | use Wikibase\Repo\Api\ApiHelperFactory; |
34 | use Wikibase\Repo\Api\ResultBuilder; |
35 | use Wikibase\Repo\ChangeOp\ChangeOpException; |
36 | use Wikibase\Repo\ChangeOp\ChangeOpFactoryProvider; |
37 | use Wikibase\Repo\ChangeOp\ChangeOpValidationException; |
38 | use Wikibase\Repo\ChangeOp\Deserialization\ClaimsChangeOpDeserializer; |
39 | use Wikibase\Repo\EditEntity\MediaWikiEditEntityFactory; |
40 | use Wikibase\Repo\Store\Store; |
41 | use Wikibase\Repo\SummaryFormatter; |
42 | use Wikimedia\ParamValidator\ParamValidator; |
43 | |
44 | /** |
45 | * @license GPL-2.0-or-later |
46 | */ |
47 | class AddSense extends ApiBase { |
48 | |
49 | use ApiCreateTempUserTrait; |
50 | |
51 | private const LATEST_REVISION = 0; |
52 | |
53 | private AddSenseRequestParser $requestParser; |
54 | |
55 | private ResultBuilder $resultBuilder; |
56 | private ApiErrorReporter $errorReporter; |
57 | private SenseSerializer $senseSerializer; |
58 | private MediaWikiEditEntityFactory $editEntityFactory; |
59 | private SummaryFormatter $summaryFormatter; |
60 | private EntityRevisionLookup $entityRevisionLookup; |
61 | |
62 | /** |
63 | * @return self |
64 | */ |
65 | public static function factory( |
66 | ApiMain $mainModule, |
67 | string $moduleName, |
68 | ApiHelperFactory $apiHelperFactory, |
69 | SerializerFactory $baseDataModelSerializerFactory, |
70 | ChangeOpFactoryProvider $changeOpFactoryProvider, |
71 | MediaWikiEditEntityFactory $editEntityFactory, |
72 | EntityIdParser $entityIdParser, |
73 | Deserializer $externalFormatStatementDeserializer, |
74 | Store $store, |
75 | StringNormalizer $stringNormalizer, |
76 | SummaryFormatter $summaryFormatter |
77 | ) { |
78 | $senseSerializer = new SenseSerializer( |
79 | $baseDataModelSerializerFactory->newTermListSerializer(), |
80 | $baseDataModelSerializerFactory->newStatementListSerializer() |
81 | ); |
82 | |
83 | return new self( |
84 | $mainModule, |
85 | $moduleName, |
86 | new AddSenseRequestParser( |
87 | $entityIdParser, |
88 | new EditSenseChangeOpDeserializer( |
89 | new GlossesChangeOpDeserializer( |
90 | new TermDeserializer(), |
91 | $stringNormalizer, |
92 | new LexemeTermSerializationValidator( |
93 | new LexemeTermLanguageValidator( WikibaseLexemeServices::getTermLanguages() ) |
94 | ) |
95 | ), |
96 | new ClaimsChangeOpDeserializer( |
97 | $externalFormatStatementDeserializer, |
98 | $changeOpFactoryProvider->getStatementChangeOpFactory() |
99 | ) |
100 | ) |
101 | ), |
102 | $senseSerializer, |
103 | $store->getEntityRevisionLookup( Store::LOOKUP_CACHING_DISABLED ), |
104 | $editEntityFactory, |
105 | $summaryFormatter, |
106 | $apiHelperFactory |
107 | ); |
108 | } |
109 | |
110 | public function __construct( |
111 | ApiMain $mainModule, |
112 | string $moduleName, |
113 | AddSenseRequestParser $requestParser, |
114 | SenseSerializer $senseSerializer, |
115 | EntityRevisionLookup $entityRevisionLookup, |
116 | MediaWikiEditEntityFactory $editEntityFactory, |
117 | SummaryFormatter $summaryFormatter, |
118 | ApiHelperFactory $apiHelperFactory |
119 | ) { |
120 | parent::__construct( $mainModule, $moduleName ); |
121 | |
122 | $this->resultBuilder = $apiHelperFactory->getResultBuilder( $this ); |
123 | $this->errorReporter = $apiHelperFactory->getErrorReporter( $this ); |
124 | $this->requestParser = $requestParser; |
125 | $this->senseSerializer = $senseSerializer; |
126 | $this->editEntityFactory = $editEntityFactory; |
127 | $this->entityRevisionLookup = $entityRevisionLookup; |
128 | $this->summaryFormatter = $summaryFormatter; |
129 | } |
130 | |
131 | /** |
132 | * @see ApiBase::execute() |
133 | * |
134 | * @throws \ApiUsageException |
135 | */ |
136 | public function execute(): void { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh |
137 | /* |
138 | * { |
139 | "glosses": [ |
140 | "en-GB": { |
141 | "value": "colour", |
142 | "language": "en-GB" |
143 | }, |
144 | "en-US": { |
145 | "value": "color", |
146 | "language": "en-US" |
147 | } |
148 | ] |
149 | } |
150 | * |
151 | */ |
152 | |
153 | //TODO: Documenting response structure. Is it possible? |
154 | |
155 | $params = $this->extractRequestParams(); |
156 | $request = $this->requestParser->parse( $params ); |
157 | |
158 | try { |
159 | $lexemeId = $request->getLexemeId(); |
160 | $lexemeRevision = $this->entityRevisionLookup->getEntityRevision( |
161 | $lexemeId, |
162 | self::LATEST_REVISION, |
163 | LookupConstants::LATEST_FROM_MASTER |
164 | ); |
165 | |
166 | if ( !$lexemeRevision ) { |
167 | $error = new LexemeNotFound( $lexemeId ); |
168 | $this->dieWithError( $error->asApiMessage( AddSenseRequestParser::PARAM_LEXEME_ID, |
169 | [] ) ); |
170 | } |
171 | } catch ( StorageException $e ) { |
172 | // TODO Test it |
173 | if ( $e->getStatus() ) { |
174 | $this->dieStatus( $e->getStatus() ); |
175 | } else { |
176 | throw new LogicException( |
177 | 'StorageException caught with no status', |
178 | 0, |
179 | $e |
180 | ); |
181 | } |
182 | } |
183 | /** @var Lexeme $lexeme */ |
184 | $lexeme = $lexemeRevision->getEntity(); |
185 | $changeOp = $request->getChangeOp(); |
186 | |
187 | $summary = new Summary(); |
188 | $result = $changeOp->validate( $lexeme ); |
189 | if ( !$result->isValid() ) { |
190 | $this->errorReporter->dieException( |
191 | new ChangeOpValidationException( $result ), |
192 | 'modification-failed' |
193 | ); |
194 | } |
195 | |
196 | try { |
197 | $changeOp->apply( $lexeme, $summary ); |
198 | } catch ( ChangeOpException $exception ) { |
199 | $this->errorReporter->dieException( $exception, 'unprocessable-request' ); |
200 | } |
201 | |
202 | $baseRevId = $request->getBaseRevId() ?: $lexemeRevision->getRevisionId(); |
203 | |
204 | $editEntity = $this->editEntityFactory->newEditEntity( |
205 | $this->getContext(), |
206 | $request->getLexemeId(), |
207 | $baseRevId |
208 | ); |
209 | $summaryString = $this->summaryFormatter->formatSummary( |
210 | $summary |
211 | ); |
212 | $flags = EDIT_UPDATE; |
213 | if ( isset( $params['bot'] ) && $params['bot'] && |
214 | $this->getPermissionManager()->userHasRight( $this->getUser(), 'bot' ) |
215 | ) { |
216 | $flags |= EDIT_FORCE_BOT; |
217 | } |
218 | |
219 | $tokenThatDoesNotNeedChecking = false; |
220 | try { |
221 | $status = $editEntity->attemptSave( |
222 | $lexeme, |
223 | $summaryString, |
224 | $flags, |
225 | $tokenThatDoesNotNeedChecking, |
226 | null, |
227 | $params['tags'] ?: [] |
228 | ); |
229 | } catch ( ConflictException $exception ) { |
230 | $this->dieWithException( new RuntimeException( 'Edit conflict: ' . $exception->getMessage() ) ); |
231 | } |
232 | |
233 | if ( !$status->isGood() ) { |
234 | $this->dieStatus( $status ); |
235 | } |
236 | |
237 | $entityRevision = $status->getRevision(); |
238 | |
239 | /** @var Lexeme $editedLexeme */ |
240 | $editedLexeme = $entityRevision->getEntity(); |
241 | '@phan-var Lexeme $editedLexeme'; |
242 | $newSense = $this->getSenseWithMaxId( $editedLexeme ); |
243 | $serializedSense = $this->senseSerializer->serialize( $newSense ); |
244 | |
245 | $this->resultBuilder->addRevisionIdFromStatusToResult( $status, null ); |
246 | $this->resultBuilder->markSuccess(); |
247 | $this->resultBuilder->addTempUser( $status, fn ( $user ) => $this->getTempUserRedirectUrl( $params, $user ) ); |
248 | $this->getResult()->addValue( null, 'sense', $serializedSense ); |
249 | } |
250 | |
251 | /** @inheritDoc */ |
252 | protected function getAllowedParams(): array { |
253 | return array_merge( [ |
254 | AddSenseRequestParser::PARAM_LEXEME_ID => [ |
255 | ParamValidator::PARAM_TYPE => 'string', |
256 | ParamValidator::PARAM_REQUIRED => true, |
257 | ], |
258 | AddSenseRequestParser::PARAM_DATA => [ |
259 | ParamValidator::PARAM_TYPE => 'text', |
260 | ParamValidator::PARAM_REQUIRED => true, |
261 | ], |
262 | AddSenseRequestParser::PARAM_BASEREVID => [ |
263 | ParamValidator::PARAM_TYPE => 'integer', |
264 | ], |
265 | 'tags' => [ |
266 | ParamValidator::PARAM_TYPE => 'tags', |
267 | ParamValidator::PARAM_ISMULTI => true, |
268 | ], |
269 | 'bot' => [ |
270 | ParamValidator::PARAM_TYPE => 'boolean', |
271 | ParamValidator::PARAM_DEFAULT => false, |
272 | ], |
273 | ], $this->getCreateTempUserParams() ); |
274 | } |
275 | |
276 | public function isWriteMode(): bool { |
277 | return true; |
278 | } |
279 | |
280 | /** |
281 | * As long as this codebase is in development and APIs might change any time without notice, we |
282 | * mark all as internal. This adds an "unstable" notice, but does not hide them in any way. |
283 | */ |
284 | public function isInternal(): bool { |
285 | return true; |
286 | } |
287 | |
288 | public function needsToken(): string { |
289 | return 'csrf'; |
290 | } |
291 | |
292 | public function mustBePosted(): bool { |
293 | return true; |
294 | } |
295 | |
296 | protected function getExamplesMessages(): array { |
297 | $lexemeId = 'L12'; |
298 | $exampleData = [ |
299 | 'glosses' => [ |
300 | 'en-US' => [ 'value' => 'Some text value', 'language' => 'en-US' ], |
301 | 'en-GB' => [ 'value' => 'Another text value', 'language' => 'en-GB' ], |
302 | ], |
303 | ]; |
304 | |
305 | $query = http_build_query( [ |
306 | 'action' => $this->getModuleName(), |
307 | AddSenseRequestParser::PARAM_LEXEME_ID => $lexemeId, |
308 | AddSenseRequestParser::PARAM_DATA => json_encode( $exampleData ), |
309 | ] ); |
310 | |
311 | $languages = array_column( $exampleData['glosses'], 'language' ); |
312 | $glosses = array_column( $exampleData['glosses'], 'value' ); |
313 | |
314 | $glossesText = $this->getLanguage()->commaList( $glosses ); |
315 | $languagesText = $this->getLanguage()->commaList( $languages ); |
316 | |
317 | $exampleMessage = new \Message( |
318 | 'apihelp-wbladdsense-example-1', |
319 | [ |
320 | $lexemeId, |
321 | $glossesText, |
322 | $languagesText, |
323 | ] |
324 | ); |
325 | |
326 | return [ |
327 | urldecode( $query ) => $exampleMessage, |
328 | ]; |
329 | } |
330 | |
331 | private function getSenseWithMaxId( Lexeme $lexeme ): Sense { |
332 | // TODO: This is all rather nasty |
333 | $maxIdNumber = $lexeme->getSenses()->maxSenseIdNumber(); |
334 | // TODO: Use some service to get the ID object! |
335 | $senseId = new SenseId( $lexeme->getId() . '-S' . $maxIdNumber ); |
336 | return $lexeme->getSense( $senseId ); |
337 | } |
338 | |
339 | } |