Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
JCCache
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 8
2070
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 get
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 memcGet
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 memcSet
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 resetCache
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
56
 loadLocal
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 loadRemote
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
132
 getPageFromApi
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2namespace JsonConfig;
3
4use MediaWiki\MediaWikiServices;
5use ObjectCache;
6
7/**
8 * Represents a json blob on a remote wiki.
9 * Handles retrieval (via HTTP) and memcached caching.
10 */
11class 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}