Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

Service::isAjax2Request()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
ccs 3
cts 3
cp 1
crap 1
1
<?php
2
3
namespace Elgg\Ajax;
4
5
use Elgg\Amd\Config;
6
use Elgg\Http\Input;
7
use Elgg\PluginHooksService;
8
use Elgg\Services\AjaxResponse;
9
use Elgg\SystemMessagesService;
10
use RuntimeException;
11
use Symfony\Component\HttpFoundation\JsonResponse;
12
13
/**
14
 * Models the Ajax API service
15
 *
16
 * @since 1.12.0
17
 * @access private
18
 * @internal
19
 */
20
class Service {
21
22
	/**
23
	 * @var PluginHooksService
24
	 */
25
	private $hooks;
26
27
	/**
28
	 * @var SystemMessagesService
29
	 */
30
	private $msgs;
31
32
	/**
33
	 * @var Input
34
	 */
35
	private $input;
36
37
	/**
38
	 * @var Config
39
	 */
40
	private $amd_config;
41
42
	/**
43
	 * @var bool
44
	 */
45
	private $response_sent = false;
46
47
	/**
48
	 * @var array
49
	 */
50
	private $allowed_views = [];
51
52
	/**
53
	 * Constructor
54
	 *
55
	 * @param PluginHooksService    $hooks     Hooks service
56
	 * @param SystemMessagesService $msgs      System messages service
57
	 * @param Input                 $input     Input service
58
	 * @param Config                $amdConfig AMD config
59
	 */
60 172
	public function __construct(PluginHooksService $hooks, SystemMessagesService $msgs, Input $input, Config $amdConfig) {
61 172
		$this->hooks = $hooks;
62 172
		$this->msgs = $msgs;
63 172
		$this->input = $input;
64 172
		$this->amd_config = $amdConfig;
65
66 172
		$message_filter = [$this, 'prepareResponse'];
67 172
		$this->hooks->registerHandler(AjaxResponse::RESPONSE_HOOK, 'all', $message_filter, 999);
68 172
	}
69
70
	/**
71
	 * Did the request come from the elgg/Ajax module?
72
	 *
73
	 * @return bool
74
	 */
75 81
	public function isAjax2Request() {
76 81
		$version = _elgg_services()->request->headers->get('X-Elgg-Ajax-API');
77 81
		return ($version === '2');
78
	}
79
80
	/**
81
	 * Is the service ready to respond to the request?
82
	 *
83
	 * Some code paths involve multiple layers of handling (e.g. router calls actions/ajax views) so
84
	 * we must check whether the response has already been sent to avoid sending it twice. We
85
	 * can't use headers_sent() because Router needs to use output buffering.
86
	 *
87
	 * @return bool
88
	 */
89 81
	public function isReady() {
90 81
		return !$this->response_sent && $this->isAjax2Request();
91
	}
92
93
	/**
94
	 * Attempt to JSON decode the given string
95
	 *
96
	 * @param mixed $string Output string
97
	 * @return mixed
98
	 */
99 34
	public function decodeJson($string) {
100 34
		if (!is_string($string)) {
101 1
			return $string;
102
		}
103 34
		$object = json_decode($string);
104 34
		return ($object === null) ? $string : $object;
105
	}
106
107
	/**
108
	 * Send a JSON HTTP response with the given output
109
	 *
110
	 * @param mixed  $output     Output from a page/action handler
111
	 * @param string $hook_type  The hook type. If given, the response will be filtered by hook
112
	 * @param bool   $try_decode Try to convert a JSON string back to an abject
113
	 * @return JsonResponse
114
	 */
115 22
	public function respondFromOutput($output, $hook_type = '', $try_decode = true) {
116 22
		if ($try_decode) {
117 22
			$output = $this->decodeJson($output);
118
		}
119
120 22
		$api_response = new Response();
121 22
		$api_response->setData((object) [
122 22
					'value' => $output,
123
		]);
124 22
		$api_response = $this->filterApiResponse($api_response, $hook_type);
125 21
		$response = $this->buildHttpResponse($api_response);
126
127 21
		$this->response_sent = true;
128 21
		return _elgg_services()->responseFactory->send($response);
129
	}
130
131
	/**
132
	 * Send a JSON HTTP response based on the given API response
133
	 *
134
	 * @param AjaxResponse $api_response API response
135
	 * @param string       $hook_type    The hook type. If given, the response will be filtered by hook
136
	 * @return JsonResponse
137
	 */
138 2
	public function respondFromApiResponse(AjaxResponse $api_response, $hook_type = '') {
139 2
		$api_response = $this->filterApiResponse($api_response, $hook_type);
140 2
		$response = $this->buildHttpResponse($api_response);
141
142 2
		$this->response_sent = true;
143 2
		return _elgg_services()->responseFactory->send($response);
144
	}
145
146
	/**
147
	 * Send a JSON HTTP 400 response
148
	 *
149
	 * @param string $msg    The error message (not displayed to the user)
150
	 * @param int    $status The HTTP status code
151
	 * @return JsonResponse
152
	 */
153 9
	public function respondWithError($msg = '', $status = 400) {
154 9
		$response = new JsonResponse(['error' => $msg], $status);
155
156 9
		$this->response_sent = true;
157 9
		return _elgg_services()->responseFactory->send($response);
158
	}
159
160
	/**
161
	 * Filter an AjaxResponse through a plugin hook
162
	 *
163
	 * @param AjaxResponse $api_response The API Response
164
	 * @param string       $hook_type    The hook type. If given, the response will be filtered by hook
165
	 *
166
	 * @return AjaxResponse
167
	 */
168 24
	private function filterApiResponse(AjaxResponse $api_response, $hook_type = '') {
169 24
		$api_response->setTtl($this->input->get('elgg_response_ttl', 0, false));
0 ignored issues
show
Bug introduced by Steve Clay
It seems like $this->input->get('elgg_response_ttl', 0, false) can also be of type string; however, parameter $ttl of Elgg\Services\AjaxResponse::setTtl() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

169
		$api_response->setTtl(/** @scrutinizer ignore-type */ $this->input->get('elgg_response_ttl', 0, false));
Loading history...
170
171 24
		if ($hook_type) {
172 20
			$hook = AjaxResponse::RESPONSE_HOOK;
173 20
			$api_response = $this->hooks->trigger($hook, $hook_type, null, $api_response);
174 20
			if (!$api_response instanceof AjaxResponse) {
175 1
				throw new RuntimeException("The value returned by hook [$hook, $hook_type] was not an ApiResponse");
176
			}
177
		}
178
179 23
		return $api_response;
180
	}
181
182
	/**
183
	 * Build a JsonResponse based on an API response object
184
	 *
185
	 * @param AjaxResponse $api_response           The API Response
186
	 * @param bool         $allow_removing_headers Alter PHP's global headers to allow caching
187
	 *
188
	 * @return JsonResponse
189
	 * @throws RuntimeException
190
	 */
191 23
	private function buildHttpResponse(AjaxResponse $api_response, $allow_removing_headers = null) {
192 23
		if ($api_response->isCancelled()) {
193 1
			return new JsonResponse(['error' => "The response was cancelled"], 400);
194
		}
195
196 22
		$response = new JsonResponse($api_response->getData());
197
198 22
		$ttl = $api_response->getTtl();
199 22
		if ($ttl > 0) {
200
			// Required to remove headers set by PHP session
201 1
			if ($allow_removing_headers) {
202
				header_remove('Expires');
203
				header_remove('Pragma');
204
				header_remove('Cache-Control');
205
			}
206
207
			// JsonRequest sets a default Cache-Control header we don't want
208 1
			$response->headers->remove('Cache-Control');
209
210 1
			$response->setClientTtl($ttl);
211
212
			// if we don't set Expires, Apache will add a far-off max-age and Expires for us.
213 1
			$response->headers->set('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + $ttl));
214
		}
215
216 22
		return $response;
217
	}
218
219
	/**
220
	 * Prepare the response with additional metadata, like system messages and required AMD modules
221
	 *
222
	 * @param string       $hook     "ajax_response"
223
	 * @param string       $type     "all"
224
	 * @param AjaxResponse $response Ajax response
225
	 * @param array        $params   Hook params
226
	 *
227
	 * @return AjaxResponse
228
	 * @access private
229
	 * @internal
230
	 */
231 20
	public function prepareResponse($hook, $type, $response, $params) {
1 ignored issue
show
Unused Code introduced by Steve Clay
The parameter $params is not used and could be removed. ( Ignorable by Annotation )

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

231
	public function prepareResponse($hook, $type, $response, /** @scrutinizer ignore-unused */ $params) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
232 20
		if (!$response instanceof AjaxResponse) {
233 1
			return;
234
		}
235
236 19
		if ($this->input->get('elgg_fetch_messages', true)) {
237 19
			$response->getData()->_elgg_msgs = (object) $this->msgs->dumpRegister();
238
		}
239
240 19
		if ($this->input->get('elgg_fetch_deps', true)) {
241 19
			$response->getData()->_elgg_deps = (array) $this->amd_config->getDependencies();
242
		}
243
244 19
		return $response;
245
	}
246
247
	/**
248
	 * Register a view to be available for ajax calls
249
	 *
250
	 * @param string $view The view name
251
	 * @return void
252
	 */
253 46
	public function registerView($view) {
254 46
		$this->allowed_views[$view] = true;
255 46
	}
256
257
	/**
258
	 * Unregister a view for ajax calls
259
	 *
260
	 * @param string $view The view name
261
	 * @return void
262
	 */
263
	public function unregisterView($view) {
264
		unset($this->allowed_views[$view]);
265
	}
266
267
	/**
268
	 * Returns an array of views allowed for ajax calls
269
	 * @return string[]
270
	 */
271 17
	public function getViews() {
272 17
		return array_keys($this->allowed_views);
273
	}
274
	
275
}
276