Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
33.65% covered (danger)
33.65%
70 / 208
28.57% covered (danger)
28.57%
4 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
EntitySchemaContentHandler
33.65% covered (danger)
33.65%
70 / 208
28.57% covered (danger)
28.57%
4 / 14
663.96
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getContentClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSlotDiffRendererWithOptions
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getPageViewLanguage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 canBeUsedOn
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getActionOverrides
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 1
2
 getActionOverridesEdit
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
 getActionOverridesSubmit
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
20
 supportsDirectApiEditing
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUndoContent
79.17% covered (warning)
79.17%
19 / 24
0.00% covered (danger)
0.00%
0 / 1
8.58
 isParserCacheSupported
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fillParserOutput
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 getFieldsForSearchIndex
76.47% covered (warning)
76.47%
13 / 17
0.00% covered (danger)
0.00%
0 / 1
8.83
 getDataForSearchIndex
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
9.03
1<?php
2
3declare( strict_types = 1 );
4
5namespace EntitySchema\MediaWiki\Content;
6
7use CirrusSearch\CirrusSearch;
8use EntitySchema\DataAccess\EntitySchemaEncoder;
9use EntitySchema\DataAccess\LabelLookup;
10use EntitySchema\MediaWiki\Actions\EntitySchemaEditAction;
11use EntitySchema\MediaWiki\Actions\EntitySchemaSubmitAction;
12use EntitySchema\MediaWiki\Actions\RestoreSubmitAction;
13use EntitySchema\MediaWiki\Actions\RestoreViewAction;
14use EntitySchema\MediaWiki\Actions\UndoSubmitAction;
15use EntitySchema\MediaWiki\Actions\UndoViewAction;
16use EntitySchema\MediaWiki\Actions\ViewEntitySchemaAction;
17use EntitySchema\MediaWiki\UndoHandler;
18use EntitySchema\Presentation\InputValidator;
19use EntitySchema\Services\Converter\EntitySchemaConverter;
20use LogicException;
21use MediaWiki\Actions\Action;
22use MediaWiki\Config\Config;
23use MediaWiki\Content\Content;
24use MediaWiki\Content\JsonContentHandler;
25use MediaWiki\Content\Renderer\ContentParseParams;
26use MediaWiki\Context\IContextSource;
27use MediaWiki\Context\RequestContext;
28use MediaWiki\HookContainer\HookContainer;
29use MediaWiki\Language\Language;
30use MediaWiki\Languages\LanguageNameUtils;
31use MediaWiki\Page\Article;
32use MediaWiki\Page\WikiPage;
33use MediaWiki\Parser\ParserOutput;
34use MediaWiki\Permissions\PermissionManager;
35use MediaWiki\Revision\RevisionRecord;
36use MediaWiki\Revision\RevisionStore;
37use MediaWiki\Revision\SlotRecord;
38use MediaWiki\Title\Title;
39use MediaWiki\User\Options\UserOptionsLookup;
40use MediaWiki\User\TempUser\TempUserConfig;
41use SearchEngine;
42use SearchIndexField;
43use Wikibase\Lib\LanguageNameLookupFactory;
44use Wikibase\Lib\SettingsArray;
45use Wikibase\Search\Elastic\Fields\DescriptionsProviderFieldDefinitions;
46use Wikibase\Search\Elastic\Fields\LabelsProviderFieldDefinitions;
47use Wikimedia\ObjectFactory\ObjectFactory;
48use Wikimedia\Rdbms\ReadOnlyMode;
49
50/**
51 * Content handler for the EntitySchema content
52 *
53 * @license GPL-2.0-or-later
54 */
55class 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}