Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
34.29% covered (danger)
34.29%
72 / 210
28.57% covered (danger)
28.57%
4 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
EntitySchemaContentHandler
34.29% covered (danger)
34.29%
72 / 210
28.57% covered (danger)
28.57%
4 / 14
646.48
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
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\Language\Language;
29use MediaWiki\Languages\LanguageNameUtils;
30use MediaWiki\Page\Article;
31use MediaWiki\Page\WikiPage;
32use MediaWiki\Parser\ParserOutput;
33use MediaWiki\Parser\Parsoid\ParsoidParserFactory;
34use MediaWiki\Permissions\PermissionManager;
35use MediaWiki\Revision\RevisionRecord;
36use MediaWiki\Revision\RevisionStore;
37use MediaWiki\Revision\SlotRecord;
38use MediaWiki\Title\Title;
39use MediaWiki\Title\TitleFactory;
40use MediaWiki\User\Options\UserOptionsLookup;
41use MediaWiki\User\TempUser\TempUserConfig;
42use SearchEngine;
43use SearchIndexField;
44use Wikibase\Lib\LanguageNameLookupFactory;
45use Wikibase\Lib\SettingsArray;
46use Wikibase\Search\Elastic\Fields\DescriptionsProviderFieldDefinitions;
47use Wikibase\Search\Elastic\Fields\LabelsProviderFieldDefinitions;
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    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}