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