Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.50% covered (warning)
72.50%
58 / 80
40.00% covered (danger)
40.00%
6 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractWikiContent
72.50% covered (warning)
72.50%
58 / 80
40.00% covered (danger)
40.00%
6 / 15
78.69
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 makeEmptyContent
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getObject
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTextForSearchIndex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWikitextForTransclusion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTextForSummary
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getNativeData
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getSize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 copy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 convert
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 isCountable
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 isValid
94.44% covered (success)
94.44%
34 / 36
0.00% covered (danger)
0.00%
0 / 1
21.08
 isValidForTitle
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getStatus
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * WikiLambda AbstractWikiContent
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\Content\AbstractContent;
15use MediaWiki\Content\TextContent;
16use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry;
17use MediaWiki\Json\FormatJson;
18use MediaWiki\MediaWikiServices;
19use MediaWiki\Title\Title;
20use StatusValue;
21
22/**
23 * This class represents the wrapper for an Abstract Wiki content object, as stored in MediaWiki.
24 */
25class AbstractWikiContent extends AbstractContent {
26
27    private string $text;
28    private ?\stdClass $object;
29    private ?StatusValue $status = null;
30
31    public const ABSTRACTCONTENT_SECTION_LEDE = 'Q8776414';
32
33    /**
34     * Builds the Content object that can be saved in the Wiki
35     *
36     * @param string $text
37     * @throws InvalidArgumentException
38     */
39    public function __construct( $text ) {
40        // Some unit tests somehow don't load our constant by this point, so defensively provide it as needed.
41        $ourModel = defined( 'CONTENT_MODEL_ABSTRACT' ) ? CONTENT_MODEL_ABSTRACT : 'abstractwiki';
42        parent::__construct( $ourModel );
43
44        // Check that the input is a valid string
45        if ( !is_string( $text ) ) {
46            throw new InvalidArgumentException(
47                'Cannot create AbstractWikiContent from a non-string: ' . gettype( $text )
48            );
49        }
50
51        $this->text = $text;
52
53        // Check that the input is a valid JSON
54        $parseStatus = FormatJson::parse( $text );
55        if ( !$parseStatus->isGood() ) {
56            $errorMessage = wfMessage( $parseStatus->getErrors()[0]['message'] )->inContentLanguage()->text();
57            throw new InvalidArgumentException( $errorMessage );
58        }
59
60        $this->object = $parseStatus->getValue();
61    }
62
63    /**
64     * @return AbstractWikiContent
65     */
66    public static function makeEmptyContent(): AbstractWikiContent {
67        // Note: This is not the specification in Z25, but an intentional deviation from for simplicity.
68        $blankContent = <<<EOD
69{
70    "qid": "Q0",
71    "sections": {
72        "Q8776414": {
73            "index": 0,
74            "fragments": [ "Z89" ]
75        }
76    }
77}
78EOD;
79
80        return new self( $blankContent );
81    }
82
83    /**
84     * @return string
85     */
86    public function getText() {
87        return $this->text;
88    }
89
90    /**
91     * @return ?\stdClass
92     */
93    public function getObject() {
94        return $this->object;
95    }
96
97    /**
98     * @inheritDoc
99     */
100    public function getTextForSearchIndex() {
101        // TODO (T271963): We'll probably want to inject something special for search with facets/etc.
102        return $this->getText();
103    }
104
105    /**
106     * @inheritDoc
107     */
108    public function getWikitextForTransclusion() {
109        // Abstract Wiki pages are not transcludable.
110        return false;
111    }
112
113    /**
114     * @inheritDoc
115     */
116    public function getTextForSummary( $maxLength = 250 ) {
117        // TODO (T362246): Dependency-inject
118        $contentLanguage = MediaWikiServices::getInstance()->getContentLanguage();
119        // Splice out newlines from content.
120        $textWithoutNewlines = preg_replace( "/[\n\r]/", ' ', $this->getText() );
121        $trimLength = max( 0, $maxLength );
122        return $contentLanguage->truncateForDatabase( $textWithoutNewlines, $trimLength );
123    }
124
125    /**
126     * @inheritDoc
127     * @deprecated since 1.33 Use ::getObject() instead.
128     */
129    public function getNativeData() {
130        wfDeprecated( __METHOD__, '1.33' );
131        return $this->getObject();
132    }
133
134    /**
135     * @inheritDoc
136     */
137    public function getSize() {
138        return strlen( $this->getText() );
139    }
140
141    /**
142     * @inheritDoc
143     */
144    public function copy() {
145        // We're immutable.
146        return $this;
147    }
148
149    /**
150     * @inheritDoc
151     */
152    public function convert( $toModel, $lossy = '' ) {
153        if ( $toModel === CONTENT_MODEL_ABSTRACT ) {
154            return $this;
155        }
156
157        if ( $toModel === CONTENT_MODEL_TEXT ) {
158            return new TextContent( $this->text );
159        }
160
161        return false;
162    }
163
164    /**
165     * @inheritDoc
166     */
167    public function isCountable( $hasLinks = null ) {
168        // TODO (T362246): Dependency-inject
169        $config = MediaWikiServices::getInstance()->getMainConfig();
170
171        return (
172            !$this->isRedirect()
173            && ( $config->get( 'ArticleCountMethod' ) === 'any' )
174        );
175    }
176
177    /**
178     * @inheritDoc
179     */
180    public function isValid() {
181        // Content exists and is the right type
182        if ( !isset( $this->object ) ) {
183            $this->status = StatusValue::newFatal( 'wikilambda-abstract-error-invalid-json' );
184            return false;
185        }
186
187        // Qid exists, is a string, and has the shape of a qid.
188        // Also consider null (Q0) qid as valid, as empty content objects must pass validation
189        if (
190            !isset( $this->object->qid ) || !is_string( $this->object->qid ) ||
191            ( !AbstractContentUtils::isValidWikidataItemReference( $this->object->qid ) &&
192            !AbstractContentUtils::isNullWikidataItemReference( $this->object->qid ) )
193        ) {
194            $badQid = $this->object->qid ?? null;
195            $badQid = is_string( $badQid ) ? $badQid : var_export( $badQid, true );
196            $this->status = StatusValue::newFatal( 'wikilambda-abstract-error-bad-qid', $badQid );
197            return false;
198        }
199
200        // Sections exists and is an object
201        if ( !isset( $this->object->sections ) || !is_object( $this->object->sections ) ) {
202            $this->status = StatusValue::newFatal( 'wikilambda-abstract-error-missing-sections' );
203            return false;
204        }
205
206        // Sections must contain a lede section
207        if ( !property_exists( $this->object->sections, self::ABSTRACTCONTENT_SECTION_LEDE ) ) {
208            $this->status = StatusValue::newFatal( 'wikilambda-abstract-error-missing-lede-section' );
209            return false;
210        }
211
212        // For each section:
213        foreach ( get_object_vars( $this->object->sections ) as $key => $section ) {
214            // Section key must be a valid qid
215            if ( !AbstractContentUtils::isValidWikidataItemReference( $key ) ) {
216                $this->status = StatusValue::newFatal( 'wikilambda-abstract-error-bad-section-qid', $key );
217                return false;
218            }
219
220            // Section must be an object
221            if ( !is_object( $section ) ) {
222                $this->status = StatusValue::newFatal( 'wikilambda-abstract-error-bad-section-content', $key );
223                return false;
224            }
225
226            // Section index must have a positive integer
227            if ( !isset( $section->index ) || !is_int( $section->index ) || $section->index < 0 ) {
228                $badIndex = $this->object->index ?? (string)null;
229                $badIndex = is_string( $badIndex ) ? $badIndex : var_export( $badIndex, true );
230                $this->status = StatusValue::newFatal( 'wikilambda-abstract-error-bad-section-index', $key, $badIndex );
231                return false;
232            }
233
234            // Section fragments must exist and contain an array
235            if ( !isset( $section->fragments ) || !is_array( $section->fragments ) ) {
236                $this->status = StatusValue::newFatal( 'wikilambda-abstract-error-missing-section-fragments', $key );
237                return false;
238            }
239
240            // Section fragments must be a benjamin array of HTML/Z89 objects
241            if ( count( $section->fragments ) === 0 || $section->fragments[0] !== ZTypeRegistry::Z_HTML_FRAGMENT ) {
242                $this->status = StatusValue::newFatal( 'wikilambda-abstract-error-bad-fragments-type', $key );
243                return false;
244            }
245        }
246
247        // Passed all checks
248        $this->status = StatusValue::newGood();
249        return true;
250    }
251
252    /**
253     * Whether the content is valid for a given title.
254     *
255     * This method only checks whether a given title matches the internal
256     * internal qid property of the content. If that passes, then we
257     * call isValid(), which checks for content validity, independently
258     * from the page identity.
259     *
260     * @param Title $title
261     * @return bool
262     */
263    public function isValidForTitle( Title $title ) {
264        // title is the same as object->qid
265        $innerQid = $this->object->qid;
266        $titleQid = $title->getBaseText();
267        if ( $innerQid !== $titleQid ) {
268            $this->status = StatusValue::newFatal( 'wikilambda-abstract-error-unmatching-qid', $innerQid, $titleQid );
269            return false;
270        }
271
272        return $this->isValid();
273    }
274
275    /**
276     * @return StatusValue|null
277     */
278    public function getStatus() {
279        return $this->status;
280    }
281}