Passed
Push — master ( 80c5b5...c72e95 )
by Daimona
01:50
created

RequestBase::setCookies()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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