Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.40% covered (success)
90.40%
113 / 125
80.77% covered (warning)
80.77%
21 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZObjectContent
90.40% covered (success)
90.40%
113 / 125
80.77% covered (warning)
80.77%
21 / 26
50.04
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
4
 validateFields
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
7
 validateContent
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 isValid
100.00% covered (success)
100.00%
3 / 3
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
 getErrors
100.00% covered (success)
100.00%
1 / 1
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
 getZObject
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInnerZObject
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getZid
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getTypeString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTypeStringAndLanguage
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
2
 getZType
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getZValue
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getLabels
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getLabel
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getAliases
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getTextForSearchIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 copy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 convert
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 isCountable
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * WikiLambda ZObjectContent
4 *
5 * @file
6 * @ingroup Extensions
7 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
8 * @license MIT
9 */
10
11namespace MediaWiki\Extension\WikiLambda;
12
13use MediaWiki\Content\AbstractContent;
14use MediaWiki\Content\TextContent;
15use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry;
16use MediaWiki\Extension\WikiLambda\ZObjects\ZError;
17use MediaWiki\Extension\WikiLambda\ZObjects\ZMultiLingualString;
18use MediaWiki\Extension\WikiLambda\ZObjects\ZMultiLingualStringSet;
19use MediaWiki\Extension\WikiLambda\ZObjects\ZObject;
20use MediaWiki\Extension\WikiLambda\ZObjects\ZPersistentObject;
21use MediaWiki\Json\FormatJson;
22use MediaWiki\Language\Language;
23use MediaWiki\MediaWikiServices;
24use MediaWiki\Status\Status;
25use MediaWiki\Title\Title;
26use MessageLocalizer;
27
28/**
29 * This class represents the wrapper for a ZObject, as stored in MediaWiki. Though its form is
30 * intentionally similar to that of a ZObject, representing a Z2/Persistent ZObject, it has several
31 * differences to account for the Content hierarchy and how it serves as the bridge between
32 * MediaWiki 'real' content and the functional model.
33 */
34class ZObjectContent extends AbstractContent {
35
36    // Define the constant for the description and label length limit
37    // TODO (T371882) Set these constants to their final values: 200 and 50
38    private const DESCRIPTION_LENGTH_LIMIT = 500;
39    private const LABEL_LENGTH_LIMIT = 500;
40
41    /**
42     * Fundamental internal representation, as stored in MediaWiki.
43     *
44     * In practice, this currently is a JSON stringification of the object model. However, this
45     * is an implementation detail, and in future might change; it should not be relied upon.
46     *
47     * @var string
48     */
49    private $text;
50
51    /**
52     * Object representation of the content, as specified in the Functional Model.
53     *
54     * @var \stdClass
55     */
56    private $object;
57
58    /**
59     * ZPersistentObject that wraps the ZObject represented in this content object.
60     *
61     * @var ZPersistentObject|null
62     */
63    private $zobject = null;
64
65    /**
66     * @var Status|null
67     */
68    private $status = null;
69
70    /**
71     * @var ZError
72     */
73    private $error = null;
74
75    /**
76     * Builds the Content object that can be saved in the Wiki
77     * Doesn't validate the ZObject, but checks for syntactical validity
78     *
79     * @param string $text
80     * @throws ZErrorException
81     */
82    public function __construct( $text ) {
83        // Some unit tests somehow don't load our constant by this point, so defensively provide it as needed.
84        $ourModel = defined( 'CONTENT_MODEL_ZOBJECT' ) ? CONTENT_MODEL_ZOBJECT : 'zobject';
85        parent::__construct( $ourModel );
86
87        // Check that the input is a valid string
88        if ( !is_string( $text ) ) {
89            throw new ZErrorException(
90                ZErrorFactory::createZErrorInstance(
91                    ZErrorTypeRegistry::Z_ERROR_INVALID_FORMAT, [
92                        'data' => $text
93                    ] )
94            );
95        }
96
97        // Check that the input is a valid JSON
98        $parseStatus = FormatJson::parse( $text );
99        if ( !$parseStatus->isGood() ) {
100            $errorMessage = wfMessage( $parseStatus->getErrors()[0]['message'] )->inContentLanguage()->text();
101            throw new ZErrorException(
102                ZErrorFactory::createZErrorInstance(
103                    ZErrorTypeRegistry::Z_ERROR_INVALID_JSON, [
104                        'message' => $errorMessage,
105                        'data' => $text
106                    ] )
107            );
108        }
109
110        // Save the string and object content
111        // TODO (T284473): We might not need the text content once we have proper diffs
112        $this->text = $text;
113        $this->object = $parseStatus->getValue();
114    }
115
116    /**
117     * Validate fields and throws a ZErrorException if any of the following validations
118     * don't pass successfully:
119     * - ZPersistentObject description: validate the length of the description field
120     * - ZPersistentObject name: validate the length of the name field
121     *
122     * @param MessageLocalizer $context The context of the action operation, for localisation of messages
123     * @throws ZErrorException
124     */
125    public function validateFields( $context ) {
126        $invalidDescriptionLanguages = $this
127            ->getZObject()
128            ->validateDescriptionLength( self::DESCRIPTION_LENGTH_LIMIT, $context );
129        $invalidLabelLanguages = $this
130            ->getZObject()
131            ->validateLabelLength( self::LABEL_LENGTH_LIMIT, $context );
132
133        $errors = [];
134        if ( count( $invalidDescriptionLanguages ) > 0 ) {
135            foreach ( $invalidDescriptionLanguages as $invalidLang ) {
136                // Create message for language and limit
137                $message = wfMessage( 'wikilambda-validation-error-description-toolong',
138                    $invalidLang, self::DESCRIPTION_LENGTH_LIMIT )->text();
139                // Create Z500 and add to list
140                $errors[] = ZErrorFactory::createZErrorInstance(
141                    ZErrorTypeRegistry::Z_ERROR_UNKNOWN,
142                    [ 'message' => $message ]
143                );
144            }
145        }
146
147        if ( count( $invalidLabelLanguages ) > 0 ) {
148            foreach ( $invalidLabelLanguages as $invalidLang ) {
149                // Create message for language and limit
150                $message = wfMessage( 'wikilambda-validation-error-name-toolong',
151                    $invalidLang, self::LABEL_LENGTH_LIMIT )->text();
152                // Create Z500 and add to list
153                $errors[] = ZErrorFactory::createZErrorInstance(
154                    ZErrorTypeRegistry::Z_ERROR_UNKNOWN,
155                    [ 'message' => $message ]
156                );
157            }
158        }
159
160        if ( count( $errors ) > 0 ) {
161            // Return Z500 if there's only one error
162            // Return Z509 if there are more than one
163            $error = ( count( $errors ) > 1 ) ? ZErrorFactory::createZErrorList( $errors ) : $errors[ 0 ];
164            throw new ZErrorException( $error );
165        }
166    }
167
168    /**
169     * Tries to build the wrapped ZObject by calling ZObjectFactory::createPersistentContent()
170     * and sets the resulting validation status on $this->status
171     */
172    private function validateContent() {
173        $this->status = new Status();
174        try {
175            $this->zobject = ZObjectFactory::createPersistentContent( $this->getObject() );
176        } catch ( ZErrorException $e ) {
177            // TODO (T362236): Add the rendering language as a parameter, don't default to English
178            $this->status->fatal( $e->getMessage() );
179            $this->error = $e->getZError();
180        }
181    }
182
183    /**
184     * @inheritDoc
185     */
186    public function isValid() {
187        if ( !( $this->status instanceof Status ) ) {
188            $this->validateContent();
189        }
190        return $this->status->isOK();
191    }
192
193    /**
194     * @return Status|null
195     */
196    public function getStatus() {
197        return $this->status;
198    }
199
200    /**
201     * @return ZError
202     */
203    public function getErrors() {
204        return $this->error;
205    }
206
207    /**
208     * @inheritDoc
209     */
210    public function getText() {
211        return $this->text;
212    }
213
214    /**
215     * @return \stdClass
216     */
217    public function getObject() {
218        return $this->object;
219    }
220
221    /**
222     * @return ZPersistentObject|null
223     */
224    public function getZObject() {
225        return $this->zobject;
226    }
227
228    /**
229     * Wrapper for ZPersistentObject getInnerZObject method. Returns the inner ZObject.
230     *
231     * @return ZObject
232     * @throws ZErrorException
233     */
234    public function getInnerZObject(): ZObject {
235        if ( !$this->isValid() ) {
236            throw new ZErrorException( $this->error );
237        }
238        return $this->zobject->getInnerZObject();
239    }
240
241    /**
242     * Wrapper for ZPersistentObject getZid method. Returns the Zid of the persistent object
243     *
244     * @return string The persisted (or null) Zid
245     * @throws ZErrorException
246     */
247    public function getZid() {
248        if ( !$this->isValid() ) {
249            throw new ZErrorException( $this->error );
250        }
251        return $this->zobject->getZid();
252    }
253
254    /**
255     * String representation of the type of this ZObject
256     *
257     * @param Language $language Language in which to provide the string.
258     * @return string
259     * @throws ZErrorException
260     */
261    public function getTypeString( $language ): string {
262        return $this->getTypeStringAndLanguage( $language )[ 'type' ];
263    }
264
265    /**
266     * Two string representations (and the language code of that representation) of this ZObject
267     *
268     * @param Language $language Language in which to provide the string.
269     * @return array An array containing the following keys (all in string form):
270     *         title => the label of the type
271     *         type => which is the label of the type and the zCode of the type
272     *         languageCode
273     * @throws ZErrorException
274     */
275    public function getTypeStringAndLanguage( $language ) {
276        $type = $this->getZType();
277        $typeTitle = Title::newFromText( $type, NS_MAIN );
278        $zObjectStore = WikiLambdaServices::getZObjectStore();
279        $typeObject = $zObjectStore->fetchZObjectByTitle( $typeTitle );
280
281        $chosenLang = $language;
282
283        if ( $typeObject ) {
284            $labelAndLang = $typeObject->getLabels()->buildStringForLanguage( $language )
285                ->fallbackWithEnglish()
286                ->placeholderNoFallback()
287                ->getStringAndLanguageCode();
288            $label = $labelAndLang[ 'title' ];
289            $chosenLang = $labelAndLang[ 'languageCode' ];
290        } else {
291            $label = wfMessage( 'wikilambda-typeunavailable' )->inContentLanguage()->text();
292        }
293
294        return [
295            'title' => $label,
296            'type' => $label
297                // (T356731) The language for word-separator and parentheses interface messages
298                // must be consistent, otherwise the word-separator would add unneeded whitespace
299                // when the parentheses is the full-width form for languages including zh-hans,
300                // zh-hant, etc.
301                . wfMessage( 'word-separator' )->text()
302                . wfMessage( 'parentheses' )->rawParams( $this->getZType() )->text(),
303            'languageCode' => $chosenLang
304        ];
305    }
306
307    /**
308     * Wrapper for ZPersistentObject getInternalZType method. Returns the ZType of the internal ZObject.
309     *
310     * @return string
311     * @throws ZErrorException
312     */
313    public function getZType(): string {
314        if ( !$this->isValid() ) {
315            throw new ZErrorException( $this->error );
316        }
317        return $this->zobject->getInternalZType();
318    }
319
320    /**
321     * Wrapper for ZPersistentObject getZValue method. Returns the value of the internal ZObject.
322     *
323     * @return mixed
324     * @throws ZErrorException
325     */
326    public function getZValue() {
327        if ( !$this->isValid() ) {
328            throw new ZErrorException( $this->error );
329        }
330        return $this->zobject->getZValue();
331    }
332
333    /**
334     * Wrapper for ZPersistentObject getLabels method. Returns the labels of the ZPersistentObject.
335     *
336     * @return ZMultilingualString
337     * @throws ZErrorException
338     */
339    public function getLabels(): ZMultiLingualString {
340        if ( !$this->isValid() ) {
341            throw new ZErrorException( $this->error );
342        }
343        return $this->zobject->getLabels();
344    }
345
346    /**
347     * Wrapper for ZPersistentObject getLabel method. Returns the label for a given Language (or its fallback).
348     *
349     * @param Language $language Language in which to provide the label.
350     * @return ?string
351     * @throws ZErrorException
352     */
353    public function getLabel( $language ): ?string {
354        if ( !$this->isValid() ) {
355            throw new ZErrorException( $this->error );
356        }
357        return $this->zobject->getLabel( $language );
358    }
359
360    /**
361     * Wrapper for ZPersistentObject getAliases method. Returns the aliases of the ZPersistentObject.
362     *
363     * @return ZMultiLingualStringSet
364     * @throws ZErrorException
365     */
366    public function getAliases(): ZMultiLingualStringSet {
367        if ( !$this->isValid() ) {
368            throw new ZErrorException( $this->error );
369        }
370        return $this->zobject->getAliases();
371    }
372
373    /**
374     * @inheritDoc
375     */
376    public function getTextForSearchIndex() {
377        // TODO (T271963): We'll probably want to inject something special for search with facets/etc.
378        return $this->getText();
379    }
380
381    /**
382     * @inheritDoc
383     */
384    public function getWikitextForTransclusion() {
385        // ZObject pages are not transcludable.
386        return false;
387    }
388
389    /**
390     * @inheritDoc
391     */
392    public function getTextForSummary( $maxLength = 250 ) {
393        // TODO (T362246): Dependency-inject
394        $contentLanguage = MediaWikiServices::getInstance()->getContentLanguage();
395        // Splice out newlines from content.
396        $textWithoutNewlines = preg_replace( "/[\n\r]/", ' ', $this->getText() );
397        $trimLength = max( 0, $maxLength );
398        return $contentLanguage->truncateForDatabase( $textWithoutNewlines, $trimLength );
399    }
400
401    /**
402     * @inheritDoc
403     */
404    public function getNativeData() {
405        return $this->getObject();
406    }
407
408    /**
409     * @inheritDoc
410     */
411    public function getSize() {
412        return strlen( $this->getText() );
413    }
414
415    /**
416     * @inheritDoc
417     */
418    public function copy() {
419        // We're immutable.
420        return $this;
421    }
422
423    /**
424     * @inheritDoc
425     */
426    public function convert( $toModel, $lossy = '' ) {
427        if ( $toModel === CONTENT_MODEL_ZOBJECT ) {
428            return $this;
429        }
430
431        if ( $toModel === CONTENT_MODEL_TEXT ) {
432            return new TextContent( $this->text );
433        }
434
435        return false;
436    }
437
438    /**
439     * @inheritDoc
440     */
441    public function isCountable( $hasLinks = null ) {
442        // TODO (T362246): Dependency-inject
443        $config = MediaWikiServices::getInstance()->getMainConfig();
444
445        return (
446            !$this->isRedirect()
447            && ( $config->get( 'ArticleCountMethod' ) === 'any' )
448        );
449    }
450}