Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 117 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
JCCache | |
0.00% |
0 / 117 |
|
0.00% |
0 / 8 |
2070 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
get | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
memcGet | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
memcSet | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
resetCache | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
56 | |||
loadLocal | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
loadRemote | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
132 | |||
getPageFromApi | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | namespace JsonConfig; |
3 | |
4 | use MediaWiki\MediaWikiServices; |
5 | use ObjectCache; |
6 | |
7 | /** |
8 | * Represents a json blob on a remote wiki. |
9 | * Handles retrieval (via HTTP) and memcached caching. |
10 | */ |
11 | class JCCache { |
12 | /** @var JCTitle */ |
13 | private $titleValue; |
14 | /** @var string */ |
15 | private $key; |
16 | /** @var \BagOStuff */ |
17 | private $cache; |
18 | |
19 | /** @var bool|string|JCContent */ |
20 | private $content = null; |
21 | |
22 | /** @var int number of seconds to keep the value in cache */ |
23 | private $cacheExpiration; |
24 | |
25 | /** |
26 | * ** DO NOT USE directly - call JCSingleton::getContent() instead. ** |
27 | * |
28 | * @param JCTitle $titleValue |
29 | * @param JCContent|null $content |
30 | */ |
31 | public function __construct( JCTitle $titleValue, $content = null ) { |
32 | global $wgJsonConfigCacheKeyPrefix; |
33 | $this->titleValue = $titleValue; |
34 | $conf = $this->titleValue->getConfig(); |
35 | $flRev = $conf->flaggedRevs; |
36 | $this->cache = ObjectCache::getInstance( CACHE_ANYTHING ); |
37 | $keyArgs = [ |
38 | 'JsonConfig', |
39 | $wgJsonConfigCacheKeyPrefix, |
40 | $conf->cacheKey, |
41 | $flRev === null ? '' : ( $flRev ? 'T' : 'F' ), |
42 | $titleValue->getNamespace(), |
43 | $titleValue->getDBkey(), |
44 | ]; |
45 | if ( $conf->isLocal ) { |
46 | $this->key = $this->cache->makeKey( ...$keyArgs ); |
47 | } else { |
48 | $this->key = $this->cache->makeGlobalKey( ...$keyArgs ); |
49 | } |
50 | $this->cacheExpiration = $conf->cacheExp; |
51 | $this->content = $content ?: null; // ensure that if we don't have content, we use 'null' |
52 | } |
53 | |
54 | /** |
55 | * Retrieves content. |
56 | * @return string|JCContent|false Content string/object or false if irretrievable. |
57 | */ |
58 | public function get() { |
59 | if ( $this->content === null ) { |
60 | $value = $this->memcGet(); // Get content from the memcached |
61 | if ( $value === false ) { |
62 | if ( $this->titleValue->getConfig()->store ) { |
63 | $this->loadLocal(); // Get it from the local wiki |
64 | } else { |
65 | $this->loadRemote(); // Get it from HTTP |
66 | } |
67 | $this->memcSet(); // Save result to memcached |
68 | } elseif ( $value === '' ) { |
69 | $this->content = false; // Invalid ID was cached |
70 | } else { |
71 | $this->content = $value; // Content was cached |
72 | } |
73 | } |
74 | |
75 | return $this->content; |
76 | } |
77 | |
78 | /** |
79 | * Retrieves content from memcached. |
80 | * @return string|bool Carrier config or false if not in cache. |
81 | */ |
82 | private function memcGet() { |
83 | global $wgJsonConfigDisableCache; |
84 | |
85 | return $wgJsonConfigDisableCache ? false : $this->cache->get( $this->key ); |
86 | } |
87 | |
88 | /** |
89 | * Store $this->content in memcached. |
90 | * If the content is invalid, store an empty string to prevent repeated attempts |
91 | */ |
92 | private function memcSet() { |
93 | global $wgJsonConfigDisableCache; |
94 | if ( !$wgJsonConfigDisableCache ) { |
95 | // caching an error condition for short time |
96 | $exp = $this->content ? $this->cacheExpiration : 10; |
97 | $value = $this->content ?: ''; |
98 | if ( !is_string( $value ) ) { |
99 | $value = $value->getNativeData(); |
100 | } |
101 | |
102 | $this->cache->set( $this->key, $value, $exp ); |
103 | } |
104 | } |
105 | |
106 | /** |
107 | * Delete any cached information related to this config |
108 | * @param null|bool $updateCacheContent controls if cache should be updated with the new content |
109 | * false = only clear cache, |
110 | * true = set cache to the new value, |
111 | * null = use configuration settings |
112 | * New content will be set only if it is present |
113 | * (either get() was called before, or it was set via ctor) |
114 | */ |
115 | public function resetCache( $updateCacheContent = null ) { |
116 | global $wgJsonConfigDisableCache; |
117 | if ( !$wgJsonConfigDisableCache ) { |
118 | $conf = $this->titleValue->getConfig(); |
119 | if ( $this->content && ( $updateCacheContent === true || |
120 | ( $updateCacheContent === null && isset( $conf->store ) && |
121 | // @phan-suppress-next-line PhanTypeExpectedObjectPropAccess |
122 | $conf->store->cacheNewValue ) ) |
123 | ) { |
124 | $this->memcSet(); // update cache with the new value |
125 | } else { |
126 | $this->cache->delete( $this->key ); // only delete existing value |
127 | } |
128 | } |
129 | } |
130 | |
131 | /** |
132 | * Retrieves the config from the local storage, |
133 | * and sets $this->content to the content object or false |
134 | */ |
135 | private function loadLocal() { |
136 | // @fixme @bug handle flagged revisions |
137 | $result = MediaWikiServices::getInstance() |
138 | ->getWikiPageFactory() |
139 | ->newFromLinkTarget( $this->titleValue ) |
140 | ->getContent(); |
141 | if ( !$result ) { |
142 | $result = false; // Keeping consistent with other usages |
143 | } elseif ( !( $result instanceof JCContent ) ) { |
144 | if ( $result->getModel() === CONTENT_MODEL_WIKITEXT ) { |
145 | // If this is a regular wiki page, allow it to be parsed as a json config |
146 | $result = $result->getNativeData(); |
147 | } else { |
148 | wfLogWarning( "The locally stored wiki page '$this->titleValue' has " . |
149 | "unsupported content model'" ); |
150 | $result = false; |
151 | } |
152 | } |
153 | $this->content = $result; |
154 | } |
155 | |
156 | /** |
157 | * Retrieves the config using HTTP and sets $this->content to string or false |
158 | */ |
159 | private function loadRemote() { |
160 | do { |
161 | $result = false; |
162 | $conf = $this->titleValue->getConfig(); |
163 | $remote = $conf->remote; |
164 | // @phan-suppress-next-line PhanTypeExpectedObjectPropAccessButGotNull |
165 | $req = JCUtils::initApiRequestObj( $remote->url, $remote->username, $remote->password ); |
166 | if ( !$req ) { |
167 | break; |
168 | } |
169 | $ns = $conf->nsName ?: MediaWikiServices::getInstance() |
170 | ->getNamespaceInfo() |
171 | ->getCanonicalName( $this->titleValue->getNamespace() ); |
172 | $articleName = $ns . ':' . $this->titleValue->getText(); |
173 | $flrevs = $conf->flaggedRevs; |
174 | // if flaggedRevs is false, get wiki page directly, |
175 | // otherwise get the flagged state first |
176 | $res = $this->getPageFromApi( $articleName, $req, $flrevs === false |
177 | ? [ |
178 | 'action' => 'query', |
179 | 'titles' => $articleName, |
180 | 'prop' => 'revisions', |
181 | 'rvprop' => 'content', |
182 | 'rvslots' => 'main', |
183 | 'continue' => '', |
184 | ] |
185 | : [ |
186 | 'action' => 'query', |
187 | 'titles' => $articleName, |
188 | 'prop' => 'info|flagged', |
189 | 'continue' => '', |
190 | ] ); |
191 | if ( $res !== false && |
192 | ( $flrevs === null || ( $flrevs === true && array_key_exists( 'flagged', $res ) ) ) |
193 | ) { |
194 | // If there is a stable flagged revision present, use it. |
195 | // else - if flaggedRevs is null, use the latest revision that exists |
196 | // otherwise, fail because flaggedRevs is true, |
197 | // which means we require rev to be flagged |
198 | $res = $this->getPageFromApi( $articleName, $req, [ |
199 | 'action' => 'query', |
200 | 'revids' => array_key_exists( 'flagged', $res ) |
201 | ? $res['flagged']['stable_revid'] : $res['lastrevid'], |
202 | 'prop' => 'revisions', |
203 | 'rvprop' => 'content', |
204 | 'rvslots' => 'main', |
205 | 'continue' => '', |
206 | ] ); |
207 | } |
208 | if ( $res === false ) { |
209 | break; |
210 | } |
211 | |
212 | $result = $res['revisions'][0]['slots']['main']['*'] ?? false; |
213 | if ( $result === false ) { |
214 | break; |
215 | } |
216 | } while ( false ); |
217 | |
218 | $this->content = $result; |
219 | } |
220 | |
221 | /** Given a legal set of API parameters, return page from API |
222 | * @param string $articleName title name used for warnings |
223 | * @param \MWHttpRequest $req logged-in session |
224 | * @param array $query |
225 | * @return bool|mixed |
226 | */ |
227 | private function getPageFromApi( $articleName, $req, $query ) { |
228 | $revInfo = JCUtils::callApi( $req, $query, 'get remote JsonConfig' ); |
229 | if ( $revInfo === false ) { |
230 | return false; |
231 | } |
232 | if ( !isset( $revInfo['query']['pages'] ) ) { |
233 | JCUtils::warn( 'Unrecognizable API result', [ 'title' => $articleName ], $query ); |
234 | return false; |
235 | } |
236 | $pages = $revInfo['query']['pages']; |
237 | if ( !is_array( $pages ) || count( $pages ) !== 1 ) { |
238 | JCUtils::warn( 'Unexpected "pages" element', [ 'title' => $articleName ], $query ); |
239 | return false; |
240 | } |
241 | $pageInfo = reset( $pages ); // get the only element of the array |
242 | if ( isset( $pageInfo['missing'] ) ) { |
243 | return false; |
244 | } |
245 | return $pageInfo; |
246 | } |
247 | } |