Passed
Push — master ( 5a8341...4cf7bd )
by Daimona
01:31
created

RequestBase   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 234
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 86
dl 0
loc 234
rs 9.84
c 0
b 0
f 0
wmc 32

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A execute() 0 17 3
A setCookies() 0 5 2
A makeRequestInternal() 0 10 2
A setPost() 0 3 1
A setUrl() 0 3 1
A newFromParams() 0 7 2
A buildHeadersString() 0 6 2
A handleErrorAndWarnings() 0 7 3
A getHeaders() 0 10 3
A getException() 0 15 4
A mergeSets() 0 8 2
A recursiveMerge() 0 11 6
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
use BotRiconferme\Exception\PermissionDeniedException;
10
11
/**
12
 * Core wrapper for an API request. Current implementations use either cURL or file_get_contents
13
 */
14
abstract class RequestBase {
15
	const USER_AGENT = 'Daimona - BotRiconferme ' . Bot::VERSION .
16
		' (https://github.com/Daimona/BotRiconferme)';
17
	const HEADERS = [
18
		'Content-Type: application/x-www-form-urlencoded',
19
		'User-Agent: ' . self::USER_AGENT
20
	];
21
	// In seconds
22
	const MAXLAG = 5;
23
24
	/** @var string */
25
	protected $url;
26
	/** @var array */
27
	protected static $cookiesToSet;
28
	/** @var array */
29
	protected $params;
30
	/** @var string */
31
	protected $method = 'GET';
32
	/** @var string[] */
33
	protected $newCookies = [];
34
35
	/**
36
	 * Use self::newFromParams, which will provide the right class to use
37
	 *
38
	 * @param array $params
39
	 */
40
	protected function __construct( array $params ) {
41
		$this->params = [ 'format' => 'json' ] + $params;
42
		$this->url = DEFAULT_URL;
43
	}
44
45
	/**
46
	 * Instance getter, will instantiate the proper subclass
47
	 *
48
	 * @param array $params
49
	 * @return self
50
	 */
51
	public static function newFromParams( array $params ) : self {
52
		if ( extension_loaded( 'curl' ) ) {
53
			$ret = new CurlRequest( $params );
54
		} else {
55
			$ret = new NativeRequest( $params );
56
		}
57
		return $ret;
58
	}
59
60
	/**
61
	 * Set the method to POST
62
	 *
63
	 * @return self For chaining
64
	 */
65
	public function setPost() : self {
66
		$this->method = 'POST';
67
		return $this;
68
	}
69
70
	/**
71
	 * Set a different URL for this request
72
	 *
73
	 * @param string $url
74
	 * @return self for chaining
75
	 */
76
	public function setUrl( string $url ) : self {
77
		$this->url = $url;
78
		return $this;
79
	}
80
81
	/**
82
	 * Entry point for an API request
83
	 *
84
	 * @return \stdClass
85
	 */
86
	public function execute() : \stdClass {
87
		$curParams = $this->params;
88
		$sets = [];
89
		do {
90
			$res = $this->makeRequestInternal( $curParams );
91
92
			$this->handleErrorAndWarnings( $res );
93
			$sets[] = $res;
94
95
			$finished = true;
96
			if ( isset( $res->continue ) ) {
97
				$curParams = array_merge( $curParams, get_object_vars( $res->continue ) );
98
				$finished = false;
99
			}
100
		} while ( !$finished );
101
102
		return $this->mergeSets( $sets );
103
	}
104
105
	/**
106
	 * Process parameters and call the actual request method
107
	 *
108
	 * @param array $params
109
	 * @return \stdClass
110
	 */
111
	private function makeRequestInternal( array $params ) : \stdClass {
112
		if ( $this->method === 'POST' ) {
113
			$params['maxlag'] = self::MAXLAG;
114
		}
115
		$params = http_build_query( $params );
116
117
		$body = $this->reallyMakeRequest( $params );
118
119
		$this->setCookies( $this->newCookies );
120
		return json_decode( $body );
121
	}
122
123
	/**
124
	 * Actual method which will make the request
125
	 *
126
	 * @param string $params
127
	 * @return string
128
	 */
129
	abstract protected function reallyMakeRequest( string $params ) : string;
130
131
	/**
132
	 * After a request, set cookies for the next ones
133
	 *
134
	 * @param array $cookies
135
	 */
136
	protected function setCookies( array $cookies ) {
137
		foreach ( $cookies as $cookie ) {
138
			$bits = explode( ';', $cookie );
139
			list( $name, $value ) = explode( '=', $bits[0] );
140
			self::$cookiesToSet[ $name ] = $value;
141
		}
142
	}
143
144
	/**
145
	 * Get a specific exception class depending on the error code
146
	 *
147
	 * @param \stdClass $res
148
	 * @return APIRequestException
149
	 */
150
	private function getException( \stdClass $res ) : APIRequestException {
151
		switch ( $res->error->code ) {
152
			case 'missingtitle':
153
				$ex = new MissingPageException;
154
				break;
155
			case 'protectedpage':
156
				$ex = new ProtectedPageException;
157
				break;
158
			case 'permissiondenied':
159
				$ex = new PermissionDeniedException( $res->error->info );
160
				break;
161
			default:
162
				$ex = new APIRequestException( $res->error->code . ' - ' . $res->error->info );
163
		}
164
		return $ex;
165
	}
166
167
	/**
168
	 * Handle known warning and errors from an API request
169
	 *
170
	 * @param \stdClass $res
171
	 * @throws APIRequestException
172
	 */
173
	protected function handleErrorAndWarnings( \stdClass $res ) {
174
		if ( isset( $res->error ) ) {
175
			throw $this->getException( $res );
176
		} elseif ( isset( $res->warnings ) ) {
177
			$act = $this->params[ 'action' ];
178
			$warning = $res->warnings->$act ?? $res->warnings->main;
179
			throw new APIRequestException( reset( $warning ) );
180
		}
181
	}
182
183
	/**
184
	 * Merge results from multiple requests in a single object
185
	 *
186
	 * @param \stdClass[] $sets
187
	 * @return \stdClass
188
	 */
189
	private function mergeSets( array $sets ) : \stdClass {
190
		// Use the first set as template
191
		$ret = array_shift( $sets );
192
193
		foreach ( $sets as $set ) {
194
			$ret = $this->recursiveMerge( $ret, $set );
195
		}
196
		return $ret;
197
	}
198
199
	/**
200
	 * Recursively merge objects, keeping the structure
201
	 *
202
	 * @param array|\stdClass $first
203
	 * @param array|\stdClass $second
204
	 * @return array|\stdClass array
205
	 */
206
	private function recursiveMerge( $first, $second ) {
207
		$ret = $first;
208
		if ( is_array( $second ) ) {
209
			$ret = is_array( $first ) ? array_merge_recursive( $first, $second ) : $second;
210
		} elseif ( is_object( $second ) ) {
211
			foreach ( get_object_vars( $second ) as $key => $val ) {
212
				$ret->$key = isset( $first->$key ) ? $this->recursiveMerge( $first->$key, $val ) : $val;
213
			}
214
		}
215
216
		return $ret;
217
	}
218
219
	/**
220
	 * Get the headers to use for a new request
221
	 *
222
	 * @return array
223
	 */
224
	protected function getHeaders() :array {
225
		$ret = self::HEADERS;
226
		if ( self::$cookiesToSet ) {
227
			$cookies = [];
228
			foreach ( self::$cookiesToSet as $cname => $cval ) {
229
				$cookies[] = trim( "$cname=$cval" );
230
			}
231
			$ret[] = 'Cookie: ' . implode( '; ', $cookies );
232
		}
233
		return $ret;
234
	}
235
236
	/**
237
	 * Utility function to implode headers
238
	 *
239
	 * @param array $headers
240
	 * @return string
241
	 */
242
	protected function buildHeadersString( array $headers ) : string {
243
		$ret = '';
244
		foreach ( $headers as $header ) {
245
			$ret .= "$header\r\n";
246
		}
247
		return $ret;
248
	}
249
}
250