Completed
Push — master ( 3c1dde...a7cf33 )
by mw
04:12
created

SocketRequest   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 289
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 96.35%

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 1
dl 0
loc 289
ccs 132
cts 137
cp 0.9635
rs 8.6
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
B ping() 0 35 5
A setOption() 0 3 1
A getOption() 0 3 2
A getLastTransferInfo() 0 3 1
A getLastError() 0 3 1
A getLastErrorCode() 0 3 1
B execute() 0 34 1
A getResourceFromSocketClient() 0 16 1
B doMakeSocketRequest() 0 36 4
C getUrlComponents() 0 27 8
B postResponseToCallback() 0 12 6
B tryToFindFollowLocation() 0 23 5
1
<?php
2
3
namespace Onoi\HttpRequest;
4
5
use InvalidArgumentException;
6
use Closure;
7
8
/**
9
 * This class creates a remote socked connection to initiate an asynchronous
10
 * http/https request.
11
 *
12
 * Once a connection is established and content has been posted, the request
13
 * will close the connection. The receiving client is responsible for open
14
 * a separate process and initiated an independent transaction.
15
 *
16
 * @license GNU GPL v2+
17
 * @since 1.1
18
 *
19
 * @author mwjames
20
 */
21
class SocketRequest implements HttpRequest {
22
23
	/**
24
	 * @var array
25
	 */
26
	private $options = array();
27
28
	/**
29
	 * @var integer
30
	 */
31
	private $errno = 0;
32
33
	/**
34
	 * @var string
35
	 */
36
	private $errstr = '';
37
38
	/**
39
	 * @var string
40
	 */
41
	private $lastTransferInfo = '';
42
43
	/**
44
	 * @var boolean
45
	 */
46
	private $followedLocation = false;
47
48
	/**
49
	 * @since 1.1
50
	 *
51
	 * @param string|null $url
52
	 */
53 12
	public function __construct( $url = null ) {
54 12
		$this->setOption( ONOI_HTTP_REQUEST_URL, $url );
55 12
		$this->setOption( ONOI_HTTP_REQUEST_CONNECTION_TIMEOUT, 15 );
56 12
		$this->setOption( ONOI_HTTP_REQUEST_CONNECTION_FAILURE_REPEAT, 2 );
57 12
		$this->setOption( ONOI_HTTP_REQUEST_SOCKET_CLIENT_FLAGS, STREAM_CLIENT_ASYNC_CONNECT | STREAM_CLIENT_CONNECT );
58 12
		$this->setOption( ONOI_HTTP_REQUEST_METHOD, 'POST' );
59 12
		$this->setOption( ONOI_HTTP_REQUEST_CONTENT_TYPE, "application/x-www-form-urlencoded" );
60 12
		$this->setOption( ONOI_HTTP_REQUEST_SSL_VERIFYPEER, false );
61 12
		$this->setOption( ONOI_HTTP_REQUEST_FOLLOWLOCATION, true );
62 12
	}
63
64
	/**
65
	 * @since 1.1
66
	 *
67
	 * {@inheritDoc}
68
	 */
69 7
	public function ping() {
70
71 7
		if ( $this->getOption( ONOI_HTTP_REQUEST_URL ) === null ) {
72 2
			return false;
73
		}
74
75 6
		$urlComponents = $this->getUrlComponents( $this->getOption( ONOI_HTTP_REQUEST_URL ) );
76
77 6
		$resource = $this->getResourceFromSocketClient(
78 6
			$urlComponents,
79
			STREAM_CLIENT_CONNECT
80 6
		);
81
82 6
		if ( !$resource ) {
83 1
			return false;
84
		}
85
86 5
		stream_set_timeout( $resource, $this->getOption( ONOI_HTTP_REQUEST_CONNECTION_TIMEOUT ) );
87
88
		$httpMessage = (
89 5
			"HEAD "  . $urlComponents['path'] . " HTTP/1.1\r\n" .
90 5
			"Host: " . $urlComponents['host'] . "\r\n" .
91
			"Connection: Close\r\n\r\n"
92 5
		);
93
94 5
		$res = @fwrite( $resource, $httpMessage );
95
96 5
		if ( $this->getOption( ONOI_HTTP_REQUEST_FOLLOWLOCATION ) && $res !== false ) {
97 5
			$this->setOption( ONOI_HTTP_REQUEST_URL, $this->tryToFindFollowLocation( $resource, $urlComponents ) );
98 5
		}
99
100 5
		@fclose( $resource );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
101
102 5
		return $res !== false;
103
	}
104
105
	/**
106
	 * @since 1.1
107
	 *
108
	 * {@inheritDoc}
109
	 */
110 17
	public function setOption( $name, $value ) {
111 17
		$this->options[$name] = $value;
112 17
	}
113
114
	/**
115
	 * @since 1.1
116
	 *
117
	 * {@inheritDoc}
118
	 */
119 13
	public function getOption( $name ) {
120 13
		return isset( $this->options[$name] ) ? $this->options[$name] : null;
121
	}
122
123
	/**
124
	 * @since 1.1
125
	 *
126
	 * {@inheritDoc}
127
	 */
128 1
	public function getLastTransferInfo( $name = null ) {
129 1
		return $this->lastTransferInfo;
130
	}
131
132
	/**
133
	 * @since 1.1
134
	 *
135
	 * {@inheritDoc}
136
	 */
137 1
	public function getLastError() {
138 1
		return $this->errstr;
139
	}
140
141
	/**
142
	 * @since 1.1
143
	 *
144
	 * {@inheritDoc}
145
	 */
146 1
	public function getLastErrorCode() {
147 1
		return $this->errno;
148
	}
149
150
	/**
151
	 * @since 1.1
152
	 *
153
	 * {@inheritDoc}
154
	 */
155 8
	public function execute() {
156
157 8
		$urlComponents = $this->getUrlComponents( $this->getOption( ONOI_HTTP_REQUEST_URL ) );
158
159 8
		$resource = $this->getResourceFromSocketClient(
160 8
			$urlComponents,
161 8
			$this->getOption( ONOI_HTTP_REQUEST_SOCKET_CLIENT_FLAGS )
162 8
		);
163
164
		// Defaults
165 8
		$requestResponse = new RequestResponse( array(
166 8
			'host' => $urlComponents['host'],
167 8
			'port' => $urlComponents['port'],
168 8
			'path' => $urlComponents['path'],
169 8
			'responseMessage'  => "$this->errstr ($this->errno)",
170 8
			'followedLocation' => false,
171 8
			'wasCompleted' => false,
172 8
			'wasAccepted'  => false,
173 8
			'connectionFailure' => -1,
174 8
			'requestProcTime'   => microtime( true )
175 8
		) );
176
177 8
		$this->doMakeSocketRequest(
178 8
			$urlComponents,
179 8
			$resource,
180
			$requestResponse
181 8
		);
182
183 8
		$this->postResponseToCallback(
184
			$requestResponse
185 8
		);
186
187 8
		return $requestResponse->get( 'wasCompleted' );
188
	}
189
190 8
	protected function getResourceFromSocketClient( $urlComponents, $flags ) {
191
192 8
		$context = stream_context_create();
193 8
		stream_context_set_option( $context, 'ssl', 'verify_peer', $this->getOption( ONOI_HTTP_REQUEST_SSL_VERIFYPEER ) );
194
195 8
		$resource = @stream_socket_client(
196 8
			$urlComponents['host'] . ':'. $urlComponents['port'],
197 8
			$this->errno,
198 8
			$this->errstr,
199 8
			$this->getOption( ONOI_HTTP_REQUEST_CONNECTION_TIMEOUT ),
200 8
			$flags,
201
			$context
202 8
		);
203
204 8
		return $resource;
205
	}
206
207 8
	private function doMakeSocketRequest( $urlComponents, $resource, RequestResponse &$requestResponse ) {
208
209 8
		if ( !$resource ) {
210 6
			return;
211
		}
212
213 2
		$requestCompleted = false;
214 2
		stream_set_timeout( $resource, $this->getOption( ONOI_HTTP_REQUEST_CONNECTION_TIMEOUT ) );
215
216
		$httpMessage = (
217 2
			strtoupper( $this->getOption( ONOI_HTTP_REQUEST_METHOD ) ) . " " . $urlComponents['path'] . " HTTP/1.1\r\n" .
218 2
			"Host: " . $urlComponents['host'] . "\r\n" .
219 2
			"Content-Type: " . $this->getOption( ONOI_HTTP_REQUEST_CONTENT_TYPE ) . "\r\n" .
220 2
			"Content-Length: " . strlen( $this->getOption( ONOI_HTTP_REQUEST_CONTENT ) ) . "\r\n" .
221 2
			"Connection: Close\r\n\r\n" .
222 2
			$this->getOption( ONOI_HTTP_REQUEST_CONTENT )
223 2
		);
224
225
		// Sometimes a response can fail (busy server, timeout etc.), try as for
226
		// as many times the FAILURE_REPEAT option dictates
227 2
		for ( $repeats = 0; $repeats < $this->getOption( ONOI_HTTP_REQUEST_CONNECTION_FAILURE_REPEAT ); $repeats++ ) {
228 2
			if ( $requestCompleted = @fwrite( $resource, $httpMessage ) ) {
229
				break;
230
			}
231 2
		}
232
233
		// Fetch the acknowledge response
234 2
		$this->lastTransferInfo = @fgets( $resource );
235 2
		@fclose( $resource );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
236
237 2
		$requestResponse->set( 'responseMessage', $this->lastTransferInfo );
238 2
		$requestResponse->set( 'followedLocation', $this->followedLocation );
239 2
		$requestResponse->set( 'wasCompleted', (bool)$requestCompleted );
240 2
		$requestResponse->set( 'wasAccepted', (bool)preg_match( '#^HTTP/\d\.\d 202 #', $this->lastTransferInfo ) );
241 2
		$requestResponse->set( 'connectionFailure', $repeats );
242 2
	}
243
244 13
	private function getUrlComponents( $url ) {
245
246 13
		$urlComponents = parse_url( $url );
247 13
		$port = 80;
248
249 13
		$urlComponents['scheme'] = isset( $urlComponents['scheme'] ) ? $urlComponents['scheme'] : '';
250 13
		$urlComponents['host'] = isset( $urlComponents['host'] ) ? $urlComponents['host'] : '';
251 13
		$urlComponents['path'] = isset( $urlComponents['path'] ) ? $urlComponents['path'] : '';
252
253 13
		if ( isset( $urlComponents['scheme'] ) && $urlComponents['scheme'] == 'https' ) {
254 2
			$urlComponents['host'] = "tls://" . $urlComponents['host'];
255 2
			$port = 443;
256 2
		}
257
258 13
		if ( $this->getOption( ONOI_HTTP_REQUEST_SOCKET_PORT ) !== null ) {
259
			$port = $this->getOption( ONOI_HTTP_REQUEST_SOCKET_PORT );
260
		}
261
262 13
		$urlComponents['port'] = isset( $urlComponents['port'] ) ? $urlComponents['port'] : $port;
263
264
		return array(
265 13
			'scheme' => $urlComponents['scheme'],
266 13
			'host'   => $urlComponents['host'],
267 13
			'port'   => $urlComponents['port'],
268 13
			'path'   => $urlComponents['path']
269 13
		);
270
	}
271
272 8
	private function postResponseToCallback( RequestResponse $requestResponse ) {
273
274 8
		$requestResponse->set( 'requestProcTime', microtime( true ) - $requestResponse->get( 'requestProcTime' ) );
275
276 8
		if ( is_callable( $this->getOption( ONOI_HTTP_REQUEST_ON_COMPLETED_CALLBACK ) ) && $requestResponse->get( 'wasCompleted' ) ) {
277
			call_user_func_array( $this->getOption( ONOI_HTTP_REQUEST_ON_COMPLETED_CALLBACK ), array( $requestResponse ) );
278
		}
279
280 8
		if ( is_callable( $this->getOption( ONOI_HTTP_REQUEST_ON_FAILED_CALLBACK ) ) && ( !$requestResponse->get( 'wasCompleted' ) || !$requestResponse->get( 'wasAccepted' ) ) ) {
281 4
			call_user_func_array( $this->getOption( ONOI_HTTP_REQUEST_ON_FAILED_CALLBACK ), array( $requestResponse ) );
282 4
		}
283 8
	}
284
285 5
	private function tryToFindFollowLocation( $resource, $urlComponents ) {
286
287
		// http://stackoverflow.com/questions/3799134/how-to-get-final-url-after-following-http-redirections-in-pure-php
288
289 5
		$response = '';
290 5
		$host = str_replace( 'tls://', '', $urlComponents['host'] );
291
292 5
		while( !feof( $resource ) ) $response .= fread( $resource, 8192 );
293
294
		// Only try to match a 301 message (Moved Permanently)
295 5
		if ( preg_match( '#^HTTP/\d\.\d 301 #', $response ) && preg_match('/^Location: (.+?)$/m', $response, $matches ) ) {
296 4
			$this->followedLocation = true;
297
298 4
			if ( substr( $matches[1], 0, 1 ) == "/" ) {
299 3
				return $urlComponents['scheme'] . "://" . $host . trim( $matches[1] );
300
			}
301
302 1
			return trim( $matches[1] );
303
		}
304
305
		// Return the URL we know
306 1
		return $this->getOption( ONOI_HTTP_REQUEST_URL );
307
	}
308
309
}
310