Elgg  Version 2.3
ResponseFactory.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Http;
4 
5 use Elgg\Ajax\Service as AjaxService;
10 use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
13 
21 
25  private $request;
26 
30  private $ajax;
31 
35  private $hooks;
36 
40  private $transport;
41 
45  private $response_sent = false;
46 
50  private $headers;
51 
60  public function __construct(Request $request, PluginHooksService $hooks, AjaxService $ajax, ResponseTransport $transport) {
61  $this->request = $request;
62  $this->hooks = $hooks;
63  $this->ajax = $ajax;
64  $this->transport = $transport;
65  $this->headers = new ResponseHeaderBag();
66  }
67 
76  public function setHeader($name, $value, $replace = true) {
77  $this->headers->set($name, $value, $replace);
78  }
79 
86  public function getHeaders($remove_existing = true) {
87  // Add headers that have already been set by underlying views
88  // e.g. viewtype page shells set content-type headers
89  $headers_list = headers_list();
90  foreach ($headers_list as $header) {
91  if (stripos($header, 'HTTP/1.1') !== false) {
92  continue;
93  }
94 
95  list($name, $value) = explode(':', $header, 2);
96  $this->setHeader($name, ltrim($value), false);
97  if ($remove_existing) {
98  header_remove($name);
99  }
100  }
101 
102  return $this->headers;
103  }
104 
113  public function prepareResponse($content = '', $status = 200, array $headers = array()) {
114  $header_bag = $this->getHeaders();
115  $header_bag->add($headers);
116  $response = new Response($content, $status, $header_bag->all());
117  $response->prepare($this->request);
118  return $response;
119  }
120 
130  public function prepareRedirectResponse($url, $status = 302, array $headers = array()) {
131  $header_bag = $this->getHeaders();
132  $header_bag->add($headers);
133  $response = new SymfonyRedirectResponse($url, $status, $header_bag->all());
134  $response->prepare($this->request);
135  return $response;
136  }
137 
144  public function send(Response $response) {
145 
146  if ($this->response_sent) {
147  if ($this->response_sent !== $response) {
148  _elgg_services()->logger->error('Unable to send the following response: ' . PHP_EOL
149  . (string) $response . PHP_EOL
150  . 'because another response has already been sent: ' . PHP_EOL
151  . (string) $this->response_sent);
152  }
153  } else {
154  if (!elgg_trigger_before_event('send', 'http_response', $response)) {
155  return false;
156  }
157 
158  if (!$this->transport->send($response)) {
159  return false;
160  }
161 
162  elgg_trigger_after_event('send', 'http_response', $response);
163  $this->response_sent = $response;
164  }
165 
166  return $this->response_sent;
167  }
168 
173  public function getSentResponse() {
174  return $this->response_sent;
175  }
176 
184  public function respond(ResponseBuilder $response) {
185 
186  $response_type = $this->parseContext();
187  $response = $this->hooks->trigger('response', $response_type, $response, $response);
188  if (!$response instanceof ResponseBuilder) {
189  throw new InvalidParameterException("Handlers for 'response','$response_type' plugin hook must "
190  . "return an instanceof " . ResponseBuilder::class);
191  }
192 
193  if ($response->isNotModified()) {
194  return $this->send($this->prepareResponse('', ELGG_HTTP_NOT_MODIFIED));
195  }
196 
197  $is_xhr = $this->request->isXmlHttpRequest();
198 
199  $is_action = false;
200  if (0 === strpos($response_type, 'action:')) {
201  $is_action = true;
202  }
203 
204  if ($is_action && $response->getForwardURL() === null) {
205  // actions must always set a redirect url
206  $response->setForwardURL(REFERRER);
207  }
208 
209  if ($response->getForwardURL() === REFERRER) {
210  $response->setForwardURL($this->request->headers->get('Referer'));
211  }
212 
213  if ($response->getForwardURL() !== null && !$is_xhr) {
214  // non-xhr requests should issue a forward if redirect url is set
215  // unless it's an error, in which case we serve an error page
216  if ($this->isAction() || (!$response->isClientError() && !$response->isServerError())) {
217  $response->setStatusCode(ELGG_HTTP_FOUND);
218  }
219  }
220 
221  if ($is_xhr && $is_action && !$this->ajax->isAjax2Request()) {
222  // xhr actions using legacy ajax API should return 200 with wrapped data
223  $response->setStatusCode(ELGG_HTTP_OK);
224  $response->setContent($this->wrapLegacyAjaxResponse($response->getContent(), $response->getForwardURL()));
225  }
226 
227  if ($is_xhr && $is_action) {
228  // Actions always respond with JSON on xhr calls
229  $headers = $response->getHeaders();
230  $headers['Content-Type'] = 'application/json; charset=UTF-8';
231  $response->setHeaders($headers);
232  }
233 
234  $content = $this->stringify($response->getContent());
235  $status_code = $response->getStatusCode();
236  $headers = $response->getHeaders();
237 
238  if ($response->isRedirection()) {
239  $redirect_url = $response->getForwardURL();
240  return $this->redirect($redirect_url, $status_code);
241  }
242 
243  if ($this->ajax->isReady() && $response->isSuccessful()) {
244  return $this->respondFromContent($content, $status_code, $headers);
245  }
246 
247  if ($response->isClientError() || $response->isServerError() || $response instanceof ErrorResponse) {
248  return $this->respondWithError($content, $status_code, $headers);
249  }
250 
251  return $this->respondFromContent($content, $status_code, $headers);
252  }
253 
263  public function respondWithError($error, $status_code = ELGG_HTTP_BAD_REQUEST, array $headers = []) {
264  if ($this->ajax->isReady()) {
265  return $this->send($this->ajax->respondWithError($error, $status_code));
266  }
267 
268  if ($this->isXhr()) {
269  // xhr calls to non-actions (e.g. ajax/view or ajax/form) need to receive proper HTTP status code
270  return $this->send($this->prepareResponse($error, $status_code, $headers));
271  }
272 
273  $forward_url = $this->request->headers->get('Referer');
274 
275  if (!$this->isAction()) {
276  $params = [
277  'current_url' => current_page_url(),
278  'forward_url' => $forward_url,
279  ];
280  // For BC, let plugins serve their own error page
281  // @see elgg_error_page_handler
282  $forward_reason = (string) $status_code;
283 
284  $forward_url = $this->hooks->trigger('forward', $forward_reason, $params, $forward_url);
285 
286  if ($this->response_sent) {
287  // Response was sent from a forward hook
288  return $this->response_sent;
289  }
290 
291  $params['type'] = $forward_reason;
292  $error_page = elgg_view_resource('error', $params);
293  return $this->send($this->prepareResponse($error_page, $status_code));
294  }
295 
297  return $this->send($this->prepareRedirectResponse($forward_url));
298  }
299 
308  public function respondFromContent($content = '', $status_code = ELGG_HTTP_OK, array $headers = []) {
309 
310  if ($this->ajax->isReady()) {
311  $hook_type = $this->parseContext();
312  // $this->ajax->setStatusCode($status_code);
313  return $this->send($this->ajax->respondFromOutput($content, $hook_type));
314  }
315 
316  return $this->send($this->prepareResponse($content, $status_code, $headers));
317  }
318 
327 
328  $content = $this->stringify($content);
329 
330  if ($forward_url === REFERRER) {
331  $forward_url = $this->request->headers->get('Referer');
332  }
333 
334  // always pass the full structure to avoid boilerplate JS code.
335  $params = [
336  'output' => '',
337  'status' => 0,
338  'system_messages' => [
339  'error' => [],
340  'success' => []
341  ],
342  'current_url' => current_page_url(),
343  'forward_url' => elgg_normalize_url($forward_url),
344  ];
345 
346  $params['output'] = $this->ajax->decodeJson($content);
347 
348  // Grab any system messages so we can inject them via ajax too
349  $system_messages = _elgg_services()->systemMessages->dumpRegister();
350 
351  if (isset($system_messages['success'])) {
352  $params['system_messages']['success'] = $system_messages['success'];
353  }
354 
355  if (isset($system_messages['error'])) {
356  $params['system_messages']['error'] = $system_messages['error'];
357  $params['status'] = -1;
358  }
359 
360  $response_type = $this->parseContext();
361  list($service, $name) = explode(':', $response_type);
362  $context = [
363  $service => $name,
364  ];
365  $params = $this->hooks->trigger('output', 'ajax', $context, $params);
366 
367  return $this->stringify($params);
368  }
369 
378  public function redirect($forward_url = REFERRER, $status_code = ELGG_HTTP_FOUND) {
379  $location = $forward_url;
380 
381  // get a secure referrer url
382  $secure_referrer = function () {
383  $unsafe_url = $this->request->headers->get('Referer');
384  $safe_url = elgg_normalize_site_url($unsafe_url);
385  if ($safe_url !== false) {
386  return $safe_url;
387  }
388 
389  return '';
390  };
391 
392  // validate we forward to a (browser) supported url
393  $secure_forward_url = function($forward_url) {
395  if (!preg_match('/^(http|https|ftp|sftp|ftps):\/\//', $forward_url)) {
397  }
398 
399  return $forward_url;
400  };
401 
402  if ($forward_url === REFERRER) {
403  $forward_url = $secure_referrer();
404  }
405 
406  $forward_url = $secure_forward_url($forward_url);
407 
408  // allow plugins to rewrite redirection URL
409  $params = [
410  'current_url' => current_page_url(),
411  'forward_url' => $forward_url,
412  'location' => $location,
413  ];
414 
415  $forward_reason = (string) $status_code;
416 
417  $forward_url = $this->hooks->trigger('forward', $forward_reason, $params, $forward_url);
418 
419  if ($this->response_sent) {
420  // Response was sent from a forward hook
421  // Clearing handlers to void infinite loops
422  return $this->response_sent;
423  }
424 
425  if ($forward_url === REFERRER) {
426  $forward_url = $secure_referrer();
427  }
428 
429  if (!is_string($forward_url)) {
430  throw new InvalidParameterException("'forward', '$forward_reason' hook must return a valid redirection URL");
431  }
432 
433  $forward_url = $secure_forward_url($forward_url);
434 
435  switch ($status_code) {
436  case 'system':
437  case 'csrf':
438  $status_code = ELGG_HTTP_OK;
439  break;
440  case 'admin':
441  case 'login':
442  case 'member':
443  case 'walled_garden':
444  default :
445  $status_code = (int) $status_code;
446  if (!$status_code || $status_code < 100 || $status_code > 599) {
447  $status_code = ELGG_HTTP_SEE_OTHER;
448  }
449  break;
450  }
451 
452  if ($this->isXhr()) {
453  if ($status_code < 100 || ($status_code >= 300 && $status_code <= 399) || $status_code > 599) {
454  // We only want to preserve OK and error codes
455  // Redirect responses should be converted to OK responses as this is an XHR request
456  $status_code = ELGG_HTTP_OK;
457  }
458  $output = ob_get_clean();
459  if (!$this->isAction() && !$this->ajax->isAjax2Request()) {
460  // legacy ajax calls are always OK
461  // actions are wrapped by ResponseFactory::respond()
462  $status_code = ELGG_HTTP_OK;
464  }
465 
466  $response = new OkResponse($output, $status_code, $forward_url);
467  $headers = $response->getHeaders();
468  $headers['Content-Type'] = 'application/json; charset=UTF-8';
469  $response->setHeaders($headers);
470  return $this->respond($response);
471  }
472 
473  if ($this->isAction()) {
474  // actions should always redirect on non xhr-calls
475  if (!is_int($status_code) || $status_code < 300 || $status_code > 399) {
476  $status_code = ELGG_HTTP_SEE_OTHER;
477  }
478  }
479 
480  $response = new OkResponse('', $status_code, $forward_url);
481  if ($response->isRedirection()) {
482  return $this->send($this->prepareRedirectResponse($forward_url, $status_code));
483  }
484  return $this->respond($response);
485  }
486 
491  public function parseContext() {
492 
493  $segments = $this->request->getUrlSegments();
494 
495  $identifier = array_shift($segments);
496  switch ($identifier) {
497  case 'ajax' :
498  $page = array_shift($segments);
499  if ($page === 'view') {
500  $view = implode('/', $segments);
501  return "view:$view";
502  } else if ($page === 'form') {
503  $form = implode('/', $segments);
504  return "form:$form";
505  }
506  array_unshift($segments, $page);
507  break;
508 
509  case 'action' :
510  $action = implode('/', $segments);
511  return "action:$action";
512  }
513 
514  array_unshift($segments, $identifier);
515  $path = implode('/', $segments);
516  return "path:$path";
517  }
518 
523  public function isXhr() {
524  return $this->request->isXmlHttpRequest();
525  }
526 
531  public function isAction() {
532  if (0 === strpos($this->parseContext(), 'action:')) {
533  return true;
534  }
535  return false;
536  }
537 
545  public function normalize($content = '') {
546  if ($content instanceof ElggEntity) {
547  $content = (array) $content->toObject();
548  }
549  if (is_array($content)) {
550  foreach ($content as $key => $value) {
551  $content[$key] = $this->normalize($value);
552  }
553  }
554  return $content;
555  }
556 
566  public function stringify($content = '') {
567  $content = $this->normalize($content);
568  if (empty($content) || (is_object($content) && is_callable([$content, '__toString']))) {
569  return (string) $content;
570  }
571  if (is_scalar($content)) {
572  return $content;
573  }
574  return json_encode($content, ELGG_JSON_ENCODING);
575  }
576 
577 }
$header
Definition: full.php:21
const ELGG_JSON_ENCODING
Default JSON encoding.
Definition: elgglib.php:2192
$view
Definition: crop.php:34
WARNING: API IN FLUX.
if(!$owner||!($owner instanceof ElggUser)||!$owner->canEdit()) $error
Definition: upload.php:14
HTTP response builder interface.
$action
Definition: full.php:133
Elgg HTTP request.
Definition: Request.php:12
respondWithError($error, $status_code=ELGG_HTTP_BAD_REQUEST, array $headers=[])
Send error HTTP response.
$context
Definition: add.php:11
const ELGG_HTTP_FOUND
Definition: elgglib.php:2143
if(!$entity->delete()) $forward_url
Definition: delete.php:37
getStatusCode()
Returns status code.
isRedirection()
Check if response is redirection.
elgg_normalize_url($url)
Definition: output.php:280
getContent()
Returns response body.
if($guid==elgg_get_logged_in_user_guid()) $name
Definition: delete.php:21
elgg_view_resource($name, array $vars=[])
Render a resource view.
Definition: views.php:510
redirect($forward_url=REFERRER, $status_code=ELGG_HTTP_FOUND)
Prepares a redirect response.
isAction()
Check if the requested path is an action.
const ELGG_HTTP_SEE_OTHER
Definition: elgglib.php:2144
getHeaders($remove_existing=true)
Get headers set to apply to all responses.
Error response builder.
isNotModified()
Check if response has been modified.
elgg_normalize_site_url($unsafe_url)
From untrusted input, get a site URL safe for forwarding.
Definition: output.php:326
$path
Definition: details.php:88
$value
Definition: longtext.php:42
current_page_url()
Returns the current page&#39;s complete URL.
Definition: input.php:65
stringify($content= '')
Stringify/serialize response data.
prepareResponse($content= '', $status=200, array $headers=array())
Creates an HTTP response.
getSentResponse()
Returns a response that was sent to the client.
elgg_trigger_before_event($event, $object_type, $object=null)
Trigger a "Before event" indicating a process is about to begin.
Definition: elgglib.php:635
Models the Ajax API service.
Definition: Service.php:20
$url
Definition: exceptions.php:24
const ELGG_HTTP_BAD_REQUEST
Definition: elgglib.php:2150
$params
Definition: login.php:72
HTTP response transport interface.
const REFERRER
Definition: elgglib.php:2113
$form
Definition: settings.php:18
isServerError()
Check if response is server error.
elgg ajax
Wrapper function for jQuery.ajax which ensures that the url being called is relative to the elgg site...
Definition: ajax.js:19
$key
Definition: summary.php:34
isXhr()
Check if the request is an XmlHttpRequest.
isSuccessful()
Check if response is successful.
wrapLegacyAjaxResponse($content= '', $forward_url=REFERRER)
Wraps content for compability with legacy Elgg ajax calls.
elgg_get_site_url($site_guid=0)
Get the URL for the current (or specified) site.
setHeader($name, $value, $replace=true)
Sets headers to apply to all responses being sent.
_elgg_services(\Elgg\Di\ServiceProvider $services=null)
Get the global service provider.
Definition: autoloader.php:17
isClientError()
Check if response is client error.
const ELGG_HTTP_NOT_MODIFIED
Definition: elgglib.php:2145
setForwardURL($forward_url=REFERRER)
Sets redirect URL.
$content
Set robots.txt action.
Definition: set_robots.php:6
send(Response $response)
Send a response.
const ELGG_HTTP_OK
Definition: elgglib.php:2131
parseContext()
Parses response type to be used as plugin hook type.
class
Definition: placeholder.php:21
elgg_trigger_after_event($event, $object_type, $object=null)
Trigger an "After event" indicating a process has finished.
Definition: elgglib.php:654
setHeaders(array $headers=[])
Sets additional response headers.
setContent($content= '')
Sets response body.
getHeaders()
Returns additional response headers.
prepareRedirectResponse($url, $status=302, array $headers=array())
Creates a redirect response.
$output
Definition: item.php:10
respondFromContent($content= '', $status_code=ELGG_HTTP_OK, array $headers=[])
Send OK response.
respond(ResponseBuilder $response)
Send HTTP response.
Response builder.
Definition: OkResponse.php:10
elgg ElggEntity
Definition: ElggEntity.js:16
http free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:5
normalize($content= '')
Normalizes content into serializable data by walking through arrays and objectifying Elgg entities...
__construct(Request $request, PluginHooksService $hooks, AjaxService $ajax, ResponseTransport $transport)
Constructor.
getForwardURL()
Returns redirect URL.
setStatusCode($status_code=ELGG_HTTP_OK)
Sets response HTTP status code.