Passed
Push — master ( 02246d...adfab7 )
by Daimona
02:13
created

RequestBase::getHeaders()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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