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