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