Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
33.65% |
70 / 208 |
|
28.57% |
4 / 14 |
CRAP | |
0.00% |
0 / 1 |
EntitySchemaContentHandler | |
33.65% |
70 / 208 |
|
28.57% |
4 / 14 |
663.96 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
getContentClass | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSlotDiffRendererWithOptions | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getPageViewLanguage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
canBeUsedOn | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
getActionOverrides | |
0.00% |
0 / 71 |
|
0.00% |
0 / 1 |
2 | |||
getActionOverridesEdit | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
30 | |||
getActionOverridesSubmit | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
20 | |||
supportsDirectApiEditing | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUndoContent | |
79.17% |
19 / 24 |
|
0.00% |
0 / 1 |
8.58 | |||
isParserCacheSupported | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
fillParserOutput | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
3 | |||
getFieldsForSearchIndex | |
76.47% |
13 / 17 |
|
0.00% |
0 / 1 |
8.83 | |||
getDataForSearchIndex | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
9.03 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace EntitySchema\MediaWiki\Content; |
6 | |
7 | use CirrusSearch\CirrusSearch; |
8 | use EntitySchema\DataAccess\EntitySchemaEncoder; |
9 | use EntitySchema\DataAccess\LabelLookup; |
10 | use EntitySchema\MediaWiki\Actions\EntitySchemaEditAction; |
11 | use EntitySchema\MediaWiki\Actions\EntitySchemaSubmitAction; |
12 | use EntitySchema\MediaWiki\Actions\RestoreSubmitAction; |
13 | use EntitySchema\MediaWiki\Actions\RestoreViewAction; |
14 | use EntitySchema\MediaWiki\Actions\UndoSubmitAction; |
15 | use EntitySchema\MediaWiki\Actions\UndoViewAction; |
16 | use EntitySchema\MediaWiki\Actions\ViewEntitySchemaAction; |
17 | use EntitySchema\MediaWiki\UndoHandler; |
18 | use EntitySchema\Presentation\InputValidator; |
19 | use EntitySchema\Services\Converter\EntitySchemaConverter; |
20 | use LogicException; |
21 | use MediaWiki\Actions\Action; |
22 | use MediaWiki\Config\Config; |
23 | use MediaWiki\Content\Content; |
24 | use MediaWiki\Content\JsonContentHandler; |
25 | use MediaWiki\Content\Renderer\ContentParseParams; |
26 | use MediaWiki\Context\IContextSource; |
27 | use MediaWiki\Context\RequestContext; |
28 | use MediaWiki\HookContainer\HookContainer; |
29 | use MediaWiki\Language\Language; |
30 | use MediaWiki\Languages\LanguageNameUtils; |
31 | use MediaWiki\Page\Article; |
32 | use MediaWiki\Page\WikiPage; |
33 | use MediaWiki\Parser\ParserOutput; |
34 | use MediaWiki\Permissions\PermissionManager; |
35 | use MediaWiki\Revision\RevisionRecord; |
36 | use MediaWiki\Revision\RevisionStore; |
37 | use MediaWiki\Revision\SlotRecord; |
38 | use MediaWiki\Title\Title; |
39 | use MediaWiki\User\Options\UserOptionsLookup; |
40 | use MediaWiki\User\TempUser\TempUserConfig; |
41 | use SearchEngine; |
42 | use SearchIndexField; |
43 | use Wikibase\Lib\LanguageNameLookupFactory; |
44 | use Wikibase\Lib\SettingsArray; |
45 | use Wikibase\Search\Elastic\Fields\DescriptionsProviderFieldDefinitions; |
46 | use Wikibase\Search\Elastic\Fields\LabelsProviderFieldDefinitions; |
47 | use Wikimedia\ObjectFactory\ObjectFactory; |
48 | use Wikimedia\Rdbms\ReadOnlyMode; |
49 | |
50 | /** |
51 | * Content handler for the EntitySchema content |
52 | * |
53 | * @license GPL-2.0-or-later |
54 | */ |
55 | class EntitySchemaContentHandler extends JsonContentHandler { |
56 | |
57 | /** |
58 | * @var LabelsProviderFieldDefinitions|null The search field definitions for labels, |
59 | * or null if WikibaseCirrusSearch is not loaded and no search fields are available. |
60 | */ |
61 | private ?LabelsProviderFieldDefinitions $labelsFieldDefinitions; |
62 | |
63 | /** |
64 | * @var DescriptionsProviderFieldDefinitions|null The search field definitions for descriptions, |
65 | * or null if WikibaseCirrusSearch is not loaded and no search fields are available. |
66 | */ |
67 | private ?DescriptionsProviderFieldDefinitions $descriptionsFieldDefinitions; |
68 | |
69 | private LanguageNameLookupFactory $languageNameLookupFactory; |
70 | |
71 | private LabelLookup $labelLookup; |
72 | |
73 | private ObjectFactory $objectFactory; |
74 | |
75 | private HookContainer $hookContainer; |
76 | |
77 | public function __construct( |
78 | string $modelId, |
79 | LabelLookup $labelLookup, |
80 | LanguageNameLookupFactory $languageNameLookupFactory, |
81 | ObjectFactory $objectFactory, |
82 | HookContainer $hookContainer, |
83 | ?LabelsProviderFieldDefinitions $labelsFieldDefinitions, |
84 | ?DescriptionsProviderFieldDefinitions $descriptionsFieldDefinitions |
85 | ) { |
86 | // $modelId is typically EntitySchemaContent::CONTENT_MODEL_ID |
87 | parent::__construct( $modelId ); |
88 | $this->labelLookup = $labelLookup; |
89 | $this->languageNameLookupFactory = $languageNameLookupFactory; |
90 | $this->labelsFieldDefinitions = $labelsFieldDefinitions; |
91 | $this->descriptionsFieldDefinitions = $descriptionsFieldDefinitions; |
92 | $this->objectFactory = $objectFactory; |
93 | $this->hookContainer = $hookContainer; |
94 | } |
95 | |
96 | protected function getContentClass(): string { |
97 | return EntitySchemaContent::class; |
98 | } |
99 | |
100 | /** @inheritDoc */ |
101 | protected function getSlotDiffRendererWithOptions( |
102 | IContextSource $context, |
103 | $options = [] |
104 | ): EntitySchemaSlotDiffRenderer { |
105 | return new EntitySchemaSlotDiffRenderer( |
106 | $context, |
107 | $this->createTextSlotDiffRenderer( $options ) |
108 | ); |
109 | } |
110 | |
111 | /** |
112 | * @see ContentHandler::getPageViewLanguage |
113 | * |
114 | * This implementation returns the user language, because Schemas get rendered in |
115 | * the user's language. The PageContentLanguage hook is bypassed. |
116 | * |
117 | * @param Title $title (unused) the page to determine the language for. |
118 | * @param Content|null $content (unused) the page's content |
119 | * |
120 | * @return Language The page's language |
121 | */ |
122 | public function getPageViewLanguage( Title $title, ?Content $content = null ): Language { |
123 | $context = RequestContext::getMain(); |
124 | return $context->getLanguage(); |
125 | } |
126 | |
127 | public function canBeUsedOn( Title $title ): bool { |
128 | return $title->inNamespace( NS_ENTITYSCHEMA_JSON ) && parent::canBeUsedOn( $title ); |
129 | } |
130 | |
131 | public function getActionOverrides(): array { |
132 | return [ |
133 | 'edit' => [ |
134 | 'factory' => function ( |
135 | Article $article, |
136 | IContextSource $context, |
137 | RevisionStore $revisionStore, |
138 | Config $mainConfig, |
139 | LanguageNameUtils $languageNameUtils, |
140 | UserOptionsLookup $userOptionsLookup, |
141 | SettingsArray $repoSettings, |
142 | TempUserConfig $tempUserConfig |
143 | ) { |
144 | return $this->getActionOverridesEdit( |
145 | $article, |
146 | $context, |
147 | $revisionStore, |
148 | $mainConfig, |
149 | $languageNameUtils, |
150 | $userOptionsLookup, |
151 | $repoSettings, |
152 | $tempUserConfig |
153 | ); |
154 | }, |
155 | 'services' => [ |
156 | 'RevisionStore', |
157 | 'MainConfig', |
158 | 'LanguageNameUtils', |
159 | 'UserOptionsLookup', |
160 | 'WikibaseRepo.Settings', |
161 | 'TempUserConfig', |
162 | ], |
163 | ], |
164 | 'submit' => [ |
165 | 'factory' => function ( |
166 | Article $article, |
167 | IContextSource $context, |
168 | ReadOnlyMode $readOnlyMode, |
169 | RevisionStore $revisionStore, |
170 | PermissionManager $permissionManager, |
171 | Config $mainConfig, |
172 | LanguageNameUtils $languageNameUtils, |
173 | UserOptionsLookup $userOptionsLookup, |
174 | SettingsArray $repoSettings, |
175 | TempUserConfig $tempUserConfig |
176 | ) { |
177 | return $this->getActionOverridesSubmit( |
178 | $article, |
179 | $context, |
180 | $readOnlyMode, |
181 | $revisionStore, |
182 | $permissionManager, |
183 | $mainConfig, |
184 | $languageNameUtils, |
185 | $userOptionsLookup, |
186 | $repoSettings, |
187 | $tempUserConfig |
188 | ); |
189 | }, |
190 | 'services' => [ |
191 | 'ReadOnlyMode', |
192 | 'RevisionStore', |
193 | 'PermissionManager', |
194 | 'MainConfig', |
195 | 'LanguageNameUtils', |
196 | 'UserOptionsLookup', |
197 | 'WikibaseRepo.Settings', |
198 | 'TempUserConfig', |
199 | ], |
200 | ], |
201 | 'view' => ViewEntitySchemaAction::class, |
202 | ]; |
203 | } |
204 | |
205 | private function getActionOverridesEdit( |
206 | Article $article, |
207 | IContextSource $context, |
208 | RevisionStore $revisionStore, |
209 | Config $mainConfig, |
210 | LanguageNameUtils $languageNameUtils, |
211 | UserOptionsLookup $userOptionsLookup, |
212 | SettingsArray $repoSettings, |
213 | TempUserConfig $tempUserConfig |
214 | ): Action { |
215 | global $wgEditSubmitButtonLabelPublish; |
216 | |
217 | if ( $article->getPage()->getRevisionRecord() === null ) { |
218 | return Action::factory( 'view', $article, $context ); |
219 | } |
220 | |
221 | $req = $context->getRequest(); |
222 | |
223 | if ( |
224 | $req->getCheck( 'undo' ) |
225 | || $req->getCheck( 'undoafter' ) |
226 | ) { |
227 | return new UndoViewAction( |
228 | $article, |
229 | $context, |
230 | $this->getSlotDiffRendererWithOptions( $context ), |
231 | $revisionStore |
232 | ); |
233 | } |
234 | |
235 | if ( $req->getCheck( 'restore' ) ) { |
236 | return new RestoreViewAction( |
237 | $article, |
238 | $context, |
239 | $this->getSlotDiffRendererWithOptions( $context ) |
240 | ); |
241 | } |
242 | |
243 | // TODo: check redirect? |
244 | // !$article->isRedirect() |
245 | return new EntitySchemaEditAction( |
246 | $article, |
247 | $context, |
248 | new InputValidator( $context, $mainConfig, $languageNameUtils ), |
249 | $wgEditSubmitButtonLabelPublish, |
250 | $userOptionsLookup, |
251 | $repoSettings->getSetting( 'dataRightsUrl' ), |
252 | $repoSettings->getSetting( 'dataRightsText' ), |
253 | $tempUserConfig |
254 | ); |
255 | } |
256 | |
257 | private function getActionOverridesSubmit( |
258 | Article $article, |
259 | IContextSource $context, |
260 | ReadOnlyMode $readOnlyMode, |
261 | RevisionStore $revisionStore, |
262 | PermissionManager $permissionManager, |
263 | Config $mainConfig, |
264 | LanguageNameUtils $languageNameUtils, |
265 | UserOptionsLookup $userOptionsLookup, |
266 | SettingsArray $repoSettings, |
267 | TempUserConfig $tempUserConfig |
268 | ): Action { |
269 | global $wgEditSubmitButtonLabelPublish; |
270 | $req = $context->getRequest(); |
271 | |
272 | if ( |
273 | $req->getCheck( 'undo' ) |
274 | || $req->getCheck( 'undoafter' ) |
275 | ) { |
276 | return new UndoSubmitAction( |
277 | $article, |
278 | $context, |
279 | $readOnlyMode, |
280 | $permissionManager, |
281 | $revisionStore |
282 | ); |
283 | } |
284 | |
285 | if ( $req->getCheck( 'restore' ) ) { |
286 | return new RestoreSubmitAction( $article, $context ); |
287 | } |
288 | |
289 | return new EntitySchemaSubmitAction( |
290 | $article, |
291 | $context, |
292 | new InputValidator( $context, $mainConfig, $languageNameUtils ), |
293 | $wgEditSubmitButtonLabelPublish, |
294 | $userOptionsLookup, |
295 | $repoSettings->getSetting( 'dataRightsUrl' ), |
296 | $repoSettings->getSetting( 'dataRightsText' ), |
297 | $tempUserConfig |
298 | ); |
299 | } |
300 | |
301 | public function supportsDirectApiEditing(): bool { |
302 | return false; |
303 | } |
304 | |
305 | /** |
306 | * Get the Content object that needs to be saved in order to undo all revisions |
307 | * between $undo and $undoafter. Revisions must belong to the same page, |
308 | * must exist and must not be deleted. |
309 | * |
310 | * @since 1.32 accepts Content objects for all parameters instead of Revision objects. |
311 | * Passing Revision objects is deprecated. |
312 | * @since 1.37 only accepts Content objects |
313 | * |
314 | * @param Content $baseContent The current text |
315 | * @param Content $undoFromContent The content of the revision to undo |
316 | * @param Content $undoToContent Must be from an earlier revision than $undo |
317 | * @param bool $undoIsLatest Set true if $undo is from the current revision (since 1.32) |
318 | * |
319 | * @return Content|false |
320 | */ |
321 | public function getUndoContent( |
322 | Content $baseContent, |
323 | Content $undoFromContent, |
324 | Content $undoToContent, |
325 | $undoIsLatest = false |
326 | ) { |
327 | if ( $undoIsLatest ) { |
328 | return $undoToContent; |
329 | } |
330 | |
331 | // Make sure correct subclass |
332 | if ( !$baseContent instanceof EntitySchemaContent || |
333 | !$undoFromContent instanceof EntitySchemaContent || |
334 | !$undoToContent instanceof EntitySchemaContent |
335 | ) { |
336 | return false; |
337 | } |
338 | |
339 | $undoHandler = new UndoHandler(); |
340 | try { |
341 | $schemaId = $undoHandler->validateContentIds( $undoToContent, $undoFromContent, $baseContent ); |
342 | } catch ( LogicException $e ) { |
343 | return false; |
344 | } |
345 | |
346 | $diffStatus = $undoHandler->getDiffFromContents( $undoFromContent, $undoToContent ); |
347 | if ( !$diffStatus->isOK() ) { |
348 | return false; |
349 | } |
350 | |
351 | $patchStatus = $undoHandler->tryPatching( $diffStatus->getValue(), $baseContent ); |
352 | if ( !$patchStatus->isOK() ) { |
353 | return false; |
354 | } |
355 | $patchedSchema = $patchStatus->getValue()->data; |
356 | |
357 | return new EntitySchemaContent( EntitySchemaEncoder::getPersistentRepresentation( |
358 | $schemaId, |
359 | $patchedSchema['labels'], |
360 | $patchedSchema['descriptions'], |
361 | $patchedSchema['aliases'], |
362 | $patchedSchema['schemaText'] |
363 | ) ); |
364 | } |
365 | |
366 | /** |
367 | * Returns true to indicate that the parser cache can be used for Schemas. |
368 | * |
369 | * @note The html representation of Schemas depends on the user language, so |
370 | * EntitySchemaContent::getParserOutput needs to make sure |
371 | * ParserOutput::recordOption( 'userlang' ) is called to split the cache by user language. |
372 | * |
373 | * @see ContentHandler::isParserCacheSupported |
374 | * |
375 | * @return bool Always true in this default implementation. |
376 | */ |
377 | public function isParserCacheSupported(): bool { |
378 | return true; |
379 | } |
380 | |
381 | /** |
382 | * @inheritDoc |
383 | */ |
384 | protected function fillParserOutput( |
385 | Content $content, |
386 | ContentParseParams $cpoParams, |
387 | ParserOutput &$parserOutput |
388 | ): void { |
389 | '@phan-var EntitySchemaContent $content'; |
390 | $parserOptions = $cpoParams->getParserOptions(); |
391 | $generateHtml = $cpoParams->getGenerateHtml(); |
392 | if ( $generateHtml && $content->isValid() ) { |
393 | $languageCode = $parserOptions->getUserLang(); |
394 | $renderer = new EntitySchemaSlotViewRenderer( |
395 | $languageCode, |
396 | $this->labelLookup, |
397 | $this->languageNameLookupFactory |
398 | ); |
399 | $renderer->fillParserOutput( |
400 | ( new EntitySchemaConverter() ) |
401 | ->getFullViewSchemaData( $content->getText() ), |
402 | $cpoParams->getPage(), |
403 | $parserOutput |
404 | ); |
405 | } else { |
406 | $parserOutput->setText( '' ); |
407 | } |
408 | } |
409 | |
410 | /** |
411 | * @param SearchEngine $engine |
412 | * @return SearchIndexField[] List of fields this content handler can provide. |
413 | */ |
414 | public function getFieldsForSearchIndex( SearchEngine $engine ): array { |
415 | if ( $this->labelsFieldDefinitions === null || $this->descriptionsFieldDefinitions === null ) { |
416 | if ( $engine instanceof CirrusSearch ) { |
417 | wfLogWarning( |
418 | 'Trying to use CirrusSearch but WikibaseCirrusSearch is not loaded. ' . |
419 | 'EntitySchema search is not available; consider loading WikibaseCirrusSearch.' |
420 | ); |
421 | } |
422 | return []; |
423 | } else { |
424 | $fields = []; |
425 | foreach ( $this->labelsFieldDefinitions->getFields() as $name => $field ) { |
426 | $mappingField = $field->getMappingField( $engine, $name ); |
427 | if ( $mappingField !== null ) { |
428 | $fields[$name] = $mappingField; |
429 | } |
430 | } |
431 | foreach ( $this->descriptionsFieldDefinitions->getFields() as $name => $field ) { |
432 | $mappingField = $field->getMappingField( $engine, $name ); |
433 | if ( $mappingField !== null ) { |
434 | $fields[$name] = $mappingField; |
435 | } |
436 | } |
437 | return $fields; |
438 | } |
439 | } |
440 | |
441 | public function getDataForSearchIndex( |
442 | WikiPage $page, |
443 | ParserOutput $output, |
444 | SearchEngine $engine, |
445 | ?RevisionRecord $revision = null |
446 | ): array { |
447 | $fieldsData = parent::getDataForSearchIndex( $page, $output, $engine, $revision ); |
448 | if ( $this->labelsFieldDefinitions === null || $this->descriptionsFieldDefinitions === null ) { |
449 | return $fieldsData; |
450 | } |
451 | $content = $revision !== null ? $revision->getContent( SlotRecord::MAIN ) : $page->getContent(); |
452 | if ( $content instanceof EntitySchemaContent ) { |
453 | $adapter = ( new EntitySchemaConverter() ) |
454 | ->getSearchEntitySchemaAdapter( $content->getText() ); |
455 | foreach ( $this->labelsFieldDefinitions->getFields() as $name => $field ) { |
456 | if ( $field !== null ) { |
457 | $fieldsData[$name] = $field->getLabelsIndexedData( $adapter ); |
458 | } |
459 | } |
460 | foreach ( $this->descriptionsFieldDefinitions->getFields() as $name => $field ) { |
461 | if ( $field !== null ) { |
462 | $fieldsData[$name] = $field->getDescriptionsIndexedData( $adapter ); |
463 | } |
464 | } |
465 | } |
466 | return $fieldsData; |
467 | } |
468 | |
469 | } |