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