Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 77 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
ApiBackend | |
0.00% |
0 / 77 |
|
0.00% |
0 / 7 |
272 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setLogger | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
retrieveThreadData | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
20 | |||
retrievePageDataById | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
retrieveTopRevisionByTitle | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
retrievePageData | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
30 | |||
apiCall | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getKey | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
isNotFoundError | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Flow\Import\LiquidThreadsApi; |
4 | |
5 | use ApiBase; |
6 | use Flow\Import\ImportException; |
7 | use InvalidArgumentException; |
8 | use Psr\Log\LoggerAwareInterface; |
9 | use Psr\Log\LoggerInterface; |
10 | use Psr\Log\NullLogger; |
11 | |
12 | abstract class ApiBackend implements LoggerAwareInterface { |
13 | |
14 | /** |
15 | * @var LoggerInterface |
16 | */ |
17 | protected $logger; |
18 | |
19 | public function __construct() { |
20 | $this->logger = new NullLogger; |
21 | } |
22 | |
23 | public function setLogger( LoggerInterface $logger ) { |
24 | $this->logger = $logger; |
25 | } |
26 | |
27 | /** |
28 | * Retrieves LiquidThreads data from the API |
29 | * |
30 | * @param array $conditions The parameters to pass to select the threads. |
31 | * Usually used in two ways: with thstartid/thpage, or with ththreadid |
32 | * @return array Data as returned under query.threads by the API |
33 | * @throws ApiNotFoundException Thrown when the remote api reports that the provided conditions |
34 | * have no matching records. |
35 | * @throws ImportException When an error is received from the remote api. This is often either |
36 | * a bad request or lqt threw an exception trying to respond to a valid request. |
37 | */ |
38 | public function retrieveThreadData( array $conditions ) { |
39 | $params = [ |
40 | 'action' => 'query', |
41 | 'list' => 'threads', |
42 | 'thprop' => 'id|subject|page|parent|ancestor|created|modified|author|summaryid' . '|type|rootid|replies|signature', |
43 | 'rawcontinue' => 1, |
44 | // We're doing continuation a different way, but this avoids a warning. |
45 | 'format' => 'json', |
46 | 'limit' => ApiBase::LIMIT_BIG1, |
47 | ]; |
48 | $data = $this->apiCall( $params + $conditions ); |
49 | |
50 | if ( !isset( $data['query']['threads'] ) ) { |
51 | if ( $this->isNotFoundError( $data ) ) { |
52 | $message = "Did not find thread with conditions: " . json_encode( $conditions ); |
53 | $this->logger->debug( __METHOD__ . ": $message" ); |
54 | throw new ApiNotFoundException( $message ); |
55 | } else { |
56 | $this->logger->error( |
57 | __METHOD__ . ': Failed API call against ' . $this->getKey( |
58 | ) . ' with conditions : ' . json_encode( $conditions ) |
59 | ); |
60 | throw new ImportException( |
61 | "Null response from API module:" . json_encode( $data ) |
62 | ); |
63 | } |
64 | } |
65 | |
66 | $firstThread = reset( $data['query']['threads'] ); |
67 | if ( !isset( $firstThread['replies'] ) ) { |
68 | throw new ImportException( |
69 | "Foreign API does not support reply exporting:" . json_encode( $data ) |
70 | ); |
71 | } |
72 | |
73 | return $data['query']['threads']; |
74 | } |
75 | |
76 | /** |
77 | * Retrieves data about a set of pages from the API |
78 | * |
79 | * @param int[] $pageIds Page IDs to return data for. There must be at least one element. |
80 | * @phan-param non-empty-list<int> $pageIds |
81 | * @return array The query.pages part of the API response. |
82 | */ |
83 | public function retrievePageDataById( array $pageIds ) { |
84 | if ( !$pageIds ) { |
85 | throw new InvalidArgumentException( 'At least one page id must be provided' ); |
86 | } |
87 | |
88 | return $this->retrievePageData( |
89 | [ |
90 | 'pageids' => implode( '|', $pageIds ), |
91 | ] |
92 | ); |
93 | } |
94 | |
95 | /** |
96 | * Retrieves data about the latest revision of the titles |
97 | * from the API |
98 | * |
99 | * @param string[] $titles Titles to return data for. There must be at least one element. |
100 | * @phan-param non-empty-list<string> $titles |
101 | * @return array The query.pages part of the API response. |
102 | * @throws ImportException |
103 | */ |
104 | public function retrieveTopRevisionByTitle( array $titles ) { |
105 | if ( !$titles ) { |
106 | throw new InvalidArgumentException( 'At least one title must be provided' ); |
107 | } |
108 | |
109 | return $this->retrievePageData( |
110 | [ |
111 | 'titles' => implode( '|', $titles ), |
112 | 'rvlimit' => 1, |
113 | 'rvdir' => 'older', |
114 | ], |
115 | true |
116 | ); |
117 | } |
118 | |
119 | /** |
120 | * Retrieves data about a set of pages from the API |
121 | * |
122 | * @param array $conditions Conditions to retrieve pages by; to be sent to the API. |
123 | * @param bool $expectContinue Pass true here when caller expects more revisions to exist than |
124 | * they are requesting information about. |
125 | * @return array The query.pages part of the API response. |
126 | * @throws ApiNotFoundException Thrown when the remote api reports that the provided conditions |
127 | * have no matching records. |
128 | * @throws ImportException When an error is received from the remote api. This is often either |
129 | * a bad request or lqt threw an exception trying to respond to a valid request. |
130 | * @throws ImportException When more revisions are available than can be returned in a single |
131 | * query and the calling code does not set $expectContinue to true. |
132 | */ |
133 | public function retrievePageData( array $conditions, $expectContinue = false ) { |
134 | $conditions += [ |
135 | 'action' => 'query', |
136 | 'prop' => 'revisions', |
137 | 'rvprop' => 'timestamp|user|content|ids', |
138 | 'format' => 'json', |
139 | 'rvlimit' => 5000, |
140 | 'rvdir' => 'newer', |
141 | 'continue' => '', |
142 | ]; |
143 | $data = $this->apiCall( $conditions ); |
144 | |
145 | if ( !isset( $data['query'] ) ) { |
146 | if ( $this->isNotFoundError( $data ) ) { |
147 | $message = "Did not find pages: " . json_encode( $conditions ); |
148 | $this->logger->debug( __METHOD__ . ": $message" ); |
149 | throw new ApiNotFoundException( $message ); |
150 | } else { |
151 | $this->logger->error( |
152 | __METHOD__ . ': Failed API call against ' . $this->getKey( |
153 | ) . ' with conditions : ' . json_encode( $conditions ) |
154 | ); |
155 | throw new ImportException( |
156 | "Null response from API module: " . json_encode( $data ) |
157 | ); |
158 | } |
159 | } elseif ( !$expectContinue && isset( $data['continue'] ) ) { |
160 | throw new ImportException( |
161 | "More revisions than can be retrieved for conditions, import would" . " be incomplete: " . json_encode( |
162 | $conditions |
163 | ) |
164 | ); |
165 | } |
166 | |
167 | return $data['query']['pages']; |
168 | } |
169 | |
170 | /** |
171 | * Calls the remote API |
172 | * |
173 | * @param array $params The API request to send |
174 | * @param int $retry Retry the request on failure this many times |
175 | * @return array API return value, decoded from JSON into an array. |
176 | */ |
177 | abstract public function apiCall( array $params, $retry = 1 ); |
178 | |
179 | /** |
180 | * @return string A unique identifier for this backend. |
181 | */ |
182 | abstract public function getKey(); |
183 | |
184 | /** |
185 | * @param array $apiResponse |
186 | * @return bool |
187 | */ |
188 | protected function isNotFoundError( $apiResponse ) { |
189 | // LQT has some bugs where not finding the requested item in the database |
190 | // returns this exception. |
191 | $expect = 'Exception Caught: Wikimedia\\Rdbms\\Database::makeList: empty input for field thread_parent'; |
192 | |
193 | return strpos( $apiResponse['error']['info'], $expect ) !== false; |
194 | } |
195 | } |