Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.54% covered (success)
92.54%
62 / 67
80.00% covered (warning)
80.00%
8 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
CampaignContent
92.54% covered (success)
92.54%
62 / 67
80.00% covered (warning)
80.00%
8 / 10
23.22
0.00% covered (danger)
0.00%
0 / 1
 getGlobalConfigAnchorLinkTarget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 copyWithNewText
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 overrideValidationStatus
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 setServices
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 initServices
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getValidationStatus
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 isValid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getData
77.78% covered (warning)
77.78%
14 / 18
0.00% covered (danger)
0.00%
0 / 1
4.18
 newCampaignRecord
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
8.01
1<?php
2
3namespace MediaWiki\Extension\MediaUploader\Campaign;
4
5use MediaWiki\Extension\MediaUploader\MediaUploaderServices;
6use MediaWiki\Linker\LinkTarget;
7use MediaWiki\Page\PageReference;
8use MWException;
9use Status;
10use Symfony\Component\Yaml\Exception\ParseException;
11use Symfony\Component\Yaml\Yaml;
12use TextContent;
13use TitleValue;
14
15/**
16 * Represents the configuration of an Upload Campaign
17 */
18class CampaignContent extends TextContent {
19
20    /**
21     * DB key of the page where the global config is anchored.
22     * The page is always in the Campaign: namespace.
23     *
24     * This page records the templates used by the global config, which allows
25     * the config to be reparsed when any of the used templates change.
26     */
27    public const GLOBAL_CONFIG_ANCHOR_DBKEY = '-';
28
29    public const MODEL_ID = 'Campaign';
30
31    public static function getGlobalConfigAnchorLinkTarget(): LinkTarget {
32        return new TitleValue( NS_CAMPAIGN, self::GLOBAL_CONFIG_ANCHOR_DBKEY );
33    }
34
35    /** @var Validator */
36    private $validator;
37
38    /** @var Status */
39    private $yamlParse;
40
41    /** @var Status */
42    private $realYamlParse;
43
44    /** @var Status */
45    private $validationStatus;
46
47    /** @var Status */
48    private $realValidationStatus;
49
50    /** @var bool Whether the services were initialized */
51    private $initializedServices = false;
52
53    /**
54     * CampaignContent constructor.
55     *
56     * See CampaignContentHandler::preSaveTransform for a usage of the second and
57     * third arguments.
58     *
59     * @param string $text
60     *
61     * @throws MWException
62     */
63    public function __construct( string $text ) {
64        parent::__construct( $text, CONTENT_MODEL_CAMPAIGN );
65    }
66
67    /**
68     * Make a copy of this content instance with new text.
69     *
70     * This carries on the services and validation statuses.
71     *
72     * @param string $text
73     *
74     * @return CampaignContent
75     * @throws MWException
76     */
77    public function copyWithNewText( string $text ): CampaignContent {
78        $content = new CampaignContent( $text );
79
80        // Carry on the validation statuses
81        $content->yamlParse = $this->yamlParse;
82        $content->validationStatus = $this->validationStatus;
83        $content->realYamlParse = $this->realYamlParse;
84        $content->realValidationStatus = $this->realValidationStatus;
85
86        // And the services as well
87        $content->setServices( $this->validator );
88        return $content;
89    }
90
91    /**
92     * Overrides the parsing and schema checks. Should only be used when saving an edit by the system user.
93     */
94    public function overrideValidationStatus() {
95        $this->realValidationStatus = $this->getValidationStatus();
96        $this->realYamlParse = $this->getData();
97        $this->yamlParse = Status::newGood();
98        $this->validationStatus = $this->yamlParse;
99    }
100
101    /**
102     * Set services for unit testing purposes.
103     *
104     * @param Validator|null $validator
105     */
106    public function setServices( Validator $validator = null ) {
107        $this->validator = $validator;
108        $this->initializedServices = true;
109    }
110
111    /**
112     * Initialize services from global state.
113     */
114    private function initServices() {
115        if ( $this->initializedServices ) {
116            return;
117        }
118
119        $this->setServices(
120            MediaUploaderServices::getCampaignValidator()
121        );
122    }
123
124    /**
125     * Checks user input YAML to make sure that it produces a valid campaign object.
126     *
127     * @return Status
128     */
129    public function getValidationStatus(): Status {
130        $this->initServices();
131
132        if ( $this->validationStatus ) {
133            return $this->validationStatus;
134        }
135
136        // First, check if the syntax is valid
137        $yamlParse = $this->getData();
138        if ( !$yamlParse->isGood() ) {
139            $this->validationStatus = $yamlParse;
140            return $this->validationStatus;
141        }
142
143        $this->validationStatus = $this->validator->validate(
144            $yamlParse->getValue()
145        );
146
147        return $this->validationStatus;
148    }
149
150    /**
151     * @return bool Whether content validates against campaign JSON Schema.
152     */
153    public function isValid() {
154        return $this->getValidationStatus()->isGood();
155    }
156
157    /**
158     * Returns the data contained on the page in array representation.
159     * The value is wrapped in the Status object.
160     *
161     * The data is guaranteed to come from a syntactically valid YAML, but may
162     * not validate against the schema. Use isValid() to check if it does.
163     *
164     * @return Status
165     */
166    public function getData(): Status {
167        if ( $this->yamlParse ) {
168            return $this->yamlParse;
169        }
170
171        try {
172            $data = Yaml::parse(
173                $this->getText(),
174                Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE
175            );
176            if ( is_array( $data ) ) {
177                $this->yamlParse = Status::newGood( $data );
178            } else {
179                $this->yamlParse = Status::newFatal(
180                    'mediauploader-yaml-parse-error',
181                    'unknown error'
182                );
183            }
184            return $this->yamlParse;
185        } catch ( ParseException $e ) {
186            return Status::newFatal(
187                'mediauploader-yaml-parse-error',
188                $e->getMessage()
189            );
190        }
191    }
192
193    /**
194     * @param PageReference $page
195     * @param int|null $pageId
196     *
197     * @return CampaignRecord
198     */
199    public function newCampaignRecord( PageReference $page, int $pageId = null ): CampaignRecord {
200        $yamlParse = $this->realYamlParse ?: $this->getData();
201        if ( !$yamlParse->isGood() ) {
202            $validity = CampaignRecord::CONTENT_INVALID_FORMAT;
203        } else {
204            $status = $this->realValidationStatus ?: $this->getValidationStatus();
205            if ( !$status->isGood() ) {
206                $validity = CampaignRecord::CONTENT_INVALID_SCHEMA;
207            } else {
208                $validity = CampaignRecord::CONTENT_VALID;
209            }
210        }
211
212        $content = $yamlParse->getValue();
213        // Content can be null, when YAML is invalid and we're force-saving
214        // with the system user. Fall back to empty array, so that the config
215        // factory doesn't do a backflip.
216        if ( $content === null && $validity === CampaignRecord::CONTENT_VALID ) {
217            $content = [];
218        }
219
220        return new CampaignRecord(
221            $pageId,
222            ( $content ?: [] )['enabled'] ?? false,
223            $validity,
224            $content,
225            $page
226        );
227    }
228}