Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.33% covered (warning)
83.33%
40 / 48
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
RemoteSchema
83.33% covered (warning)
83.33%
40 / 48
75.00% covered (warning)
75.00%
6 / 8
18.34
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 get
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 jsonSerialize
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 memcGet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 memcSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 httpGet
76.47% covered (warning)
76.47%
13 / 17
0.00% covered (danger)
0.00%
0 / 1
4.21
 lock
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUri
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\EventLogging;
4
5use BagOStuff;
6use FormatJson;
7use JsonSerializable;
8use MediaWiki\Http\HttpRequestFactory;
9use MediaWiki\MediaWikiServices;
10use ObjectCache;
11use stdClass;
12
13/**
14 * Represents a schema revision on a remote wiki.
15 * Handles retrieval (via HTTP) and local caching.
16 */
17class RemoteSchema implements JsonSerializable {
18
19    public const LOCK_TIMEOUT = 20;
20
21    public $title;
22    public $revision;
23    public $cache;
24    public $httpRequestFactory;
25    public $key;
26    public $content = false;
27
28    /**
29     * Constructor.
30     * @param string $title
31     * @param int $revision
32     * @param BagOStuff|null $cache (optional) cache client.
33     * @param HttpRequestFactory|null $httpRequestFactory (optional) HttpRequestFactory client.
34     */
35    public function __construct( $title, $revision, $cache = null, $httpRequestFactory = null ) {
36        global $wgEventLoggingSchemaApiUri;
37
38        $this->title = $title;
39        $this->revision = $revision;
40        $this->cache = $cache ?: ObjectCache::getInstance( CACHE_ANYTHING );
41        $this->httpRequestFactory = $httpRequestFactory ?: MediaWikiServices::getInstance()->getHttpRequestFactory();
42        $this->key = $this->cache->makeGlobalKey(
43            'eventlogging-schema',
44            $wgEventLoggingSchemaApiUri,
45            $revision
46        );
47    }
48
49    /**
50     * Retrieves schema content.
51     * @return array|bool Schema or false if irretrievable.
52     */
53    public function get() {
54        if ( $this->content ) {
55            return $this->content;
56        }
57
58        $this->content = $this->memcGet();
59        if ( $this->content ) {
60            return $this->content;
61        }
62
63        $this->content = $this->httpGet();
64        if ( $this->content ) {
65            $this->memcSet();
66        }
67
68        return $this->content;
69    }
70
71    /**
72     * Returns an object containing serializable properties.
73     * @return array
74     */
75    public function jsonSerialize(): array {
76        return [
77            'schema'   => $this->get() ?: new stdClass(),
78            'revision' => $this->revision
79        ];
80    }
81
82    /**
83     * Retrieves content from memcached.
84     * @return array|bool Schema or false if not in cache.
85     */
86    protected function memcGet() {
87        return $this->cache->get( $this->key );
88    }
89
90    /**
91     * Store content in memcached.
92     * @return bool
93     */
94    protected function memcSet() {
95        return $this->cache->set( $this->key, $this->content );
96    }
97
98    /**
99     * Retrieves the schema using HTTP.
100     * Uses a memcached lock to avoid cache stampedes.
101     * @return array|bool Schema or false if unable to fetch.
102     */
103    protected function httpGet() {
104        $uri = $this->getUri();
105        if ( !$this->lock() ) {
106            EventLogging::getLogger()->warning(
107                'Failed to get lock for requesting {schema_uri}.',
108                [ 'schema_uri' => $uri ]
109            );
110            return false;
111        }
112        $raw = $this->httpRequestFactory->get( $uri, [
113            'timeout' => self::LOCK_TIMEOUT * 0.8
114        ], __METHOD__ );
115        $content = FormatJson::decode( $raw, true );
116        if ( !$content ) {
117            EventLogging::getLogger()->error(
118                'Request to {schema_uri} failed.',
119                [ 'schema_uri' => $uri ]
120            );
121        }
122        return $content ?: false;
123    }
124
125    /**
126     * Acquire a mutex lock for HTTP retrieval.
127     * @return bool Whether lock was successfully acquired.
128     */
129    protected function lock() {
130        return $this->cache->add( $this->key . ':lock', 1, self::LOCK_TIMEOUT );
131    }
132
133    /**
134     * Constructs URI for retrieving schema from remote wiki.
135     * @return string URI.
136     */
137    protected function getUri() {
138        global $wgEventLoggingSchemaApiUri;
139
140        return wfAppendQuery( $wgEventLoggingSchemaApiUri, [
141            'action' => 'jsonschema',
142            'revid'  => $this->revision,
143            'formatversion' => 2,
144            'format' => 'json',
145        ] );
146    }
147}