Completed
Push — 3.x ( d6a6ad...56ebd1 )
by Jeroen
61:31 queued 11s
created

ResponseFactory::finalizeResponsePreparation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Elgg\Http;
4
5
use Elgg\Ajax\Service as AjaxService;
6
use Elgg\EventsService;
7
use Elgg\PluginHooksService;
8
use ElggEntity;
9
use InvalidArgumentException;
10
use InvalidParameterException;
11
use Symfony\Component\HttpFoundation\Cookie;
12
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
13
use Symfony\Component\HttpFoundation\Response;
14
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
15
use Symfony\Component\HttpFoundation\JsonResponse;
16
17
/**
18
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
19
 *
20
 * @since 2.3
21
 * @access private
22
 */
23
class ResponseFactory {
24
25
	/**
26
	 * @var Request
27
	 */
28
	private $request;
29
30
	/**
31
	 * @var AjaxService
32
	 */
33
	private $ajax;
34
35
	/**
36
	 * @var PluginHooksService
37
	 */
38
	private $hooks;
39
40
	/**
41
	 * @var ResponseTransport
42
	 */
43
	private $transport;
44
45
	/**
46
	 * @var Response|false
47
	 */
48
	private $response_sent = false;
49
50
	/**
51
	 * @var ResponseHeaderBag
52
	 */
53
	private $headers;
54
	
55
	/**
56
	 * @var EventsService
57
	 */
58
	private $events;
59
60
	/**
61
	 * Constructor
62
	 *
63
	 * @param Request            $request   HTTP request
64
	 * @param PluginHooksService $hooks     Plugin hooks service
65
	 * @param AjaxService        $ajax      AJAX service
66
	 * @param ResponseTransport  $transport Response transport
67
	 * @param EventsService      $events    Events service
68
	 */
69 291
	public function __construct(Request $request, PluginHooksService $hooks, AjaxService $ajax, ResponseTransport $transport, EventsService $events) {
70 291
		$this->request = $request;
71 291
		$this->hooks = $hooks;
72 291
		$this->ajax = $ajax;
73 291
		$this->transport = $transport;
74 291
		$this->events = $events;
75
		
76 291
		$this->headers = new ResponseHeaderBag();
77 291
	}
78
79
	/**
80
	 * Sets headers to apply to all responses being sent
81
	 *
82
	 * @param string $name    Header name
83
	 * @param string $value   Header value
84
	 * @param bool   $replace Replace existing headers
85
	 * @return void
86
	 */
87 35
	public function setHeader($name, $value, $replace = true) {
88 35
		$this->headers->set($name, $value, $replace);
89 35
	}
90
91
	/**
92
	 * Set a cookie, but allow plugins to customize it first.
93
	 *
94
	 * To customize all cookies, register for the 'init:cookie', 'all' event.
95
	 *
96
	 * @param \ElggCookie $cookie The cookie that is being set
97
	 * @return bool
98
	 */
99 7
	public function setCookie(\ElggCookie $cookie) {
100 7
		if (!$this->events->trigger('init:cookie', $cookie->name, $cookie)) {
101
			return false;
102
		}
103
104 7
		$symfony_cookie = new Cookie(
105 7
			$cookie->name,
106 7
			$cookie->value,
107 7
			$cookie->expire,
108 7
			$cookie->path,
109 7
			$cookie->domain,
110 7
			$cookie->secure,
111 7
			$cookie->httpOnly
112
		);
113
114 7
		$this->headers->setCookie($symfony_cookie);
115 7
		return true;
116
	}
117
118
	/**
119
	 * Get headers set to apply to all responses
120
	 *
121
	 * @param bool $remove_existing Remove existing headers found in headers_list()
122
	 * @return ResponseHeaderBag
123
	 */
124 146
	public function getHeaders($remove_existing = true) {
125
		// Add headers that have already been set by underlying views
126
		// e.g. viewtype page shells set content-type headers
127 146
		$headers_list = headers_list();
128 146
		foreach ($headers_list as $header) {
129
			if (stripos($header, 'HTTP/1.1') !== false) {
130
				continue;
131
			}
132
133
			list($name, $value) = explode(':', $header, 2);
134
			$this->setHeader($name, ltrim($value), false);
135
			if ($remove_existing) {
136
				header_remove($name);
137
			}
138
		}
139
140 146
		return $this->headers;
141
	}
142
143
	/**
144
	 * Creates an HTTP response
145
	 *
146
	 * @param mixed   $content The response content
147
	 * @param integer $status  The response status code
148
	 * @param array   $headers An array of response headers
149
	 *
150
	 * @return Response
151
	 * @throws InvalidArgumentException
152
	 */
153 103
	public function prepareResponse($content = '', $status = 200, array $headers = []) {
154 103
		$header_bag = $this->getHeaders();
155 103
		$header_bag->add($headers);
156
		
157 103
		$response = new Response($content, $status, $header_bag->all());
158
		
159 103
		return $this->finalizeResponsePreparation($response, $header_bag);
160
	}
161
162
	/**
163
	 * Creates a redirect response
164
	 *
165
	 * @param string  $url     URL to redirect to
166
	 * @param integer $status  The status code (302 by default)
167
	 * @param array   $headers An array of response headers (Location is always set to the given URL)
168
	 *
169
	 * @return SymfonyRedirectResponse
170
	 * @throws InvalidArgumentException
171
	 */
172 20
	public function prepareRedirectResponse($url, $status = 302, array $headers = []) {
173 20
		$header_bag = $this->getHeaders();
174 20
		$header_bag->add($headers);
175
		
176 20
		$response = new SymfonyRedirectResponse($url, $status, $header_bag->all());
177
		
178 19
		return $this->finalizeResponsePreparation($response, $header_bag);
179
	}
180
	
181
	/**
182
	 * Creates an JSON response
183
	 *
184
	 * @param mixed   $content The response content
185
	 * @param integer $status  The response status code
186
	 * @param array   $headers An array of response headers
187
	 *
188
	 * @return JsonResponse
189
	 * @throws InvalidArgumentException
190
	 */
191 23
	public function prepareJsonResponse($content = '', $status = 200, array $headers = []) {
192 23
		$header_bag = $this->getHeaders();
193 23
		$header_bag->add($headers);
194
		
195
		/**
196
		 * Removing Content-Type header because in some cases content-type headers were already set
197
		 * This is a problem when serving a cachable view (for example a .css) in ajax/view
198
		 *
199
		 * @see https://github.com/Elgg/Elgg/issues/9794
200
		 */
201 23
		$header_bag->remove('Content-Type');
202
		
203 23
		$response = new JsonResponse($content, $status, $header_bag->all());
204
		
205 23
		return $this->finalizeResponsePreparation($response, $header_bag);
206
	}
207
	
208
	/**
209
	 * Last preparations on a response
210
	 *
211
	 * @param Response          $response The response to prepare
212
	 * @param ResponseHeaderBag $headers  Header container with additional content
213
	 *
214
	 * @return Response
215
	 * @todo revisit this when upgrading to Symfony/HttpFoundation v3.3+
216
	 */
217 143
	private function finalizeResponsePreparation(Response $response, ResponseHeaderBag $headers) {
218
		// Cookies aren't part of the headers, need to copy manualy
219 143
		foreach ($headers->getCookies() as $cookie) {
220 4
			$response->headers->setCookie($cookie);
221
		}
222
		
223 143
		$response->prepare($this->request);
224
		
225 143
		return $response;
226
	}
227
228
	/**
229
	 * Send a response
230
	 *
231
	 * @param Response $response Response object
232
	 * @return Response|false
233
	 */
234 147
	public function send(Response $response) {
235
236 147
		if ($this->response_sent) {
237 35
			if ($this->response_sent !== $response) {
238 4
				_elgg_services()->logger->error('Unable to send the following response: ' . PHP_EOL
239 4
						. (string) $response . PHP_EOL
240 4
						. 'because another response has already been sent: ' . PHP_EOL
241 35
						. (string) $this->response_sent);
242
			}
243
		} else {
244 147
			if (!$this->events->triggerBefore('send', 'http_response', $response)) {
245
				return false;
246
			}
247
248 147
			$request = $this->request;
249 147
			$method = $request->getRealMethod() ? : 'GET';
250 147
			$path = $request->getElggPath();
251
252 147
			_elgg_services()->logger->notice("Responding to {$method} {$path}");
253 147
			if (!$this->transport->send($response)) {
254
				return false;
255
			}
256
257 147
			$this->events->triggerAfter('send', 'http_response', $response);
258 147
			$this->response_sent = $response;
259
			
260 147
			$this->closeSession();
261
		}
262
263 147
		return $this->response_sent;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $this->response_sent also could return the type true which is incompatible with the documented return type Symfony\Component\HttpFoundation\Response|false.
Loading history...
264
	}
265
266
	/**
267
	 * Returns a response that was sent to the client
268
	 *
269
	 * @return Response|false
270
	 */
271 138
	public function getSentResponse() {
272 138
		return $this->response_sent;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $this->response_sent also could return the type boolean which is incompatible with the documented return type Symfony\Component\HttpFoundation\Response|false.
Loading history...
273
	}
274
275
	/**
276
	 * Send HTTP response
277
	 *
278
	 * @param ResponseBuilder $response ResponseBuilder instance
279
	 *                                  An instance of an ErrorResponse, OkResponse or RedirectResponse
280
	 * @return Response
281
	 * @throws \InvalidParameterException
282
	 */
283 135
	public function respond(ResponseBuilder $response) {
284
285 135
		$response_type = $this->parseContext();
286 135
		$response = $this->hooks->trigger('response', $response_type, $response, $response);
287 135
		if (!$response instanceof ResponseBuilder) {
288
			throw new InvalidParameterException("Handlers for 'response','$response_type' plugin hook must "
289
			. "return an instanceof " . ResponseBuilder::class);
290
		}
291
292 135
		if ($response->isNotModified()) {
293
			return $this->send($this->prepareResponse('', ELGG_HTTP_NOT_MODIFIED));
294
		}
295
296 135
		$is_xhr = $this->request->isXmlHttpRequest();
297
298 135
		$is_action = false;
299 135
		if (0 === strpos($response_type, 'action:')) {
300 23
			$is_action = true;
301
		}
302
303 135
		if ($is_action && $response->getForwardURL() === null) {
304
			// actions must always set a redirect url
305
			$response->setForwardURL(REFERRER);
306
		}
307
308 135
		if ($response->getForwardURL() === REFERRER) {
309 19
			$response->setForwardURL($this->request->headers->get('Referer'));
310
		}
311
312 135
		if ($response->getForwardURL() !== null && !$is_xhr) {
313
			// non-xhr requests should issue a forward if redirect url is set
314
			// unless it's an error, in which case we serve an error page
315 19
			if ($this->isAction() || (!$response->isClientError() && !$response->isServerError())) {
316 14
				$response->setStatusCode(ELGG_HTTP_FOUND);
317
			}
318
		}
319
320 135
		if ($is_xhr && ($is_action || $this->ajax->isAjax2Request())) {
321 33
			if (!$this->ajax->isAjax2Request()) {
322
				// xhr actions using legacy ajax API should return 200 with wrapped data
323 6
				$response->setStatusCode(ELGG_HTTP_OK);
324
			}
325
326
			// Actions always respond with JSON on xhr calls
327 33
			$headers = $response->getHeaders();
328 33
			$headers['Content-Type'] = 'application/json; charset=UTF-8';
329 33
			$response->setHeaders($headers);
330
331 33
			if ($response->isOk()) {
332 26
				$response->setContent($this->wrapAjaxResponse($response->getContent(), $response->getForwardURL()));
333
			}
334
		}
335
336 135
		$content = $this->stringify($response->getContent());
337 135
		$status_code = $response->getStatusCode();
338 135
		$headers = $response->getHeaders();
339
340 135
		if ($response->isRedirection()) {
341 17
			$redirect_url = $response->getForwardURL();
342 17
			return $this->redirect($redirect_url, $status_code);
343
		}
344
345 121
		if ($this->ajax->isReady() && $response->isSuccessful()) {
346 20
			return $this->respondFromContent($content, $status_code, $headers);
347
		}
348
349 101
		if ($response->isClientError() || $response->isServerError() || $response instanceof ErrorResponse) {
350 20
			return $this->respondWithError($content, $status_code, $headers);
351
		}
352
353 81
		return $this->respondFromContent($content, $status_code, $headers);
354
	}
355
356
	/**
357
	 * Send error HTTP response
358
	 *
359
	 * @param string $error       Error message
360
	 * @param int    $status_code HTTP status code
361
	 * @param array  $headers     HTTP headers (will be discarded on AJAX requests)
362
	 * @return Response
363
	 * @throws \InvalidParameterException
364
	 */
365 20
	public function respondWithError($error, $status_code = ELGG_HTTP_BAD_REQUEST, array $headers = []) {
366 20
		if ($this->ajax->isReady()) {
367 7
			return $this->send($this->ajax->respondWithError($error, $status_code));
368
		}
369
370 13
		if ($this->isXhr()) {
371
			// xhr calls to non-actions (e.g. ajax/view or ajax/form) need to receive proper HTTP status code
372 5
			return $this->send($this->prepareResponse($error, $status_code, $headers));
373
		}
374
375 8
		$forward_url = $this->getSiteRefererUrl();
376
377 8
		if (!$this->isAction()) {
378
			$params = [
379 8
				'current_url' => current_page_url(),
380 8
				'forward_url' => $forward_url,
381
			];
382
			// For BC, let plugins serve their own error page
383
			// @see elgg_error_page_handler
384 8
			$forward_reason = (string) $status_code;
385
386 8
			$this->hooks->trigger('forward', $forward_reason, $params, $forward_url);
387
388 8
			if ($this->response_sent) {
389
				// Response was sent from a forward hook
390
				return $this->response_sent;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $this->response_sent also could return the type true which is incompatible with the documented return type Symfony\Component\HttpFoundation\Response.
Loading history...
391
			}
392
393 8
			if (elgg_view_exists('resources/error')) {
394 7
				$params['type'] = $forward_reason;
395 7
				$params['params']['error'] = $error;
396 7
				$error_page = elgg_view_resource('error', $params);
397
			} else {
398 1
				$error_page = $error;
399
			}
400
401 8
			return $this->send($this->prepareResponse($error_page, $status_code));
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->send($this...or_page, $status_code)) could also return false which is incompatible with the documented return type Symfony\Component\HttpFoundation\Response. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
402
		}
403
404
		$forward_url = $this->makeSecureForwardUrl($forward_url);
405
		return $this->send($this->prepareRedirectResponse($forward_url));
406
	}
407
408
	/**
409
	 * Send OK response
410
	 *
411
	 * @param string $content     Response body
412
	 * @param int    $status_code HTTP status code
413
	 * @param array  $headers     HTTP headers (will be discarded for AJAX requests)
414
	 *
415
	 * @return Response|false
416
	 */
417 101
	public function respondFromContent($content = '', $status_code = ELGG_HTTP_OK, array $headers = []) {
418
419 101
		if ($this->ajax->isReady()) {
420 20
			$hook_type = $this->parseContext();
421
			// $this->ajax->setStatusCode($status_code);
422 20
			return $this->send($this->ajax->respondFromOutput($content, $hook_type));
423
		}
424
425 81
		return $this->send($this->prepareResponse($content, $status_code, $headers));
426
	}
427
428
	/**
429
	 * Wraps response content in an Ajax2 compatible format
430
	 *
431
	 * @param string $content     Response content
432
	 * @param string $forward_url Forward URL
433
	 * @return string
434
	 */
435 26
	public function wrapAjaxResponse($content = '', $forward_url = null) {
436
437 26
		if (!$this->ajax->isAjax2Request()) {
438 6
			return $this->wrapLegacyAjaxResponse($content, $forward_url);
439
		}
440
441 20
		$content = $this->stringify($content);
442
443 20
		if ($forward_url === REFERRER) {
1 ignored issue
show
introduced by
The condition $forward_url === Elgg\Http\REFERRER is always false.
Loading history...
444
			$forward_url = $this->getSiteRefererUrl();
445
		}
446
447
		$params = [
448 20
			'value' => '',
449 20
			'current_url' => current_page_url(),
450 20
			'forward_url' => elgg_normalize_url($forward_url),
451
		];
452
453 20
		$params['value'] = $this->ajax->decodeJson($content);
454
455 20
		return $this->stringify($params);
456
	}
457
458
	/**
459
	 * Wraps content for compability with legacy Elgg ajax calls
460
	 *
461
	 * @param string $content     Response content
462
	 * @param string $forward_url Forward URL
463
	 * @return string
464
	 */
465 11
	public function wrapLegacyAjaxResponse($content = '', $forward_url = REFERRER) {
466
467 11
		$content = $this->stringify($content);
468
469 11
		if ($forward_url === REFERRER) {
470
			$forward_url = $this->getSiteRefererUrl();
471
		}
472
473
		// always pass the full structure to avoid boilerplate JS code.
474
		$params = [
475 11
			'output' => '',
476 11
			'status' => 0,
477
			'system_messages' => [
478
				'error' => [],
479
				'success' => []
480
			],
481 11
			'current_url' => current_page_url(),
482 11
			'forward_url' => elgg_normalize_url($forward_url),
483
		];
484
485 11
		$params['output'] = $this->ajax->decodeJson($content);
486
487
		// Grab any system messages so we can inject them via ajax too
488 11
		$system_messages = _elgg_services()->systemMessages->dumpRegister();
489
490 11
		if (isset($system_messages['success'])) {
491 2
			$params['system_messages']['success'] = $system_messages['success'];
492
		}
493
494 11
		if (isset($system_messages['error'])) {
495 3
			$params['system_messages']['error'] = $system_messages['error'];
496 3
			$params['status'] = -1;
497
		}
498
499 11
		$response_type = $this->parseContext();
500 11
		list($service, $name) = explode(':', $response_type);
501
		$context = [
502 11
			$service => $name,
503
		];
504 11
		$params = $this->hooks->trigger('output', 'ajax', $context, $params);
505
506 11
		return $this->stringify($params);
507
	}
508
509
	/**
510
	 * Prepares a redirect response
511
	 *
512
	 * @param string $forward_url Redirection URL
513
	 * @param mixed  $status_code HTTP status code or forward reason
514
	 * @return SymfonyRedirectResponse
515
	 * @throws InvalidParameterException
516
	 */
517 31
	public function redirect($forward_url = REFERRER, $status_code = ELGG_HTTP_FOUND) {
518 31
		$location = $forward_url;
519
		
520 31
		if ($forward_url === REFERRER) {
521 3
			$forward_url = $this->getSiteRefererUrl();
522
		}
523
524 31
		$forward_url = $this->makeSecureForwardUrl($forward_url);
525
526
		// allow plugins to rewrite redirection URL
527
		$params = [
528 31
			'current_url' => current_page_url(),
529 31
			'forward_url' => $forward_url,
530 31
			'location' => $location,
531
		];
532
533 31
		$forward_reason = (string) $status_code;
534
535 31
		$forward_url = $this->hooks->trigger('forward', $forward_reason, $params, $forward_url);
536
		
537 31
		if ($this->response_sent) {
538
			// Response was sent from a forward hook
539
			// Clearing handlers to void infinite loops
540 1
			return $this->response_sent;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $this->response_sent returns the type true which is incompatible with the documented return type Symfony\Component\HttpFoundation\RedirectResponse.
Loading history...
541
		}
542
543 31
		if ($forward_url === REFERRER) {
544
			$forward_url = $this->getSiteRefererUrl();
545
		}
546
547 31
		if (!is_string($forward_url)) {
548
			throw new InvalidParameterException("'forward', '$forward_reason' hook must return a valid redirection URL");
549
		}
550
551 31
		$forward_url = $this->makeSecureForwardUrl($forward_url);
552
553 31
		switch ($status_code) {
554 31
			case 'system':
555 31
			case 'csrf':
556
				$status_code = ELGG_HTTP_OK;
557
				break;
558 31
			case 'admin':
559 31
			case 'login':
560 31
			case 'member':
561 31
			case 'walled_garden':
562
			default :
563 31
				$status_code = (int) $status_code;
564 31
				if (!$status_code || $status_code < 100 || $status_code > 599) {
565
					$status_code = ELGG_HTTP_SEE_OTHER;
566
				}
567 31
				break;
568
		}
569
570 31
		if ($this->isXhr()) {
571 12
			if ($status_code < 100 || ($status_code >= 300 && $status_code <= 399) || $status_code > 599) {
572
				// We only want to preserve OK and error codes
573
				// Redirect responses should be converted to OK responses as this is an XHR request
574 9
				$status_code = ELGG_HTTP_OK;
575
			}
576 12
			$output = ob_get_clean();
577 12
			if (!$this->isAction() && !$this->ajax->isAjax2Request()) {
578
				// legacy ajax calls are always OK
579
				// actions are wrapped by ResponseFactory::respond()
580 5
				$status_code = ELGG_HTTP_OK;
581 5
				$output = $this->wrapLegacyAjaxResponse($output, $forward_url);
582
			}
583
584 12
			$response = new OkResponse($output, $status_code, $forward_url);
585 12
			$headers = $response->getHeaders();
586 12
			$headers['Content-Type'] = 'application/json; charset=UTF-8';
587 12
			$response->setHeaders($headers);
588 12
			return $this->respond($response);
589
		}
590
591 19
		if ($this->isAction()) {
592
			// actions should always redirect on non xhr-calls
593 7
			if (!is_int($status_code) || $status_code < 300 || $status_code > 399) {
1 ignored issue
show
introduced by
The condition is_int($status_code) is always true.
Loading history...
594
				$status_code = ELGG_HTTP_SEE_OTHER;
595
			}
596
		}
597
598 19
		$response = new OkResponse('', $status_code, $forward_url);
599 19
		if ($response->isRedirection()) {
600 17
			return $this->send($this->prepareRedirectResponse($forward_url, $status_code));
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->send($this...ard_url, $status_code)) returns the type false which is incompatible with the documented return type Symfony\Component\HttpFoundation\RedirectResponse.
Loading history...
601
		}
602 2
		return $this->respond($response);
603
	}
604
605
	/**
606
	 * Parses response type to be used as plugin hook type
607
	 * @return string
608
	 */
609 147
	public function parseContext() {
610
611 147
		$segments = $this->request->getUrlSegments();
612
613 147
		$identifier = array_shift($segments);
614 147
		switch ($identifier) {
615 147
			case 'ajax' :
616 21
				$page = array_shift($segments);
617 21
				if ($page === 'view') {
618 14
					$view = implode('/', $segments);
619 14
					return "view:$view";
620 7
				} else if ($page === 'form') {
621 5
					$form = implode('/', $segments);
622 5
					return "form:$form";
623
				}
624 2
				array_unshift($segments, $page);
625 2
				break;
626
627 126
			case 'action' :
628 27
				$action = implode('/', $segments);
629 27
				return "action:$action";
630
		}
631
632 102
		array_unshift($segments, $identifier);
633 102
		$path = implode('/', $segments);
634 102
		return "path:$path";
635
	}
636
637
	/**
638
	 * Check if the request is an XmlHttpRequest
639
	 * @return bool
640
	 */
641 44
	public function isXhr() {
642 44
		return $this->request->isXmlHttpRequest();
643
	}
644
645
	/**
646
	 * Check if the requested path is an action
647
	 * @return bool
648
	 */
649 38
	public function isAction() {
650 38
		if (0 === strpos($this->parseContext(), 'action:')) {
651 11
			return true;
652
		}
653 28
		return false;
654
	}
655
656
	/**
657
	 * Normalizes content into serializable data by walking through arrays
658
	 * and objectifying Elgg entities
659
	 *
660
	 * @param mixed $content Data to normalize
661
	 * @return mixed
662
	 */
663 135
	public function normalize($content = '') {
664 135
		if ($content instanceof ElggEntity) {
665 1
			$content = (array) $content->toObject();
666
		}
667 135
		if (is_array($content)) {
668 35
			foreach ($content as $key => $value) {
669 35
				$content[$key] = $this->normalize($value);
670
			}
671
		}
672 135
		return $content;
673
	}
674
675
	/**
676
	 * Stringify/serialize response data
677
	 *
678
	 * Casts objects implementing __toString method to strings
679
	 * Serializes non-scalar values to JSON
680
	 *
681
	 * @param mixed $content Content to serialize
682
	 * @return string
683
	 */
684 135
	public function stringify($content = '') {
685 135
		$content = $this->normalize($content);
686 135
		if (empty($content) || (is_object($content) && is_callable($content, '__toString'))) {
0 ignored issues
show
Bug introduced by
'__toString' of type string is incompatible with the type boolean expected by parameter $syntax_only of is_callable(). ( Ignorable by Annotation )

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

686
		if (empty($content) || (is_object($content) && is_callable($content, /** @scrutinizer ignore-type */ '__toString'))) {
Loading history...
687 73
			return (string) $content;
688
		}
689 72
		if (is_scalar($content)) {
690 68
			return $content;
691
		}
692 35
		return json_encode($content, ELGG_JSON_ENCODING);
693
	}
694
695
	/**
696
	 * Replaces response transport
697
	 *
698
	 * @param ResponseTransport $transport Transport interface
699
	 * @return void
700
	 */
701 24
	public function setTransport(ResponseTransport $transport) {
702 24
		$this->transport = $transport;
703 24
	}
704
	
705
	/**
706
	 * Ensures the referer header is a site url
707
	 *
708
	 * @return string
709
	 */
710 11
	protected function getSiteRefererUrl() {
711 11
		$unsafe_url = $this->request->headers->get('Referer');
712 11
		$safe_url = elgg_normalize_site_url($unsafe_url);
713 11
		if ($safe_url !== false) {
714 10
			return $safe_url;
715
		}
716
		
717 1
		return '';
718
	}
719
	
720
	/**
721
	 * Ensure the url has a valid protocol for browser use
722
	 *
723
	 * @param string $url url the secure
724
	 *
725
	 * @return string
726
	 */
727 31
	protected function makeSecureForwardUrl($url) {
728 31
		$url = elgg_normalize_url($url);
729 31
		if (!preg_match('/^(http|https|ftp|sftp|ftps):\/\//', $url)) {
730
			return elgg_get_site_url();
731
		}
732
		
733 31
		return $url;
734
	}
735
	
736
	/**
737
	 * Closes the session
738
	 *
739
	 * Force closing the session so session is saved to the database before headers are sent
740
	 * preventing race conditions with session data
741
	 *
742
	 * @see https://github.com/Elgg/Elgg/issues/12348
743
	 *
744
	 * @return void
745
	 */
746 147
	protected function closeSession() {
747 147
		$session = elgg_get_session();
748 147
		if ($session->isStarted()) {
749 121
			$session->save();
750
		}
751 147
	}
752
}
753