Issues (946)

engine/classes/Elgg/Http/ResponseFactory.php (3 issues)

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
 * @internal
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 331
	public function __construct(Request $request, PluginHooksService $hooks, AjaxService $ajax, ResponseTransport $transport, EventsService $events) {
70 331
		$this->request = $request;
71 331
		$this->hooks = $hooks;
72 331
		$this->ajax = $ajax;
73 331
		$this->transport = $transport;
74 331
		$this->events = $events;
75
		
76 331
		$this->headers = new ResponseHeaderBag();
77 331
	}
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 40
	public function setHeader($name, $value, $replace = true) {
88 40
		$this->headers->set($name, $value, $replace);
89 40
	}
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 152
	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 152
		$headers_list = headers_list();
128 152
		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 152
		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 109
	public function prepareResponse($content = '', $status = 200, array $headers = []) {
154 109
		$header_bag = $this->getHeaders();
155 109
		$header_bag->add($headers);
156
		
157 109
		$response = new Response($content, $status, $header_bag->all());
158
		
159 109
		$response->prepare($this->request);
160
		
161 109
		return $response;
162
	}
163
164
	/**
165
	 * Creates a redirect response
166
	 *
167
	 * @param string  $url     URL to redirect to
168
	 * @param integer $status  The status code (302 by default)
169
	 * @param array   $headers An array of response headers (Location is always set to the given URL)
170
	 *
171
	 * @return SymfonyRedirectResponse
172
	 * @throws InvalidArgumentException
173
	 */
174 20
	public function prepareRedirectResponse($url, $status = 302, array $headers = []) {
175 20
		$header_bag = $this->getHeaders();
176 20
		$header_bag->add($headers);
177
		
178 20
		$response = new SymfonyRedirectResponse($url, $status, $header_bag->all());
179
		
180 19
		$response->prepare($this->request);
181
		
182 19
		return $response;
183
	}
184
	
185
	/**
186
	 * Creates an JSON response
187
	 *
188
	 * @param mixed   $content The response content
189
	 * @param integer $status  The response status code
190
	 * @param array   $headers An array of response headers
191
	 *
192
	 * @return JsonResponse
193
	 * @throws InvalidArgumentException
194
	 */
195 23
	public function prepareJsonResponse($content = '', $status = 200, array $headers = []) {
196 23
		$header_bag = $this->getHeaders();
197 23
		$header_bag->add($headers);
198
		
199
		/**
200
		 * Removing Content-Type header because in some cases content-type headers were already set
201
		 * This is a problem when serving a cachable view (for example a .css) in ajax/view
202
		 *
203
		 * @see https://github.com/Elgg/Elgg/issues/9794
204
		 */
205 23
		$header_bag->remove('Content-Type');
206
		
207 23
		$response = new JsonResponse($content, $status, $header_bag->all());
208
		
209 23
		$response->prepare($this->request);
210
		
211 23
		return $response;
212
	}
213
214
	/**
215
	 * Send a response
216
	 *
217
	 * @param Response $response Response object
218
	 * @return Response|false
219
	 */
220 153
	public function send(Response $response) {
221
222 153
		if ($this->response_sent) {
223 35
			if ($this->response_sent !== $response) {
224 4
				_elgg_services()->logger->error('Unable to send the following response: ' . PHP_EOL
225 4
						. (string) $response . PHP_EOL
226 4
						. 'because another response has already been sent: ' . PHP_EOL
227 35
						. (string) $this->response_sent);
228
			}
229
		} else {
230 153
			if (!$this->events->triggerBefore('send', 'http_response', $response)) {
231
				return false;
232
			}
233
234 153
			$request = $this->request;
235 153
			$method = $request->getRealMethod() ? : 'GET';
236 153
			$path = $request->getElggPath();
237
238 153
			_elgg_services()->logger->notice("Responding to {$method} {$path}");
239 153
			if (!$this->transport->send($response)) {
240
				return false;
241
			}
242
243 153
			$this->events->triggerAfter('send', 'http_response', $response);
244 153
			$this->response_sent = $response;
245
			
246 153
			$this->closeSession();
247
		}
248
249 153
		return $this->response_sent;
250
	}
251
252
	/**
253
	 * Returns a response that was sent to the client
254
	 *
255
	 * @return Response|false
256
	 */
257 139
	public function getSentResponse() {
258 139
		return $this->response_sent;
259
	}
260
261
	/**
262
	 * Send HTTP response
263
	 *
264
	 * @param ResponseBuilder $response ResponseBuilder instance
265
	 *                                  An instance of an ErrorResponse, OkResponse or RedirectResponse
266
	 * @return Response
267
	 * @throws \InvalidParameterException
268
	 */
269 136
	public function respond(ResponseBuilder $response) {
270
271 136
		$response_type = $this->parseContext();
272 136
		$response = $this->hooks->trigger('response', $response_type, $response, $response);
273 136
		if (!$response instanceof ResponseBuilder) {
274
			throw new InvalidParameterException("Handlers for 'response','$response_type' plugin hook must "
275
			. "return an instanceof " . ResponseBuilder::class);
276
		}
277
278 136
		if ($response->isNotModified()) {
279
			return $this->send($this->prepareResponse('', ELGG_HTTP_NOT_MODIFIED));
280
		}
281
282 136
		$is_xhr = $this->request->isXmlHttpRequest();
283
284 136
		$is_action = false;
285 136
		if (0 === strpos($response_type, 'action:')) {
286 23
			$is_action = true;
287
		}
288
289 136
		if ($is_action && $response->getForwardURL() === null) {
290
			// actions must always set a redirect url
291
			$response->setForwardURL(REFERRER);
292
		}
293
294 136
		if ($response->getForwardURL() === REFERRER) {
295 19
			$response->setForwardURL($this->request->headers->get('Referer'));
296
		}
297
298 136
		if ($response->getForwardURL() !== null && !$is_xhr) {
299
			// non-xhr requests should issue a forward if redirect url is set
300
			// unless it's an error, in which case we serve an error page
301 19
			if ($this->isAction() || (!$response->isClientError() && !$response->isServerError())) {
302 14
				$response->setStatusCode(ELGG_HTTP_FOUND);
303
			}
304
		}
305
306 136
		if ($is_xhr && ($is_action || $this->ajax->isAjax2Request())) {
307 33
			if (!$this->ajax->isAjax2Request()) {
308
				// xhr actions using legacy ajax API should return 200 with wrapped data
309 6
				$response->setStatusCode(ELGG_HTTP_OK);
310
			}
311
312
			// Actions always respond with JSON on xhr calls
313 33
			$headers = $response->getHeaders();
314 33
			$headers['Content-Type'] = 'application/json; charset=UTF-8';
315 33
			$response->setHeaders($headers);
316
317 33
			if ($response->isOk()) {
318 26
				$response->setContent($this->wrapAjaxResponse($response->getContent(), $response->getForwardURL()));
319
			}
320
		}
321
322 136
		if ($response->isRedirection()) {
323 17
			$redirect_url = $response->getForwardURL();
324 17
			return $this->redirect($redirect_url, $response->getStatusCode());
325
		}
326
327 122
		if ($this->ajax->isReady() && $response->isSuccessful()) {
328 20
			return $this->respondFromContent($response);
329
		}
330
331 102
		if ($response->isClientError() || $response->isServerError() || $response instanceof ErrorResponse) {
332 20
			return $this->respondWithError($response);
333
		}
334
335 82
		return $this->respondFromContent($response);
336
	}
337
338
	/**
339
	 * Send error HTTP response
340
	 *
341
	 * @param ResponseBuilder $response ResponseBuilder instance
342
	 *                                  An instance of an ErrorResponse, OkResponse or RedirectResponse
343
	 *
344
	 * @return Response
345
	 * @throws \InvalidParameterException
346
	 */
347 25
	public function respondWithError(ResponseBuilder $response) {
348 25
		$error = $this->stringify($response->getContent());
349 25
		$status_code = $response->getStatusCode();
350
351 25
		if ($this->ajax->isReady()) {
352 7
			return $this->send($this->ajax->respondWithError($error, $status_code));
353
		}
354
355 18
		if ($this->isXhr()) {
356
			// xhr calls to non-actions (e.g. ajax/view or ajax/form) need to receive proper HTTP status code
357 5
			return $this->send($this->prepareResponse($error, $status_code, $response->getHeaders()));
358
		}
359
360 13
		$forward_url = $this->getSiteRefererUrl();
361
362 13
		if (!$this->isAction()) {
363
			$params = [
364 13
				'current_url' => current_page_url(),
365 13
				'forward_url' => $forward_url,
366
			];
367
			// For BC, let plugins serve their own error page
368
			// @see elgg_error_page_handler
369 13
			$forward_reason = (string) $status_code;
370
371 13
			$this->hooks->trigger('forward', $forward_reason, $params, $forward_url);
372
373 13
			if ($this->response_sent) {
374
				// Response was sent from a forward hook
375
				return $this->response_sent;
376
			}
377
378 13
			if (elgg_view_exists('resources/error')) {
379 12
				$params['type'] = $forward_reason;
380 12
				$params['exception'] = $response->getException();
0 ignored issues
show
The method getException() does not exist on Elgg\Http\ResponseBuilder. Since it exists in all sub-types, consider adding an abstract or default implementation to Elgg\Http\ResponseBuilder. ( Ignorable by Annotation )

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

380
				/** @scrutinizer ignore-call */ 
381
    $params['exception'] = $response->getException();
Loading history...
381 12
				if (!elgg_is_empty($error)) {
382 3
					$params['params']['error'] = $error;
383
				}
384 12
				$error_page = elgg_view_resource('error', $params);
385
			} else {
386 1
				$error_page = $error;
387
			}
388
389 13
			return $this->send($this->prepareResponse($error_page, $status_code));
0 ignored issues
show
Bug Best Practice introduced by Ismayil Khayredinov
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...
390
		}
391
392
		$forward_url = $this->makeSecureForwardUrl($forward_url);
393
		return $this->send($this->prepareRedirectResponse($forward_url));
394
	}
395
396
	/**
397
	 * Send OK response
398
	 *
399
	 * @param ResponseBuilder $response ResponseBuilder instance
400
	 *                                  An instance of an ErrorResponse, OkResponse or RedirectResponse
401
	 *
402
	 * @return Response|false
403
	 */
404 102
	public function respondFromContent(ResponseBuilder $response) {
405 102
		$content = $this->stringify($response->getContent());
406
		
407 102
		if ($this->ajax->isReady()) {
408 20
			$hook_type = $this->parseContext();
409 20
			return $this->send($this->ajax->respondFromOutput($content, $hook_type));
410
		}
411
412 82
		return $this->send($this->prepareResponse($content, $response->getStatusCode(), $response->getHeaders()));
413
	}
414
415
	/**
416
	 * Wraps response content in an Ajax2 compatible format
417
	 *
418
	 * @param string $content     Response content
419
	 * @param string $forward_url Forward URL
420
	 * @return string
421
	 */
422 26
	public function wrapAjaxResponse($content = '', $forward_url = null) {
423
424 26
		if (!$this->ajax->isAjax2Request()) {
425 6
			return $this->wrapLegacyAjaxResponse($content, $forward_url);
426
		}
427
428 20
		$content = $this->stringify($content);
429
430 20
		if ($forward_url === REFERRER) {
431
			$forward_url = $this->getSiteRefererUrl();
432
		}
433
434
		$params = [
435 20
			'value' => '',
436 20
			'current_url' => current_page_url(),
437 20
			'forward_url' => elgg_normalize_url($forward_url),
438
		];
439
440 20
		$params['value'] = $this->ajax->decodeJson($content);
441
442 20
		return $this->stringify($params);
443
	}
444
445
	/**
446
	 * Wraps content for compability with legacy Elgg ajax calls
447
	 *
448
	 * @param string $content     Response content
449
	 * @param string $forward_url Forward URL
450
	 * @return string
451
	 */
452 11
	public function wrapLegacyAjaxResponse($content = '', $forward_url = REFERRER) {
453
454 11
		$content = $this->stringify($content);
455
456 11
		if ($forward_url === REFERRER) {
457
			$forward_url = $this->getSiteRefererUrl();
458
		}
459
460
		// always pass the full structure to avoid boilerplate JS code.
461
		$params = [
462 11
			'output' => '',
463 11
			'status' => 0,
464
			'system_messages' => [
465
				'error' => [],
466
				'success' => []
467
			],
468 11
			'current_url' => current_page_url(),
469 11
			'forward_url' => elgg_normalize_url($forward_url),
470
		];
471
472 11
		$params['output'] = $this->ajax->decodeJson($content);
473
474
		// Grab any system messages so we can inject them via ajax too
475 11
		$system_messages = _elgg_services()->systemMessages->dumpRegister();
476
477 11
		if (isset($system_messages['success'])) {
478 2
			$params['system_messages']['success'] = $system_messages['success'];
479
		}
480
481 11
		if (isset($system_messages['error'])) {
482 3
			$params['system_messages']['error'] = $system_messages['error'];
483 3
			$params['status'] = -1;
484
		}
485
486 11
		$response_type = $this->parseContext();
487 11
		list($service, $name) = explode(':', $response_type);
488
		$context = [
489 11
			$service => $name,
490
		];
491 11
		$params = $this->hooks->trigger('output', 'ajax', $context, $params);
492
493 11
		return $this->stringify($params);
494
	}
495
496
	/**
497
	 * Prepares a redirect response
498
	 *
499
	 * @param string $forward_url Redirection URL
500
	 * @param mixed  $status_code HTTP status code or forward reason
501
	 * @return SymfonyRedirectResponse
502
	 * @throws InvalidParameterException
503
	 */
504 31
	public function redirect($forward_url = REFERRER, $status_code = ELGG_HTTP_FOUND) {
505 31
		$location = $forward_url;
506
		
507 31
		if ($forward_url === REFERRER) {
508 3
			$forward_url = $this->getSiteRefererUrl();
509
		}
510
511 31
		$forward_url = $this->makeSecureForwardUrl($forward_url);
512
513
		// allow plugins to rewrite redirection URL
514
		$params = [
515 31
			'current_url' => current_page_url(),
516 31
			'forward_url' => $forward_url,
517 31
			'location' => $location,
518
		];
519
520 31
		$forward_reason = (string) $status_code;
521
522 31
		$forward_url = $this->hooks->trigger('forward', $forward_reason, $params, $forward_url);
523
		
524 31
		if ($this->response_sent) {
525
			// Response was sent from a forward hook
526
			// Clearing handlers to void infinite loops
527 1
			return $this->response_sent;
528
		}
529
530 31
		if ($forward_url === REFERRER) {
531
			$forward_url = $this->getSiteRefererUrl();
532
		}
533
534 31
		if (!is_string($forward_url)) {
535
			throw new InvalidParameterException("'forward', '$forward_reason' hook must return a valid redirection URL");
536
		}
537
538 31
		$forward_url = $this->makeSecureForwardUrl($forward_url);
539
540 31
		switch ($status_code) {
541 31
			case 'system':
542 31
			case 'csrf':
543
				$status_code = ELGG_HTTP_OK;
544
				break;
545 31
			case 'admin':
546 31
			case 'login':
547 31
			case 'member':
548 31
			case 'walled_garden':
549
			default :
550 31
				$status_code = (int) $status_code;
551 31
				if (!$status_code || $status_code < 100 || $status_code > 599) {
552
					$status_code = ELGG_HTTP_SEE_OTHER;
553
				}
554 31
				break;
555
		}
556
557 31
		if ($this->isXhr()) {
558 12
			if ($status_code < 100 || ($status_code >= 300 && $status_code <= 399) || $status_code > 599) {
559
				// We only want to preserve OK and error codes
560
				// Redirect responses should be converted to OK responses as this is an XHR request
561 9
				$status_code = ELGG_HTTP_OK;
562
			}
563 12
			$output = ob_get_clean();
564 12
			if (!$this->isAction() && !$this->ajax->isAjax2Request()) {
565
				// legacy ajax calls are always OK
566
				// actions are wrapped by ResponseFactory::respond()
567 5
				$status_code = ELGG_HTTP_OK;
568 5
				$output = $this->wrapLegacyAjaxResponse($output, $forward_url);
569
			}
570
571 12
			$response = new OkResponse($output, $status_code, $forward_url);
572 12
			$headers = $response->getHeaders();
573 12
			$headers['Content-Type'] = 'application/json; charset=UTF-8';
574 12
			$response->setHeaders($headers);
575 12
			return $this->respond($response);
576
		}
577
578 19
		if ($this->isAction()) {
579
			// actions should always redirect on non xhr-calls
580 7
			if (!is_int($status_code) || $status_code < 300 || $status_code > 399) {
581
				$status_code = ELGG_HTTP_SEE_OTHER;
582
			}
583
		}
584
585 19
		$response = new OkResponse('', $status_code, $forward_url);
586 19
		if ($response->isRedirection()) {
587 17
			return $this->send($this->prepareRedirectResponse($forward_url, $status_code));
0 ignored issues
show
Bug Best Practice introduced by Ismayil Khayredinov
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...
588
		}
589 2
		return $this->respond($response);
590
	}
591
592
	/**
593
	 * Parses response type to be used as plugin hook type
594
	 * @return string
595
	 */
596 153
	public function parseContext() {
597
598 153
		$segments = $this->request->getUrlSegments();
599
600 153
		$identifier = array_shift($segments);
601 153
		switch ($identifier) {
602 153
			case 'ajax' :
603 21
				$page = array_shift($segments);
604 21
				if ($page === 'view') {
605 14
					$view = implode('/', $segments);
606 14
					return "view:$view";
607 7
				} else if ($page === 'form') {
608 5
					$form = implode('/', $segments);
609 5
					return "form:$form";
610
				}
611 2
				array_unshift($segments, $page);
612 2
				break;
613
614 132
			case 'action' :
615 27
				$action = implode('/', $segments);
616 27
				return "action:$action";
617
		}
618
619 108
		array_unshift($segments, $identifier);
620 108
		$path = implode('/', $segments);
621 108
		return "path:$path";
622
	}
623
624
	/**
625
	 * Check if the request is an XmlHttpRequest
626
	 * @return bool
627
	 */
628 49
	public function isXhr() {
629 49
		return $this->request->isXmlHttpRequest();
630
	}
631
632
	/**
633
	 * Check if the requested path is an action
634
	 * @return bool
635
	 */
636 43
	public function isAction() {
637 43
		if (0 === strpos($this->parseContext(), 'action:')) {
638 11
			return true;
639
		}
640 33
		return false;
641
	}
642
643
	/**
644
	 * Normalizes content into serializable data by walking through arrays
645
	 * and objectifying Elgg entities
646
	 *
647
	 * @param mixed $content Data to normalize
648
	 * @return mixed
649
	 */
650 127
	public function normalize($content = '') {
651 127
		if ($content instanceof ElggEntity) {
652
			$content = (array) $content->toObject();
653
		}
654 127
		if (is_array($content)) {
655 33
			foreach ($content as $key => $value) {
656 33
				$content[$key] = $this->normalize($value);
657
			}
658
		}
659 127
		return $content;
660
	}
661
662
	/**
663
	 * Stringify/serialize response data
664
	 *
665
	 * Casts objects implementing __toString method to strings
666
	 * Serializes non-scalar values to JSON
667
	 *
668
	 * @param mixed $content Content to serialize
669
	 * @return string
670
	 */
671 127
	public function stringify($content = '') {
672 127
		$content = $this->normalize($content);
673 127
		if (empty($content) || (is_object($content) && is_callable([$content, '__toString']))) {
674 71
			return (string) $content;
675
		}
676 66
		if (is_scalar($content)) {
677 64
			return $content;
678
		}
679 33
		return json_encode($content, ELGG_JSON_ENCODING);
680
	}
681
682
	/**
683
	 * Replaces response transport
684
	 *
685
	 * @param ResponseTransport $transport Transport interface
686
	 * @return void
687
	 */
688 24
	public function setTransport(ResponseTransport $transport) {
689 24
		$this->transport = $transport;
690 24
	}
691
	
692
	/**
693
	 * Ensures the referer header is a site url
694
	 *
695
	 * @return string
696
	 */
697 16
	protected function getSiteRefererUrl() {
698 16
		$unsafe_url = $this->request->headers->get('Referer');
699 16
		$safe_url = elgg_normalize_site_url($unsafe_url);
700 16
		if ($safe_url !== false) {
701 10
			return $safe_url;
702
		}
703
		
704 6
		return '';
705
	}
706
	
707
	/**
708
	 * Ensure the url has a valid protocol for browser use
709
	 *
710
	 * @param string $url url the secure
711
	 *
712
	 * @return string
713
	 */
714 31
	protected function makeSecureForwardUrl($url) {
715 31
		$url = elgg_normalize_url($url);
716 31
		if (!preg_match('/^(http|https|ftp|sftp|ftps):\/\//', $url)) {
717
			return elgg_get_site_url();
718
		}
719
		
720 31
		return $url;
721
	}
722
	
723
	/**
724
	 * Closes the session
725
	 *
726
	 * Force closing the session so session is saved to the database before headers are sent
727
	 * preventing race conditions with session data
728
	 *
729
	 * @see https://github.com/Elgg/Elgg/issues/12348
730
	 *
731
	 * @return void
732
	 */
733 153
	protected function closeSession() {
734 153
		$session = elgg_get_session();
735 153
		if ($session->isStarted()) {
736 126
			$session->save();
737
		}
738 153
	}
739
}
740