Passed
Push — master ( ebe4f4...85a9ba )
by Daimona
02:09
created

RequestBase::handleErrorAndWarnings()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 17
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 15
nc 5
nop 1
dl 0
loc 17
rs 9.4555
c 0
b 0
f 0
1
<?php declare( strict_types=1 );
2
3
namespace BotRiconferme\Request;
4
5
use BotRiconferme\Bot;
6
use BotRiconferme\Exception\APIRequestException;
7
use BotRiconferme\Exception\MissingPageException;
8
use BotRiconferme\Exception\ProtectedPageException;
9
10
/**
11
 * Core wrapper for an API request. Current implementations use either cURL or file_get_contents
12
 */
13
abstract class RequestBase {
14
	const USER_AGENT = 'Daimona - BotRiconferme ' . Bot::VERSION .
15
		' (https://github.com/Daimona/BotRiconferme)';
16
	const HEADERS = [
17
		'Content-Type: application/x-www-form-urlencoded',
18
		'User-Agent: ' . self::USER_AGENT
19
	];
20
	// In seconds
21
	const MAXLAG = 5;
22
23
	/** @var string */
24
	public static $url = 'https://it.wikipedia.org/w/api.php';
25
	/** @var array */
26
	protected static $cookiesToSet;
27
	/** @var array */
28
	protected $params;
29
	/** @var string */
30
	protected $method;
31
	/** @var string[] */
32
	protected $newCookies = [];
33
	/** @var int */
34
	private $limit = -1;
35
36
	/**
37
	 * Use self::newFromParams, which will provide the right class to use
38
	 *
39
	 * @param array $params
40
	 * @param bool $isPOST
41
	 */
42
	protected function __construct( array $params, bool $isPOST = false ) {
43
		$this->params = [ 'format' => 'json' ] + $params;
44
		$this->method = $isPOST ? 'POST' : 'GET';
45
	}
46
47
	/**
48
	 * Instance getter, will instantiate the proper subclass
49
	 *
50
	 * @param array $params
51
	 * @param bool $isPOST
52
	 * @return self
53
	 */
54
	public static function newFromParams( array $params, bool $isPOST = false ) : self {
55
		if ( extension_loaded( 'curl' ) ) {
56
			$ret = new CurlRequest( $params, $isPOST );
57
		} else {
58
			$ret = new NativeRequest( $params, $isPOST );
59
		}
60
		return $ret;
61
	}
62
63
	/**
64
	 * Set a limit to the amount of returned results. -1 means no limit
65
	 *
66
	 * @param int $val
67
	 */
68
	public function setResultLimit( int $val ) {
69
		$this->limit = $val;
70
	}
71
72
	/**
73
	 * Entry point for an API request
74
	 *
75
	 * @return \stdClass
76
	 */
77
	public function execute() : \stdClass {
78
		$curParams = $this->params;
79
		$sets = [];
80
		$amount = 0;
81
		do {
82
			$res = $this->makeRequestInternal( $curParams );
83
			$amount += count( $res );
0 ignored issues
show
Bug introduced by
$res of type stdClass is incompatible with the type Countable|array expected by parameter $var of count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

83
			$amount += count( /** @scrutinizer ignore-type */ $res );
Loading history...
84
85
			$this->handleErrorAndWarnings( $res );
86
			$sets[] = $res;
87
88
			$finished = true;
89
			if ( isset( $res->continue ) && $this->limit > -1 && $amount < $this->limit ) {
90
				$curParams = array_merge( $curParams, get_object_vars( $res->continue ) );
91
				$finished = false;
92
			}
93
		} while ( !$finished );
94
95
		return $this->mergeSets( $sets );
96
	}
97
98
	/**
99
	 * Process parameters and call the actual request method
100
	 *
101
	 * @param array $params
102
	 * @return \stdClass
103
	 */
104
	private function makeRequestInternal( array $params ) : \stdClass {
105
		if ( $this->method === 'POST' ) {
106
			$params['maxlag'] = self::MAXLAG;
107
		}
108
		$params = http_build_query( $params );
109
110
		$body = $this->reallyMakeRequest( $params );
111
112
		$this->setCookies( $this->newCookies );
113
		return json_decode( $body );
114
	}
115
116
	/**
117
	 * Actual method which will make the request
118
	 *
119
	 * @param string $params
120
	 * @return string
121
	 */
122
	abstract protected function reallyMakeRequest( string $params ) : string;
123
124
	/**
125
	 * After a request, set cookies for the next ones
126
	 *
127
	 * @param array $cookies
128
	 */
129
	protected function setCookies( array $cookies ) {
130
		foreach ( $cookies as $cookie ) {
131
			$bits = explode( ';', $cookie );
132
			list( $name, $value ) = explode( '=', $bits[0] );
133
			self::$cookiesToSet[ $name ] = $value;
134
		}
135
	}
136
137
	/**
138
	 * Handle known warning and errors from an API request
139
	 *
140
	 * @param \stdClass $res
141
	 * @throws APIRequestException
142
	 */
143
	protected function handleErrorAndWarnings( $res ) {
144
		if ( isset( $res->error ) ) {
145
			switch ( $res->error->code ) {
146
				case 'missingtitle':
147
					$ex = new MissingPageException;
148
					break;
149
				case 'protectedpage':
150
					$ex = new ProtectedPageException;
151
					break;
152
				default:
153
					$ex = new APIRequestException( $res->error->code . ' - ' . $res->error->info );
154
			}
155
			throw $ex;
156
		} elseif ( isset( $res->warnings ) ) {
157
			$act = $this->params[ 'action' ];
158
			$warning = $res->warnings->$act;
159
			throw new APIRequestException( reset( $warning ) );
160
		}
161
	}
162
163
	/**
164
	 * Merge results from multiple requests in a single object
165
	 *
166
	 * @param \stdClass[] $sets
167
	 * @return \stdClass
168
	 */
169
	private function mergeSets( array $sets ) : \stdClass {
170
		// Use the first set as template
171
		$ret = array_shift( $sets );
172
173
		foreach ( $sets as $set ) {
174
			$ret = $this->recursiveMerge( $ret, $set );
175
		}
176
		return $ret;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $ret could return the type array which is incompatible with the type-hinted return stdClass. Consider adding an additional type-check to rule them out.
Loading history...
177
	}
178
179
	/**
180
	 * Recursively merge objects, keeping the structure
181
	 *
182
	 * @param array|\stdClass $first
183
	 * @param array|\stdClass $second
184
	 * @return array|\stdClass array
185
	 */
186
	private function recursiveMerge( $first, $second ) {
187
		$ret = $first;
188
		if ( is_array( $second ) ) {
189
			$ret = is_array( $first ) ? array_merge_recursive( $first, $second ) : $second;
190
		} elseif ( is_object( $second ) ) {
191
			foreach ( get_object_vars( $second ) as $key => $val ) {
192
				$ret->$key = isset( $first->$key ) ? $this->recursiveMerge( $first->$key, $val ) : $val;
193
			}
194
		}
195
196
		return $ret;
197
	}
198
199
	/**
200
	 * Get the headers to use for a new request
201
	 *
202
	 * @return array
203
	 */
204
	protected function getHeaders() :array {
205
		$ret = self::HEADERS;
206
		if ( self::$cookiesToSet ) {
207
			$cookies = [];
208
			foreach ( self::$cookiesToSet as $cname => $cval ) {
209
				$cookies[] = trim( "$cname=$cval" );
210
			}
211
			$ret[] = 'Cookie: ' . implode( '; ', $cookies );
212
		}
213
		return $ret;
214
	}
215
216
	/**
217
	 * Utility function to implode headers
218
	 *
219
	 * @param array $headers
220
	 * @return string
221
	 */
222
	protected function buildHeadersString( array $headers ) : string {
223
		$ret = '';
224
		foreach ( $headers as $header ) {
225
			$ret .= "$header\r\n";
226
		}
227
		return $ret;
228
	}
229
}
230