Passed
Push — master ( 3decfe...46f54c )
by Daimona
01:32
created

RequestBase::recursiveMerge()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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