Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.06% covered (success)
92.06%
58 / 63
61.54% covered (warning)
61.54%
8 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
WikiPageStore
92.06% covered (success)
92.06%
58 / 63
61.54% covered (warning)
61.54%
8 / 13
24.29
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getConfigurationTitle
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getInfoPageLinkTarget
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeCacheKey
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 fetchJsonBlob
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 loadConfiguration
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 loadConfigurationUncached
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 removeVersionDataFromStatus
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getVersion
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 alwaysStoreConfiguration
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
2.00
 storeConfiguration
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 probablyCanEdit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 definitelyCanEdit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\CommunityConfiguration\Store;
4
5use ApiRawMessage;
6use LogicException;
7use MediaWiki\Content\JsonContent;
8use MediaWiki\Extension\CommunityConfiguration\Store\WikiPage\Writer;
9use MediaWiki\Linker\LinkTarget;
10use MediaWiki\Permissions\Authority;
11use MediaWiki\Permissions\PermissionStatus;
12use MediaWiki\Revision\RevisionLookup;
13use MediaWiki\Revision\RevisionRecord;
14use MediaWiki\Revision\SlotRecord;
15use MediaWiki\Status\Status;
16use MediaWiki\Title\MalformedTitleException;
17use MediaWiki\Title\Title;
18use MediaWiki\Title\TitleFactory;
19use StatusValue;
20use WANObjectCache;
21
22class WikiPageStore extends AbstractJsonStore {
23
24    public const OPTION_EXTRA_TAGS = 'extraTags';
25    private const CACHE_VERSION = 1;
26
27    public const VERSION_FIELD_NAME = '$version';
28    public const TAG_NAME = 'community configuration';
29
30    private ?string $configLocation;
31    private ?Title $configTitle = null;
32    private TitleFactory $titleFactory;
33    private RevisionLookup $revisionLookup;
34    private Writer $writer;
35
36    /**
37     * @param string|null $configLocation
38     * @param WANObjectCache $cache
39     * @param TitleFactory $titleFactory
40     * @param RevisionLookup $revisionLookup
41     * @param Writer $writer
42     */
43    public function __construct(
44        ?string $configLocation,
45        WANObjectCache $cache,
46        TitleFactory $titleFactory,
47        RevisionLookup $revisionLookup,
48        Writer $writer
49    ) {
50        parent::__construct( $cache );
51
52        $this->configLocation = $configLocation;
53        $this->titleFactory = $titleFactory;
54        $this->revisionLookup = $revisionLookup;
55        $this->writer = $writer;
56    }
57
58    /**
59     * @throws MalformedTitleException
60     */
61    public function getConfigurationTitle(): Title {
62        if ( $this->configTitle === null && $this->configLocation ) {
63            $this->configTitle = $this->titleFactory->newFromTextThrow( $this->configLocation );
64        }
65        return $this->configTitle;
66    }
67
68    /**
69     * @inheritDoc
70     */
71    public function getInfoPageLinkTarget(): ?LinkTarget {
72        return $this->getConfigurationTitle();
73    }
74
75    protected function makeCacheKey(): string {
76        $configPage = $this->getConfigurationTitle();
77        return $this->cache->makeKey( __CLASS__,
78            self::CACHE_VERSION,
79            $configPage->getNamespace(), $configPage->getDBkey() );
80    }
81
82    /**
83     * @inheritDoc
84     */
85    protected function fetchJsonBlob(): StatusValue {
86        $configPage = $this->getConfigurationTitle();
87
88        if ( $configPage->isExternal() ) {
89            throw new LogicException( 'Config page should not be external' );
90        }
91
92        $revision = $this->revisionLookup->getRevisionByTitle( $configPage );
93        if ( !$revision ) {
94            // The configuration page does not exist. Pretend it does not contain anything (failure
95            // mode and empty-page behavior is equal, see T325236).
96            // Top-level types different from object will require a corresponding empty value. eg: [] for arrays.
97            return StatusValue::newGood( '{}' );
98        }
99
100        $content = $revision->getContent( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC );
101        if ( !$content instanceof JsonContent ) {
102            return StatusValue::newFatal( new ApiRawMessage(
103                'The configuration title has no content or is not JSON content.'
104            ) );
105        }
106
107        // Do not return the parsed JSON just yet, to ensure each caller gets their own copy of
108        // deserialized data. This needs to happen to avoid cache pollution. See T364101 for more
109        // details.
110        return StatusValue::newGood( $content->getText() );
111    }
112
113    /**
114     * @inheritDoc
115     * @param bool $dropVersion Should version be dropped from the result?
116     */
117    public function loadConfiguration( bool $dropVersion = true ): StatusValue {
118        $result = parent::loadConfiguration();
119        if ( $dropVersion ) {
120            $result = self::removeVersionDataFromStatus( $result );
121        }
122        return $result;
123    }
124
125    /**
126     * @inheritDoc
127     * @param bool $dropVersion Should version be dropped from the result?
128     */
129    public function loadConfigurationUncached( bool $dropVersion = true ): StatusValue {
130        $result = parent::loadConfigurationUncached();
131        if ( $dropVersion ) {
132            $result = self::removeVersionDataFromStatus( $result );
133        }
134        return $result;
135    }
136
137    /**
138     * Remove version data from status returned by the WikiPageStore
139     *
140     * @internal Only public to be used from ValidationHooks
141     * @param StatusValue $status as returned by WikiPageStore::loadConfiguration(Uncached)
142     * @return StatusValue
143     */
144    public static function removeVersionDataFromStatus( StatusValue $status ): StatusValue {
145        $data = $status->getValue();
146        if ( $data ) {
147            unset( $data->{self::VERSION_FIELD_NAME} );
148            $status->setResult( $status->isOK(), $data );
149        }
150        return $status;
151    }
152
153    /**
154     * @inheritDoc
155     */
156    public function getVersion(): ?string {
157        $status = $this->loadConfiguration( false );
158        if ( !$status->isOK() ) {
159            return null;
160        }
161        return $status->getValue()->{self::VERSION_FIELD_NAME} ?? null;
162    }
163
164    /**
165     * @inheritDoc
166     */
167    public function alwaysStoreConfiguration(
168        $config,
169        ?string $version,
170        Authority $authority,
171        string $summary = ''
172    ): StatusValue {
173        if ( $version ) {
174            $config->{self::VERSION_FIELD_NAME} = $version;
175        }
176
177        $status = $this->writer->save(
178            $this->getConfigurationTitle(),
179            $config,
180            $authority,
181            $summary,
182            false,
183            array_merge(
184                [ self::TAG_NAME ],
185                $this->getOption( self::OPTION_EXTRA_TAGS ) ?? []
186            )
187        )->getStatusValue();
188        $this->invalidate();
189        return $status;
190    }
191
192    /**
193     * @inheritDoc
194     */
195    public function storeConfiguration(
196        $config,
197        ?string $version,
198        Authority $authority,
199        string $summary = ''
200    ): StatusValue {
201        $permissionStatus = PermissionStatus::newGood();
202        if ( !$authority->authorizeWrite( 'edit', $this->getConfigurationTitle(), $permissionStatus ) ) {
203            return Status::wrap( $permissionStatus );
204        }
205        return $this->alwaysStoreConfiguration( $config, $version, $authority, $summary );
206    }
207
208    /**
209     * @inheritDoc
210     */
211    public function probablyCanEdit( Authority $authority ): bool {
212        return $authority->probablyCan( 'edit', $this->getConfigurationTitle() );
213    }
214
215    /**
216     * @inheritDoc
217     */
218    public function definitelyCanEdit( Authority $authority ): bool {
219        return $authority->definitelyCan( 'edit', $this->getConfigurationTitle() );
220    }
221}