Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiBackend
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 7
272
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLogger
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 retrieveThreadData
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
20
 retrievePageDataById
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 retrieveTopRevisionByTitle
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 retrievePageData
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
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% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Flow\Import\LiquidThreadsApi;
4
5use ApiBase;
6use Flow\Import\ImportException;
7use InvalidArgumentException;
8use Psr\Log\LoggerAwareInterface;
9use Psr\Log\LoggerInterface;
10use Psr\Log\NullLogger;
11
12abstract 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}