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