Issues (3)

src/HTTP.php (3 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Wrapper around Nyholm\Psr7 library with a few helper methods and a basic emitter.
7
 *
8
 * For use in WordPress during ajax calls.
9
 *
10
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
11
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
12
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
13
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
14
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
15
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
16
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
17
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
18
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
19
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
20
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
21
 *
22
 * @author Glynn Quelch <[email protected]>
23
 * @license http://www.opensource.org/licenses/mit-license.html  MIT License
24
 * @package PinkCrab\HTTP
25
 */
26
27
namespace PinkCrab\HTTP;
28
29
use RuntimeException;
30
use WP_HTTP_Response;
31
use Nyholm\Psr7\Stream;
32
use Nyholm\Psr7\Request;
33
use Nyholm\Psr7\Response;
34
use InvalidArgumentException;
35
use Psr\Http\Message\UriInterface;
36
use Nyholm\Psr7\Factory\Psr17Factory;
37
use Psr\Http\Message\StreamInterface;
38
use Psr\Http\Message\RequestInterface;
39
use Psr\Http\Message\ResponseInterface;
40
use Nyholm\Psr7Server\ServerRequestCreator;
41
use Psr\Http\Message\ServerRequestInterface;
42
43
class HTTP {
44
45
	/**
46
	 * Returns the current request from glbals
47
	 *
48
	 * @uses Psr17Factory::class
49
	 * @uses ServerRequestCreator::class
50
	 *
51
	 * @return ServerRequestInterface
52
	 */
53
	public function request_from_globals(): ServerRequestInterface {
54
55
		$psr17_factory = new Psr17Factory();
56
57
		return ( new ServerRequestCreator(
0 ignored issues
show
Bug Best Practice introduced by
The expression return new Nyholm\Psr7Se...am_from_scalar($_POST)) returns the type Psr\Http\Message\MessageInterface which includes types incompatible with the type-hinted return Psr\Http\Message\ServerRequestInterface.
Loading history...
58
			$psr17_factory,
59
			$psr17_factory,
60
			$psr17_factory,
61
			$psr17_factory
62
		) )->fromGlobals()
63
			->withBody( $this->stream_from_scalar( $_POST ) );  // phpcs:ignore WordPress.Security.NonceVerification.Missing
64
	}
65
66
	/**
67
	 * Wrapper for making a PS7 request.
68
	 *
69
	 * @uses Nyholm\Psr7::Request()
70
	 * @param string                               $method  HTTP method
71
	 * @param string|UriInterface                  $uri     URI
72
	 * @param array<string, string>                $headers Request headers
73
	 * @param string|resource|StreamInterface|null $body    Request body
74
	 * @param string                               $version Protocol version
75
	 *
76
	 * @return RequestInterface
77
	 */
78
	public function psr7_request(
79
		string $method,
80
		$uri,
81
		array $headers = array(),
82
		$body = null,
83
		string $version = '1.1'
84
	): RequestInterface {
85
		return new Request( $method, $uri, $headers, $body, $version );
86
	}
87
88
	/**
89
	 * Returns a PS7 Response object.
90
	 *
91
	 * @param array<string, string>|string|resource|StreamInterface|null $body    The response body.
92
	 * @param integer                                                    $status  The response status.
93
	 * @param array<string, string>                                      $headers The response headers.
94
	 * @param string                                                     $version The response version.
95
	 * @param string|null                                                $reason  The response reason.
96
	 *
97
	 * @return ResponseInterface
98
	 */
99
	public function psr7_response(
100
		$body = null,
101
		int $status = 200,
102
		array $headers = array(),
103
		string $version = '1.1',
104
		?string $reason = null
105
	): ResponseInterface {
106
		// Json Encode if body is array or object.
107
		if ( is_array( $body ) || is_object( $body ) ) {
108
			$body = wp_json_encode( $body );
109
		}
110
111
		// If body is false, pass as null. @phpstan
112
		return new Response( $status, $headers, $body ?: null, $version, $reason );
113
	}
114
115
	/**
116
	 * Returns a WP_Rest_Response
117
	 *
118
	 * @param array<string, string>|object|string|null $data    The response data.
119
	 * @param integer                                  $status  The response status.
120
	 * @param array<string, string>                    $headers The response headers.
121
	 *
122
	 * @return WP_HTTP_Response
123
	 */
124
	public function wp_response(
125
		$data = null,
126
		int $status = 200,
127
		array $headers = array()
128
	): WP_HTTP_Response {
129
		return new WP_HTTP_Response( $data, $status, $headers );
130
	}
131
132
	/**
133
	 * Emits either a PS7 or WP_HTTP Response.
134
	 *
135
	 * @param ResponseInterface|WP_HTTP_Response|object $response The response to emit.
136
	 *
137
	 * @return void
138
	 *
139
	 * @throws InvalidArgumentException If response is not a valid type.
140
	 */
141
	public function emit_response( $response ): void {
142
143
		// Throw if not a valid response.
144
		if ( ! $response instanceof ResponseInterface
145
		&& ! $response instanceof WP_HTTP_Response ) {
146
			throw new InvalidArgumentException( 'Only ResponseInterface & WP_REST_Response responses can be emitted.' );
147
		}
148
149
		// Based on type, emit the response.
150
		if ( $response instanceof ResponseInterface ) {
151
			$this->emit_psr7_response( $response );
152
		} else {
153
			$this->emit_wp_response( $response );
154
		}
155
	}
156
157
	/**
158
	 * Emits a PSR7 response.
159
	 *
160
	 * @param ResponseInterface $response
161
	 * @return void
162
	 */
163
	public function emit_psr7_response( ResponseInterface $response ): void {
164
165
		// If headers sent, throw headers already sent.
166
		$this->headers_sent();
167
168
		// Set Set status line..
169
		$status_line = sprintf(
170
			'HTTP/%s %s %s',
171
			$response->getProtocolVersion(),
172
			$response->getStatusCode(),
173
			$response->getReasonPhrase()
174
		);
175
		header( $status_line, true );
176
177
		// Append headers.
178
		foreach ( $this->headers_with_json( $response->getHeaders() )
179
			as $name => $values ) {
180
181
			// If values are an array, join.
182
			$values = is_array( $values ) ? join( ',', $values ) : (string) $values;
183
184
			$response_header = sprintf( '%s: %s', $name, $values );
185
			header( $response_header, false );
186
		}
187
188
		// Emit body.
189
		echo $response->getBody(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
190
		return; // phpcs:ignore Squiz.PHP.NonExecutableCode.ReturnNotRequired
191
	}
192
193
	/**
194
	 * Emits a WP_HTTP Response.
195
	 *
196
	 * @param WP_HTTP_Response $response
197
	 * @return void
198
	 */
199
	public function emit_wp_response( WP_HTTP_Response $response ): void {
200
201
		// If headers sent, throw headers already sent.
202
		$this->headers_sent();
203
204
		// Append headers.
205
		foreach ( $this->headers_with_json( $response->get_headers() )
206
			as $name => $values ) {
207
			$values = is_array( $values ) ? join( ',', $values ) : (string) $values;
208
209
			$header = sprintf( '%s: %s', $name, $values );
210
211
			// Set the headers.
212
			header( $header, false );
213
		}
214
215
		// Emit body.
216
		$body = $response->get_data();
217
		print is_string( $body ) ? $body : wp_json_encode( $body ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
0 ignored issues
show
Are you sure is_string($body) ? $body : wp_json_encode($body) of type false|string can be used in print()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

217
		print /** @scrutinizer ignore-type */ is_string( $body ) ? $body : wp_json_encode( $body ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
Loading history...
218
		return; // phpcs:ignore Squiz.PHP.NonExecutableCode.ReturnNotRequired
219
	}
220
221
	/**
222
	 * Adds the JSON content type header if no header set.
223
	 *
224
	 * @param array<string, mixed> $headers
225
	 * @return array<string, mixed>
226
	 */
227
	public function headers_with_json( array $headers = array() ): array {
228
		if ( ! array_key_exists( 'Content-Type', $headers ) ) {
229
			$headers['Content-Type'] = 'application/json; charset=' . get_option( 'blog_charset' );
0 ignored issues
show
Are you sure get_option('blog_charset') of type false|mixed can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

229
			$headers['Content-Type'] = 'application/json; charset=' . /** @scrutinizer ignore-type */ get_option( 'blog_charset' );
Loading history...
230
		}
231
		return $headers;
232
	}
233
234
	/**
235
	 * Throws RunTime error if headers sent.
236
	 *
237
	 * @return void
238
	 * @throws RuntimeException
239
	 */
240
	protected function headers_sent(): void {
241
		if ( headers_sent() ) {
242
			throw new RuntimeException( 'Headers were already sent. The response could not be emitted!' );
243
		}
244
	}
245
246
	/**
247
	 * Wraps any value which can be json encoded in a StreamInterface
248
	 *
249
	 * @param string|integer|float|object|array<mixed> $data
250
	 * @return \Psr\Http\Message\StreamInterface
251
	 */
252
	public function stream_from_scalar( $data ): StreamInterface {
253
		return Stream::create( json_encode( $data ) ?: '' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
254
	}
255
}
256