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