Passed
Push — master ( 1d252d...bd204f )
by Daimona
01:55
created

RequestBase::execute()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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