Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
92.19% |
59 / 64 |
|
62.50% |
5 / 8 |
CRAP | |
0.00% |
0 / 1 |
WikiPageConfigLoader | |
92.19% |
59 / 64 |
|
62.50% |
5 / 8 |
18.15 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
makeCacheKey | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
invalidate | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
load | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
3.01 | |||
loadFromWanCache | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
removeCustomFlags | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
loadUncached | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
fetchConfig | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
6.01 |
1 | <?php |
2 | |
3 | namespace AutoModerator\Config; |
4 | |
5 | use AutoModerator\Config\Validation\ConfigValidatorFactory; |
6 | use AutoModerator\Util; |
7 | use MediaWiki\Api\ApiRawMessage; |
8 | use MediaWiki\Content\JsonContent; |
9 | use MediaWiki\Http\HttpRequestFactory; |
10 | use MediaWiki\Json\FormatJson; |
11 | use MediaWiki\Linker\LinkTarget; |
12 | use MediaWiki\Revision\RevisionLookup; |
13 | use MediaWiki\Revision\RevisionRecord; |
14 | use MediaWiki\Revision\SlotRecord; |
15 | use MediaWiki\Title\TitleFactory; |
16 | use MediaWiki\Utils\UrlUtils; |
17 | use StatusValue; |
18 | use Wikimedia\LightweightObjectStore\ExpirationAwareness; |
19 | use Wikimedia\ObjectCache\HashBagOStuff; |
20 | use Wikimedia\ObjectCache\WANObjectCache; |
21 | use Wikimedia\Rdbms\DBAccessObjectUtils; |
22 | use Wikimedia\Rdbms\IDBAccessObject; |
23 | |
24 | /** |
25 | * This class allows callers to fetch various variables |
26 | * from JSON pages stored on-wiki (the pages need to have JSON |
27 | * as their content model). |
28 | * |
29 | * The MediaWiki pages need to be formatted like this: |
30 | * { |
31 | * "ConfigVariable": "value", |
32 | * "OtherConfigVariable": "value" |
33 | * } |
34 | */ |
35 | class WikiPageConfigLoader implements ICustomReadConstants { |
36 | |
37 | private ConfigValidatorFactory $configValidatorFactory; |
38 | private HttpRequestFactory $requestFactory; |
39 | private RevisionLookup $revisionLookup; |
40 | private TitleFactory $titleFactory; |
41 | private WANObjectCache $cache; |
42 | private HashBagOStuff $inProcessCache; |
43 | private UrlUtils $urlUtils; |
44 | /** |
45 | * @var bool Hack to disable DB access in non-database tests. The proper replacement to this would be a |
46 | * NullConfigLoader or similar class, and the ServiceWiring code would determine which one to use. |
47 | */ |
48 | private bool $isTestWithStorageDisabled; |
49 | |
50 | /** |
51 | * @param WANObjectCache $cache |
52 | * @param ConfigValidatorFactory $configValidatorFactory |
53 | * @param HttpRequestFactory $requestFactory |
54 | * @param RevisionLookup $revisionLookup |
55 | * @param TitleFactory $titleFactory |
56 | * @param UrlUtils $urlUtils |
57 | * @param bool $isTestWithStorageDisabled |
58 | */ |
59 | public function __construct( |
60 | WANObjectCache $cache, |
61 | ConfigValidatorFactory $configValidatorFactory, |
62 | HttpRequestFactory $requestFactory, |
63 | RevisionLookup $revisionLookup, |
64 | TitleFactory $titleFactory, |
65 | UrlUtils $urlUtils, |
66 | bool $isTestWithStorageDisabled |
67 | ) { |
68 | $this->cache = $cache; |
69 | $this->inProcessCache = new HashBagOStuff(); |
70 | $this->configValidatorFactory = $configValidatorFactory; |
71 | $this->requestFactory = $requestFactory; |
72 | $this->revisionLookup = $revisionLookup; |
73 | $this->titleFactory = $titleFactory; |
74 | $this->urlUtils = $urlUtils; |
75 | $this->isTestWithStorageDisabled = $isTestWithStorageDisabled; |
76 | } |
77 | |
78 | /** |
79 | * @param LinkTarget $configPage |
80 | * @return string |
81 | */ |
82 | private function makeCacheKey( LinkTarget $configPage ): string { |
83 | return $this->cache->makeKey( 'AutoModerator', |
84 | 'config', $configPage->getNamespace(), $configPage->getDBkey() ); |
85 | } |
86 | |
87 | /** |
88 | * @param LinkTarget $configPage |
89 | */ |
90 | public function invalidate( LinkTarget $configPage ) { |
91 | $cacheKey = $this->makeCacheKey( $configPage ); |
92 | $this->cache->delete( $cacheKey ); |
93 | $this->inProcessCache->delete( $cacheKey ); |
94 | } |
95 | |
96 | /** |
97 | * Load the configured page, with caching. |
98 | * @param LinkTarget $configPage |
99 | * @param int $flags bit field, see IDBAccessObject::READ_XXX |
100 | * @return array|StatusValue The content of the configuration page (as JSON |
101 | * data in PHP-native format), or a StatusValue on error. |
102 | */ |
103 | public function load( LinkTarget $configPage, int $flags = 0 ) { |
104 | if ( |
105 | DBAccessObjectUtils::hasFlags( $flags, IDBAccessObject::READ_LATEST ) || |
106 | // This is a custom flag, but bitfield logic should work regardless. |
107 | DBAccessObjectUtils::hasFlags( $flags, self::READ_UNCACHED ) |
108 | ) { |
109 | // User does not want to use cached data, invalidate the cache. |
110 | $this->invalidate( $configPage ); |
111 | } |
112 | |
113 | // WANObjectCache has an in-process cache (pcTTL), but it is not subject |
114 | // to invalidation, which breaks WikiPageConfigLoaderTest. |
115 | return $this->inProcessCache->getWithSetCallback( |
116 | $this->makeCacheKey( $configPage ), |
117 | ExpirationAwareness::TTL_INDEFINITE, |
118 | function () use ( $configPage, $flags ) { |
119 | return $this->loadFromWanCache( $configPage, $flags ); |
120 | } |
121 | ); |
122 | } |
123 | |
124 | /** |
125 | * Load configuration from the WAN cache |
126 | * |
127 | * @param LinkTarget $configPage |
128 | * @param int $flags bit field, see IDBAccessObject::READ_XXX |
129 | * @return array|StatusValue The content of the configuration page (as JSON |
130 | * data in PHP-native format), or a StatusValue on error. |
131 | */ |
132 | private function loadFromWanCache( LinkTarget $configPage, int $flags = 0 ) { |
133 | return $this->cache->getWithSetCallback( |
134 | $this->makeCacheKey( $configPage ), |
135 | // Cache config for a day; cache is invalidated by ConfigHooks::onPageSaveComplete |
136 | // and WikiPageConfigWriter::save when config files are changed., |
137 | ExpirationAwareness::TTL_DAY, |
138 | function ( $oldValue, &$ttl ) use ( $configPage, $flags ) { |
139 | $result = $this->loadUncached( $configPage, $flags ); |
140 | if ( $result instanceof StatusValue ) { |
141 | // error should not be cached |
142 | $ttl = ExpirationAwareness::TTL_UNCACHEABLE; |
143 | } |
144 | return $result; |
145 | } |
146 | ); |
147 | } |
148 | |
149 | /** |
150 | * |
151 | * @param int $flags Bitfield consisting of READ_* constants |
152 | * @return int Bitfield consisting only of standard IDBAccessObject READ_* constants. |
153 | */ |
154 | private function removeCustomFlags( int $flags ): int { |
155 | return $flags & ~self::READ_UNCACHED; |
156 | } |
157 | |
158 | /** |
159 | * Load the configuration page, bypassing caching. |
160 | * |
161 | * Caller is responsible for caching the result if desired. |
162 | * |
163 | * @param LinkTarget $configPage |
164 | * @param int $flags |
165 | * @return array|StatusValue |
166 | */ |
167 | private function loadUncached( LinkTarget $configPage, int $flags = 0 ) { |
168 | $result = false; |
169 | $status = $this->fetchConfig( $configPage, $this->removeCustomFlags( $flags ) ); |
170 | if ( $status->isOK() ) { |
171 | $result = $status->getValue(); |
172 | $status->merge( |
173 | $this->configValidatorFactory |
174 | ->newConfigValidator( $configPage ) |
175 | ->validate( $result ) |
176 | ); |
177 | } |
178 | if ( !$status->isOK() ) { |
179 | $result = $status; |
180 | } |
181 | |
182 | return $result; |
183 | } |
184 | |
185 | /** |
186 | * Fetch the contents of the configuration page, without caching. |
187 | * |
188 | * Result is not validated with a config validator. |
189 | * |
190 | * @param LinkTarget $configPage |
191 | * @param int $flags bit field, see IDBAccessObject::READ_XXX; do NOT pass READ_UNCACHED |
192 | * @return StatusValue Status object, with the configuration (as JSON data) on success. |
193 | */ |
194 | private function fetchConfig( LinkTarget $configPage, int $flags ) { |
195 | if ( $configPage->isExternal() ) { |
196 | $url = Util::getRawUrl( $configPage, $this->titleFactory, $this->urlUtils ); |
197 | return Util::getJsonUrl( $this->requestFactory, $url ); |
198 | } |
199 | |
200 | $revision = $this->isTestWithStorageDisabled |
201 | ? null |
202 | : $this->revisionLookup->getRevisionByTitle( $configPage, 0, $flags ); |
203 | if ( !$revision ) { |
204 | // The configuration page does not exist. Pretend it does not configure anything |
205 | // specific (failure mode and empty-page behavior is equal, see T325236). |
206 | return StatusValue::newGood( $this->configValidatorFactory |
207 | ->newConfigValidator( $configPage ) |
208 | ->getDefaultContent() |
209 | ); |
210 | } |
211 | $content = $revision->getContent( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ); |
212 | if ( !$content || !$content instanceof JsonContent ) { |
213 | return StatusValue::newFatal( new ApiRawMessage( |
214 | 'The configuration title has no content or is not JSON content.', |
215 | 'automoderator-configuration-loader-content-error' |
216 | ) ); |
217 | } |
218 | |
219 | return FormatJson::parse( $content->getText(), FormatJson::FORCE_ASSOC ); |
220 | } |
221 | } |