Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
73.44% |
47 / 64 |
|
71.43% |
5 / 7 |
CRAP | |
0.00% |
0 / 1 |
RemoteApiRequestExecutor | |
73.44% |
47 / 64 |
|
71.43% |
5 / 7 |
24.07 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
setLogger | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
execute | |
36.36% |
4 / 11 |
|
0.00% |
0 / 1 |
8.12 | |||
getCentralId | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
canUseCentralAuth | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getAuthorizedApiUrl | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getCsrfToken | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
3 | |||
doRequest | |
52.38% |
11 / 21 |
|
0.00% |
0 / 1 |
7.70 |
1 | <?php |
2 | |
3 | namespace FileImporter\Remote\MediaWiki; |
4 | |
5 | use Exception; |
6 | use FileImporter\Data\SourceUrl; |
7 | use FileImporter\Services\Http\HttpRequestExecutor; |
8 | use MediaWiki\User\CentralId\CentralIdLookup; |
9 | use MediaWiki\User\User; |
10 | use MediaWiki\User\UserIdentity; |
11 | use Psr\Log\LoggerAwareInterface; |
12 | use Psr\Log\LoggerInterface; |
13 | use Psr\Log\NullLogger; |
14 | |
15 | /** |
16 | * Use CentralAuth to execute API calls on a sibling wiki. |
17 | * |
18 | * @license GPL-2.0-or-later |
19 | */ |
20 | class RemoteApiRequestExecutor implements LoggerAwareInterface { |
21 | |
22 | private HttpApiLookup $httpApiLookup; |
23 | private HttpRequestExecutor $httpRequestExecutor; |
24 | private CentralAuthTokenProvider $centralAuthTokenProvider; |
25 | private CentralIdLookup $centralIdLookup; |
26 | private LoggerInterface $logger; |
27 | |
28 | public function __construct( |
29 | HttpApiLookup $httpApiLookup, |
30 | HttpRequestExecutor $httpRequestExecutor, |
31 | CentralAuthTokenProvider $centralAuthTokenProvider, |
32 | CentralIdLookup $centralIdLookup |
33 | ) { |
34 | $this->httpApiLookup = $httpApiLookup; |
35 | $this->httpRequestExecutor = $httpRequestExecutor; |
36 | $this->centralAuthTokenProvider = $centralAuthTokenProvider; |
37 | $this->centralIdLookup = $centralIdLookup; |
38 | $this->logger = new NullLogger(); |
39 | } |
40 | |
41 | /** |
42 | * @codeCoverageIgnore |
43 | */ |
44 | public function setLogger( LoggerInterface $logger ) { |
45 | $this->logger = $logger; |
46 | } |
47 | |
48 | /** |
49 | * @param SourceUrl $sourceUrl |
50 | * @param User $user |
51 | * @param array $params API request params |
52 | * @param bool $usePost |
53 | * @return array|null Null in case of an error. Calling code can't understand why, but the error |
54 | * is logged. |
55 | */ |
56 | public function execute( |
57 | SourceUrl $sourceUrl, |
58 | User $user, |
59 | array $params, |
60 | bool $usePost = false |
61 | ): ?array { |
62 | // TODO handle error |
63 | if ( !$this->canUseCentralAuth( $user ) ) { |
64 | $this->logger->error( __METHOD__ . ' user can\'t use CentralAuth.' ); |
65 | return null; |
66 | } |
67 | |
68 | $result = $this->doRequest( $sourceUrl, $user, $params, $usePost ); |
69 | |
70 | // It's an array of "errors" with errorformat=plaintext, but a single "error" without. |
71 | // Each error contains "code" and "info" with formatversion=2, but "code" and "*" without. |
72 | if ( isset( $result['errors'] ) || isset( $result['error'] ) ) { |
73 | $this->logger->error( 'Remote API responded with an error', [ |
74 | 'sourceUrl' => $sourceUrl->getUrl(), |
75 | 'apiParameters' => $params, |
76 | 'response' => $result, |
77 | ] ); |
78 | } |
79 | |
80 | return $result; |
81 | } |
82 | |
83 | private function getCentralId( UserIdentity $user ): int { |
84 | return $this->centralIdLookup->centralIdFromLocalUser( |
85 | $user, |
86 | CentralIdLookup::AUDIENCE_RAW |
87 | ); |
88 | } |
89 | |
90 | private function canUseCentralAuth( User $user ): bool { |
91 | return $user->isSafeToLoad() && |
92 | $this->getCentralId( $user ) !== 0; |
93 | } |
94 | |
95 | /** |
96 | * @throws Exception |
97 | */ |
98 | private function getAuthorizedApiUrl( |
99 | SourceUrl $sourceUrl, |
100 | User $user |
101 | ): string { |
102 | $url = $this->httpApiLookup->getApiUrl( $sourceUrl ); |
103 | return wfAppendQuery( $url, [ |
104 | 'centralauthtoken' => $this->centralAuthTokenProvider->getToken( $user ), |
105 | ] ); |
106 | } |
107 | |
108 | public function getCsrfToken( SourceUrl $sourceUrl, User $user ): ?string { |
109 | try { |
110 | $tokenRequestUrl = $this->getAuthorizedApiUrl( $sourceUrl, $user ); |
111 | } catch ( Exception $ex ) { |
112 | $this->logger->error( 'Failed to get centralauthtoken: ' . |
113 | $ex->getMessage() ); |
114 | return null; |
115 | } |
116 | $tokenRequest = $this->httpRequestExecutor->execute( $tokenRequestUrl, [ |
117 | 'action' => 'query', |
118 | 'meta' => 'tokens', |
119 | 'type' => 'csrf', |
120 | 'format' => 'json', |
121 | 'formatversion' => 2, |
122 | ] ); |
123 | |
124 | $tokenData = json_decode( $tokenRequest->getContent(), true ); |
125 | $token = $tokenData['query']['tokens']['csrftoken'] ?? null; |
126 | |
127 | if ( !$token ) { |
128 | $this->logger->error( __METHOD__ . ' failed to get CSRF token.' ); |
129 | } |
130 | |
131 | return $token; |
132 | } |
133 | |
134 | /** |
135 | * @param SourceUrl $sourceUrl |
136 | * @param User $user |
137 | * @param array $params API request params |
138 | * @param bool $usePost |
139 | * @return array|null Null in case of an error. Calling code can't understand why, but the error |
140 | * is logged. |
141 | */ |
142 | private function doRequest( |
143 | SourceUrl $sourceUrl, |
144 | User $user, |
145 | array $params, |
146 | bool $usePost |
147 | ): ?array { |
148 | /** @var array|null $result */ |
149 | $result = null; |
150 | /** @var \MWHttpRequest|null $request */ |
151 | $request = null; |
152 | |
153 | try { |
154 | $requestUrl = $this->getAuthorizedApiUrl( $sourceUrl, $user ); |
155 | $this->logger->debug( 'Got cross-wiki, authorized API URL: ' . $requestUrl ); |
156 | if ( $usePost ) { |
157 | $request = $this->httpRequestExecutor->executePost( $requestUrl, $params ); |
158 | } else { |
159 | $request = $this->httpRequestExecutor->execute( $requestUrl, $params ); |
160 | } |
161 | $result = json_decode( $request->getContent(), true ); |
162 | if ( $result === null ) { |
163 | $this->logger->error( __METHOD__ . ' failed to decode response from ' . |
164 | $request->getFinalUrl() ); |
165 | } |
166 | } catch ( Exception $ex ) { |
167 | if ( !$request ) { |
168 | $msg = __METHOD__ . ' failed to do remote request to ' . $sourceUrl->getHost() . |
169 | ' with params ' . json_encode( $params ) . ': ' . $ex->getMessage(); |
170 | } else { |
171 | $msg = __METHOD__ . ' failed to do remote request to ' . $request->getFinalUrl() . |
172 | ' with params ' . json_encode( $params ) . |
173 | ' and response headers ' . json_encode( $request->getResponseHeaders() ) . |
174 | ': ' . $ex->getMessage(); |
175 | } |
176 | $this->logger->error( $msg ); |
177 | } |
178 | |
179 | return $result; |
180 | } |
181 | |
182 | } |