Completed
Push — 3.1 ( d59679...4b8741 )
by Jeroen
62:38 queued 13s
created

engine/classes/Elgg/Ajax/Service.php (1 issue)

1
<?php
2
3
namespace Elgg\Ajax;
4
5
use Elgg\Amd\Config;
6
use Elgg\Http\Request;
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
 * @internal
18
 */
19
class Service {
20
21
	/**
22
	 * @var PluginHooksService
23
	 */
24
	private $hooks;
25
26
	/**
27
	 * @var SystemMessagesService
28
	 */
29
	private $msgs;
30
31
	/**
32
	 * @var Request
33
	 */
34
	private $request;
35
36
	/**
37
	 * @var Config
38
	 */
39
	private $amd_config;
40
41
	/**
42
	 * @var bool
43
	 */
44
	private $response_sent = false;
45
46
	/**
47
	 * @var array
48
	 */
49
	private $allowed_views = [];
50
51
	/**
52
	 * Constructor
53
	 *
54
	 * @param PluginHooksService    $hooks     Hooks service
55
	 * @param SystemMessagesService $msgs      System messages service
56
	 * @param Request               $request   Http Request
57
	 * @param Config                $amdConfig AMD config
58
	 */
59 334
	public function __construct(PluginHooksService $hooks, SystemMessagesService $msgs, Request $request, Config $amdConfig) {
60 334
		$this->hooks = $hooks;
61 334
		$this->msgs = $msgs;
62 334
		$this->request = $request;
63 334
		$this->amd_config = $amdConfig;
64
65 334
		$message_filter = [$this, 'prepareResponse'];
66 334
		$this->hooks->registerHandler(AjaxResponse::RESPONSE_HOOK, 'all', $message_filter, 999);
67 334
	}
68
69
	/**
70
	 * Did the request come from the elgg/Ajax module?
71
	 *
72
	 * @return bool
73
	 */
74 127
	public function isAjax2Request() {
75 127
		$version = $this->request->headers->get('X-Elgg-Ajax-API');
76 127
		return ($version === '2');
77
	}
78
79
	/**
80
	 * Is the service ready to respond to the request?
81
	 *
82
	 * Some code paths involve multiple layers of handling (e.g. router calls actions/ajax views) so
83
	 * we must check whether the response has already been sent to avoid sending it twice. We
84
	 * can't use headers_sent() because Router needs to use output buffering.
85
	 *
86
	 * @return bool
87
	 */
88 127
	public function isReady() {
89 127
		return !$this->response_sent && $this->isAjax2Request();
90
	}
91
92
	/**
93
	 * Attempt to JSON decode the given string
94
	 *
95
	 * @param mixed $string Output string
96
	 * @return mixed
97
	 */
98 34
	public function decodeJson($string) {
99 34
		if (!is_string($string)) {
100 1
			return $string;
101
		}
102 34
		$object = json_decode($string);
103 34
		return ($object === null) ? $string : $object;
104
	}
105
106
	/**
107
	 * Send a JSON HTTP response with the given output
108
	 *
109
	 * @param mixed  $output     Output from a page/action handler
110
	 * @param string $hook_type  The hook type. If given, the response will be filtered by hook
111
	 * @param bool   $try_decode Try to convert a JSON string back to an abject
112
	 * @return JsonResponse
113
	 */
114 22
	public function respondFromOutput($output, $hook_type = '', $try_decode = true) {
115 22
		if ($try_decode) {
116 22
			$output = $this->decodeJson($output);
117
		}
118
119 22
		$api_response = new Response();
120 22
		if (is_object($output) && isset($output->value)) {
121 20
			$api_response->setData($output);
122 2
		} else if (is_array($output) && isset($output['value'])) {
123
			$api_response->setData((object) $output);
124
		} else {
125 2
			$api_response->setData((object) ['value' => $output]);
126
		}
127 22
		$api_response = $this->filterApiResponse($api_response, $hook_type);
128 21
		$response = $this->buildHttpResponse($api_response);
129
130 21
		$this->response_sent = true;
131 21
		return _elgg_services()->responseFactory->send($response);
132
	}
133
134
	/**
135
	 * Send a JSON HTTP response based on the given API response
136
	 *
137
	 * @param AjaxResponse $api_response API response
138
	 * @param string       $hook_type    The hook type. If given, the response will be filtered by hook
139
	 * @return JsonResponse
140
	 */
141 2
	public function respondFromApiResponse(AjaxResponse $api_response, $hook_type = '') {
142 2
		$api_response = $this->filterApiResponse($api_response, $hook_type);
143 2
		$response = $this->buildHttpResponse($api_response);
144
145 2
		$this->response_sent = true;
146 2
		return _elgg_services()->responseFactory->send($response);
147
	}
148
149
	/**
150
	 * Send a JSON HTTP 400 response
151
	 *
152
	 * @param string $msg    The error message (not displayed to the user)
153
	 * @param int    $status The HTTP status code
154
	 * @return JsonResponse
155
	 */
156 9
	public function respondWithError($msg = '', $status = 400) {
157 9
		$response = new JsonResponse(['error' => $msg], $status);
158
		
159
		// clear already set system messages as we respond directly with an error as message body
160 9
		$this->msgs->dumpRegister();
161
162 9
		$this->response_sent = true;
163 9
		return _elgg_services()->responseFactory->send($response);
164
	}
165
166
	/**
167
	 * Filter an AjaxResponse through a plugin hook
168
	 *
169
	 * @param AjaxResponse $api_response The API Response
170
	 * @param string       $hook_type    The hook type. If given, the response will be filtered by hook
171
	 *
172
	 * @return AjaxResponse
173
	 */
174 24
	private function filterApiResponse(AjaxResponse $api_response, $hook_type = '') {
0 ignored issues
show
Private method name "Service::filterApiResponse" must be prefixed with an underscore
Loading history...
175 24
		$api_response->setTtl($this->request->getParam('elgg_response_ttl', 0, false));
176
177 24
		if ($hook_type) {
178 20
			$hook = AjaxResponse::RESPONSE_HOOK;
179 20
			$api_response = $this->hooks->trigger($hook, $hook_type, null, $api_response);
180 20
			if (!$api_response instanceof AjaxResponse) {
181 1
				throw new RuntimeException("The value returned by hook [$hook, $hook_type] was not an ApiResponse");
182
			}
183
		}
184
185 23
		return $api_response;
186
	}
187
188
	/**
189
	 * Build a JsonResponse based on an API response object
190
	 *
191
	 * @param AjaxResponse $api_response           The API Response
192
	 * @param bool         $allow_removing_headers Alter PHP's global headers to allow caching
193
	 *
194
	 * @return JsonResponse
195
	 * @throws RuntimeException
196
	 */
197 23
	private function buildHttpResponse(AjaxResponse $api_response, $allow_removing_headers = null) {
198 23
		if ($api_response->isCancelled()) {
199 1
			return new JsonResponse(['error' => "The response was cancelled"], 400);
200
		}
201
202 22
		$response = _elgg_services()->responseFactory->prepareJsonResponse($api_response->getData());
203
204 22
		$ttl = $api_response->getTtl();
205 22
		if ($ttl > 0) {
206
			// Required to remove headers set by PHP session
207 1
			if ($allow_removing_headers) {
208
				header_remove('Expires');
209
				header_remove('Pragma');
210
				header_remove('Cache-Control');
211
			}
212
213
			// JsonRequest sets a default Cache-Control header we don't want
214 1
			$response->headers->remove('Cache-Control');
215
216 1
			$response->setClientTtl($ttl);
217
218
			// if we don't set Expires, Apache will add a far-off max-age and Expires for us.
219 1
			$response->headers->set('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + $ttl));
220
		}
221
222 22
		return $response;
223
	}
224
225
	/**
226
	 * Prepare the response with additional metadata, like system messages and required AMD modules
227
	 *
228
	 * @param \Elgg\Hook $hook "ajax_response", "all"
229
	 *
230
	 * @return AjaxResponse
231
	 * @internal
232
	 */
233 20
	public function prepareResponse(\Elgg\Hook $hook) {
234 20
		$response = $hook->getValue();
235 20
		if (!$response instanceof AjaxResponse) {
236 1
			return;
237
		}
238
239 19
		if ($this->request->getParam('elgg_fetch_messages', true)) {
240 19
			$response->getData()->_elgg_msgs = (object) $this->msgs->dumpRegister();
241
		}
242
243 19
		if ($this->request->getParam('elgg_fetch_deps', true)) {
244 19
			$response->getData()->_elgg_deps = (array) $this->amd_config->getDependencies();
245
		}
246
247 19
		return $response;
248
	}
249
250
	/**
251
	 * Register a view to be available for ajax calls
252
	 *
253
	 * @param string $view The view name
254
	 * @return void
255
	 */
256 128
	public function registerView($view) {
257 128
		$this->allowed_views[$view] = true;
258 128
	}
259
260
	/**
261
	 * Unregister a view for ajax calls
262
	 *
263
	 * @param string $view The view name
264
	 * @return void
265
	 */
266
	public function unregisterView($view) {
267
		unset($this->allowed_views[$view]);
268
	}
269
270
	/**
271
	 * Returns an array of views allowed for ajax calls
272
	 * @return string[]
273
	 */
274 17
	public function getViews() {
275 17
		return array_keys($this->allowed_views);
276
	}
277
	
278
}
279