Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.16% covered (warning)
89.16%
74 / 83
75.00% covered (warning)
75.00%
12 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractWikiContentHandler
89.16% covered (warning)
89.16%
74 / 83
75.00% covered (warning)
75.00%
12 / 16
25.80
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 canBeUsedOn
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 makeEmptyContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContentClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 serializeContent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 unserializeContent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 validateSave
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getSecondaryDataUpdates
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDeletionUpdates
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 supportsDirectEditing
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getActionOverrides
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 generateHTMLOnEdit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fillParserOutput
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
1 / 1
4
 createDifferenceEngine
0.00% covered (danger)
0.00%
0 / 3
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
1<?php
2/**
3 * WikiLambda content handler for Abstract Wiki content objects
4 *
5 * @file
6 * @ingroup Extensions
7 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
8 * @license MIT
9 */
10
11namespace MediaWiki\Extension\WikiLambda\AbstractContent;
12
13use InvalidArgumentException;
14use MediaWiki\Config\Config;
15use MediaWiki\Content\Content;
16use MediaWiki\Content\ContentHandler;
17use MediaWiki\Content\ContentHandlerFactory;
18use MediaWiki\Content\Renderer\ContentParseParams;
19use MediaWiki\Content\ValidationParams;
20use MediaWiki\Context\IContextSource;
21use MediaWiki\Context\RequestContext;
22use MediaWiki\Exception\MWContentSerializationException;
23use MediaWiki\Extension\WikiLambda\UIUtils;
24use MediaWiki\Html\Html;
25use MediaWiki\Logger\LoggerFactory;
26use MediaWiki\Parser\ParserOutput;
27use MediaWiki\Revision\SlotRenderingProvider;
28use MediaWiki\Title\Title;
29use StatusValue;
30use TextSlotDiffRenderer;
31
32class AbstractWikiContentHandler extends ContentHandler {
33
34    // private const ABSTRACTCONTENT_TYPE_WIKIPEDIA = 'Q50081413';
35    // private const ABSTRACTCONTENT_TYPE_DRAFT = 'Q560361';
36    // // XXX: There is not yet a QID for Wiktionary content pages; this is used here as a placeholder
37    // private const ABSTRACTCONTENT_TYPE_WIKTIONARY = 'Q15138389';
38
39    /**
40     * @param string $modelId
41     * @param Config $config
42     * @param ContentHandlerFactory $contentHandlerFactory
43     */
44    public function __construct(
45        $modelId,
46        private readonly Config $config,
47        private readonly ContentHandlerFactory $contentHandlerFactory
48    ) {
49        if ( $modelId !== CONTENT_MODEL_ABSTRACT ) {
50            throw new InvalidArgumentException( __CLASS__ . " initialised for invalid content model" );
51        }
52
53        // Triggers use of message content-model-abstractcontent
54        parent::__construct( CONTENT_MODEL_ABSTRACT, [ CONTENT_FORMAT_TEXT ] );
55    }
56
57    /**
58     * @param Title $title Page to check
59     * @return bool
60     */
61    public function canBeUsedOn( Title $title ) {
62        if ( !$this->config->get( 'WikiLambdaEnableAbstractMode' ) ) {
63            return false;
64        }
65
66        $enabledNamespace = $this->config->get( 'WikiLambdaAbstractNamespaces' );
67        if ( array_key_exists( $title->getNamespace(), $enabledNamespace ) ) {
68            return true;
69        }
70
71        return false;
72    }
73
74    /**
75     * @return AbstractWikiContent
76     */
77    public function makeEmptyContent() {
78        return AbstractWikiContent::makeEmptyContent();
79    }
80
81    /**
82     * @param string $data
83     * @param Title|null $title
84     * @param string|null $modelId
85     * @param string|null $format
86     * @return AbstractWikiContent
87     */
88    public static function makeContent( $data, ?Title $title = null, $modelId = null, $format = null ) {
89        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
90        return parent::makeContent( $data, $title, $modelId, $format );
91    }
92
93    /**
94     * @return string
95     */
96    protected function getContentClass() {
97        return AbstractWikiContent::class;
98    }
99
100    /**
101     * @param Content $content
102     * @param string|null $format
103     * @return string
104     */
105    public function serializeContent( Content $content, $format = null ) {
106        $this->checkFormat( $format );
107
108        if ( !( $content instanceof AbstractWikiContent ) ) {
109            // Throw?
110            return '';
111        }
112
113        return $content->getText();
114    }
115
116    /**
117     * @param string $text
118     * @param string|null $format
119     * @return AbstractWikiContent
120     * @throws MWContentSerializationException if input causes an error
121     */
122    public function unserializeContent( $text, $format = null ) {
123        $class = $this->getContentClass();
124        try {
125            return new $class( $text );
126        } catch ( InvalidArgumentException $error ) {
127            // (T381115) If the passed user input isn't valid, we're expected to throw this particular MW error
128            throw new MWContentSerializationException( $error->getMessage() );
129        }
130    }
131
132    /**
133     * @inheritDoc
134     */
135    public function validateSave( Content $content, ValidationParams $validationParams ) {
136        /** @var AbstractWikiContent $content */
137        '@phan-var AbstractWikiContent $content';
138
139        $title = Title::newFromPageIdentity( $validationParams->getPageIdentity() );
140
141        if ( $content->isValidForTitle( $title ) ) {
142            return StatusValue::newGood();
143        }
144        return $content->getStatus();
145    }
146
147    /**
148     * @inheritDoc
149     */
150    public function getSecondaryDataUpdates(
151        Title $title,
152        Content $content,
153        $role,
154        SlotRenderingProvider $slotOutput
155    ) {
156        return parent::getSecondaryDataUpdates( $title, $content, $role, $slotOutput );
157    }
158
159    /**
160     * @inheritDoc
161     */
162    public function getDeletionUpdates( Title $title, $role ) {
163        return parent::getDeletionUpdates( $title, $role );
164    }
165
166    /**
167     * @inheritDoc
168     */
169    public function supportsDirectEditing() {
170        return true;
171    }
172
173    /**
174     * @inheritDoc
175     */
176    public function getActionOverrides() {
177        return [
178            'edit' => AbstractContentEditAction::class,
179            'history' => AbstractContentHistoryAction::class
180        ];
181    }
182
183    /**
184     * Do not render HTML on edit
185     *
186     * @return bool
187     */
188    public function generateHTMLOnEdit(): bool {
189        return false;
190    }
191
192    /**
193     * Set the HTML and add the appropriate styles.
194     *
195     * @inheritDoc
196     * @param Content $content
197     * @param ContentParseParams $cpoParams
198     * @param ParserOutput &$parserOutput The output object to fill (reference).
199     */
200    protected function fillParserOutput(
201        Content $content,
202        ContentParseParams $cpoParams,
203        ParserOutput &$parserOutput
204    ) {
205        $userLang = RequestContext::getMain()->getLanguage();
206        $logger = LoggerFactory::getInstance( 'WikiLambdaAbstract' );
207
208        // Ensure the stored content is a valid AbstractWikiContent
209        if ( !( $content instanceof AbstractWikiContent ) || !$content->isValid() ) {
210            $parserOutput->setContentHolderText(
211                Html::element(
212                    'div',
213                    [
214                        'class' => [ 'ext-wikilambda-view-invalidcontent', 'warning' ],
215                    ],
216                    wfMessage( 'wikilambda-abstract-invalidcontent' )->inLanguage( $userLang )->text()
217                )
218            );
219            // Exit early, as the rest of the code relies on the stored content being ours.
220            return;
221        }
222
223        // Don't do further work if the requester doesn't want the HTML version generated.
224        if ( !$cpoParams->getGenerateHtml() ) {
225            $parserOutput->setContentHolderText( '' );
226            return;
227        }
228
229        // TODO (T362245): Re-work our code to use PageReferences rather than Titles
230        $pageIdentity = $cpoParams->getPage();
231        $title = Title::castFromPageReference( $pageIdentity );
232        '@phan-var Title $title';
233
234        // Set config variables
235        $wikilambdaConfig = [
236            'abstractContent' => true,
237            'content' => $content->getText(),
238            'createNewPage' => false,
239            'title' => $title->getBaseText(),
240            'page' => $title->getPrefixedDBkey(),
241            'zlang' => $userLang->getCode(),
242            'viewmode' => true
243        ];
244        $parserOutput->setJsConfigVar( 'wgWikiLambda', $wikilambdaConfig );
245
246        // Load styles and Vue app modules
247        $parserOutput->addModuleStyles( [ 'ext.wikilambda.viewpage.styles' ] );
248        $parserOutput->addModules( [ 'ext.wikilambda.app' ] );
249
250        // Build HTML fragments to load Vue app
251        $loadingMessage = wfMessage( 'wikilambda-loading' )->inLanguage( $userLang )->text();
252        $parserOutput->setContentHolderText(
253            // Placeholder div for the Vue template with Codex progress indicator.
254            Html::rawElement(
255                'div',
256                [ 'id' => 'ext-wikilambda-app' ],
257                UIUtils::createCodexProgressIndicator( $loadingMessage )
258            )
259            // Fallback message for users without JavaScript.
260            . Html::rawElement(
261                'noscript',
262                [],
263                wfMessage( 'wikilambda-nojs' )->inLanguage( $userLang )->parse()
264            )
265        );
266    }
267
268    /**
269     * @inheritDoc
270     */
271    public function createDifferenceEngine(
272        IContextSource $context,
273        $oldContentRevisionId = 0,
274        $newContentRevisionId = 0,
275        $recentChangesId = 0,
276        $refreshCache = false,
277        $unhide = false
278    ) {
279        return new AbstractContentDifferenceEngine(
280            $context, $oldContentRevisionId, $newContentRevisionId, $recentChangesId, $refreshCache, $unhide
281        );
282    }
283
284    /**
285     * @inheritDoc
286     *
287     * Access level widened to public for use in AbstractContentDifferenceEngine
288     */
289    public function getSlotDiffRendererWithOptions( IContextSource $context, $options = [] ) {
290        $slotDiffRenderer = $this->contentHandlerFactory
291            ->getContentHandler( CONTENT_MODEL_TEXT )
292            ->getSlotDiffRenderer( $context );
293        '@phan-var TextSlotDiffRenderer $slotDiffRenderer';
294        return $slotDiffRenderer;
295    }
296
297}