Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 84
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
RemoteMessageContentFetcher
0.00% covered (danger)
0.00%
0 / 84
0.00% covered (danger)
0.00%
0 / 4
132
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getContent
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
20
 parseQueryApiResponse
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
20
 getApiEndpoint
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\MassMessage\MessageContentFetcher;
5
6use MediaWiki\Config\SiteConfiguration;
7use MediaWiki\Http\HttpRequestFactory;
8use MediaWiki\MassMessage\LanguageAwareText;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\Status\Status;
11
12/**
13 * Fetches content from a remote wiki
14 * @author Abijeet Patro
15 * @since 2022.01
16 * @license GPL-2.0-or-later
17 */
18class RemoteMessageContentFetcher {
19    /** @var HttpRequestFactory */
20    private $httpRequestFactory;
21    /** @var SiteConfiguration */
22    private $siteConfiguration;
23
24    /**
25     * @param HttpRequestFactory $requestFactory
26     * @param SiteConfiguration $siteConfiguration
27     */
28    public function __construct( HttpRequestFactory $requestFactory, SiteConfiguration $siteConfiguration ) {
29        $this->httpRequestFactory = $requestFactory;
30        $this->siteConfiguration = $siteConfiguration;
31    }
32
33    /**
34     * Fetch remote content and return Status object. The Status value contains a LanguageAwareText object
35     * @param string $pageTitle
36     * @param string $wikiId
37     * @return Status
38     */
39    public function getContent( string $pageTitle, string $wikiId ): Status {
40        $apiUrl = $this->getApiEndpoint( $wikiId );
41        if ( !$apiUrl ) {
42            return Status::newFatal(
43                'massmessage-page-message-wiki-not-found',
44                $wikiId,
45                $pageTitle
46            );
47        }
48
49        $queryParams = [
50            'action' => 'query',
51            'format' => 'json',
52            'prop' => 'info|revisions',
53            'rvprop' => 'content',
54            'rvslots' => 'main',
55            'titles' => $pageTitle,
56            'formatversion' => 2,
57        ];
58
59        $options = [
60            'method' => 'GET',
61            'timeout' => 15,
62        ];
63
64        $apiUrl .= '?' . http_build_query( $queryParams );
65        $req = $this->httpRequestFactory->create( $apiUrl, $options, __METHOD__ );
66
67        $status = $req->execute();
68        if ( !$status->isOK() ) {
69            // FIXME: Formatting is broken here, needs to be improved.
70            return Status::newFatal(
71                "massmessage-page-message-fetch-error-in-wiki",
72                $wikiId,
73                $pageTitle,
74                $status->getMessage()->text()
75            );
76        }
77
78        $json = $req->getContent();
79        $response = json_decode( $json, true );
80        if ( $response === null ) {
81            return Status::newFatal(
82                "massmessage-page-message-parsing-error-in-wiki",
83                $wikiId,
84                $pageTitle,
85                json_last_error_msg()
86            );
87        }
88
89        return $this->parseQueryApiResponse( $response, $wikiId, $pageTitle, $json );
90    }
91
92    /**
93     * @param array $response
94     * @param string $wikiId
95     * @param string $pageTitle
96     * @param string $json
97     * @return Status
98     */
99    private function parseQueryApiResponse(
100        array $response,
101        string $wikiId,
102        string $pageTitle,
103        string $json
104    ): Status {
105        // Example response:
106        // {
107        //   "batchcomplete": true,
108        //   "query": {
109        //     "pages": [ {
110        //       "pageid": 11285354,
111        //       "ns": 0,
112        //       "title": "Tech/News/2021/12",
113        //       "contentmodel": "wikitext",
114        //       "pagelanguage": "en",
115        //       "pagelanguagehtmlcode": "en",
116        //       "pagelanguagedir": "ltr",
117        //       "touched": "2021-03-23T06:05:06Z",
118        //       "lastrevid": 21247464,
119        //       "length": 4585,
120        //       "revisions": [ {
121        //         "slots": {
122        //           "main": {
123        //             "contentmodel": "wikitext",
124        //             "contentformat": "text/x-wiki",
125        //             "content": "[...]"
126        //           }
127        //         }
128        //       } ]
129        //     } ]
130        //   }
131        // }
132
133        $pages = $response['query']['pages'] ?? [];
134        if ( isset( $response['error']['info'] ) || count( $pages ) !== 1 ) {
135            return Status::newFatal(
136                'massmessage-page-message-parse-invalid-in-wiki',
137                $wikiId,
138                $pageTitle,
139                $response['error']['info'] ?? $json
140            );
141        }
142
143        // Take first and only one out of the list
144        $page = current( $pages );
145
146        if ( isset( $page['missing'] ) ) {
147            // Page was not found
148            return Status::newFatal(
149                'massmessage-page-message-not-found-in-wiki',
150                $wikiId,
151                $pageTitle
152            );
153        }
154
155        $content = new LanguageAwareText(
156            $page['revisions'][0]['slots']['main']['content'],
157            $page['pagelanguage'],
158            $page['pagelanguagedir']
159        );
160
161        return Status::newGood( $content );
162    }
163
164    /**
165     * @param string $wiki
166     * @return string|null
167     */
168    private function getApiEndpoint( string $wiki ): ?string {
169        $this->siteConfiguration->loadFullData();
170
171        $siteFromDB = $this->siteConfiguration->siteFromDB( $wiki );
172        [ $major, $minor ] = $siteFromDB;
173
174        if ( $major === null ) {
175            return null;
176        }
177
178        $configOpts = [ 'lang' => $minor, 'site' => $major ];
179        $server = $this->siteConfiguration->get(
180            'wgServer',
181            $wiki,
182            null,
183            $configOpts
184        );
185        $scriptPath = $this->siteConfiguration->get(
186            'wgScriptPath',
187            $wiki,
188            null,
189            $configOpts
190        );
191
192        $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
193        $apiPath = $urlUtils->expand( $server . $scriptPath . '/api.php', PROTO_INTERNAL );
194
195        return $apiPath;
196    }
197}