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;
7 use ElggEntity;
8 use InvalidArgumentException;
10 use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
11 use Symfony\Component\HttpFoundation\Response;
12 use Symfony\Component\HttpFoundation\ResponseHeaderBag;
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 }
$content
Set robots.txt action.
Definition: set_robots.php:6
$view
Definition: crop.php:34
if(! $owner||!($owner instanceof ElggUser)||! $owner->canEdit()) $error
Definition: upload.php:14
$params
Definition: login.php:72
$context
Definition: add.php:11
if($guid==elgg_get_logged_in_user_guid()) $name
Definition: delete.php:21
ui datepicker calendar ui state default
Definition: admin.css.php:687
Models the Ajax API service.
Definition: Service.php:20
Error response builder.
Response builder.
Definition: OkResponse.php:10
Elgg HTTP request.
Definition: Request.php:12
WARNING: API IN FLUX.
getHeaders($remove_existing=true)
Get headers set to apply to all responses.
prepareResponse($content='', $status=200, array $headers=array())
Creates an HTTP response.
stringify($content='')
Stringify/serialize response data.
redirect($forward_url=REFERRER, $status_code=ELGG_HTTP_FOUND)
Prepares a redirect response.
setHeader($name, $value, $replace=true)
Sets headers to apply to all responses being sent.
wrapLegacyAjaxResponse($content='', $forward_url=REFERRER)
Wraps content for compability with legacy Elgg ajax calls.
parseContext()
Parses response type to be used as plugin hook type.
getSentResponse()
Returns a response that was sent to the client.
normalize($content='')
Normalizes content into serializable data by walking through arrays and objectifying Elgg entities.
respondFromContent($content='', $status_code=ELGG_HTTP_OK, array $headers=[])
Send OK response.
respondWithError($error, $status_code=ELGG_HTTP_BAD_REQUEST, array $headers=[])
Send error HTTP response.
__construct(Request $request, PluginHooksService $hooks, AjaxService $ajax, ResponseTransport $transport)
Constructor.
send(Response $response)
Send a response.
isXhr()
Check if the request is an XmlHttpRequest.
isAction()
Check if the requested path is an action.
prepareRedirectResponse($url, $status=302, array $headers=array())
Creates a redirect response.
respond(ResponseBuilder $response)
Send HTTP response.
elgg_get_site_url($site_guid=0)
Get the URL for the current (or specified) site.
$form
Definition: settings.php:18
$path
Definition: details.php:88
$header
Definition: full.php:21
const ELGG_HTTP_FOUND
Definition: elgglib.php:2143
const ELGG_HTTP_OK
Definition: elgglib.php:2131
const ELGG_JSON_ENCODING
Default JSON encoding.
Definition: elgglib.php:2192
elgg_trigger_after_event($event, $object_type, $object=null)
Trigger an "After event" indicating a process has finished.
Definition: elgglib.php:654
const ELGG_HTTP_NOT_MODIFIED
Definition: elgglib.php:2145
const ELGG_HTTP_SEE_OTHER
Definition: elgglib.php:2144
elgg_trigger_before_event($event, $object_type, $object=null)
Trigger a "Before event" indicating a process is about to begin.
Definition: elgglib.php:635
const ELGG_HTTP_BAD_REQUEST
Definition: elgglib.php:2150
const REFERRER
Definition: elgglib.php:2113
_elgg_services(\Elgg\Di\ServiceProvider $services=null)
Get the global service provider.
Definition: autoloader.php:17
current_page_url()
Returns the current page's complete URL.
Definition: input.php:65
if(! $entity->delete()) $forward_url
Definition: delete.php:37
$url
Definition: exceptions.php:24
$value
Definition: longtext.php:42
HTTP response builder interface.
setStatusCode($status_code=ELGG_HTTP_OK)
Sets response HTTP status code.
isRedirection()
Check if response is redirection.
setHeaders(array $headers=[])
Sets additional response headers.
isSuccessful()
Check if response is successful.
getContent()
Returns response body.
setForwardURL($forward_url=REFERRER)
Sets redirect URL.
getStatusCode()
Returns status code.
setContent($content='')
Sets response body.
getHeaders()
Returns additional response headers.
getForwardURL()
Returns redirect URL.
isNotModified()
Check if response has been modified.
isServerError()
Check if response is server error.
isClientError()
Check if response is client error.
HTTP response transport interface.
elgg_view_resource($name, array $vars=[])
Render a resource view.
Definition: views.php:510
elgg_normalize_site_url($unsafe_url)
From untrusted input, get a site URL safe for forwarding.
Definition: output.php:326
elgg_normalize_url($url)
Definition: output.php:280
$action
Definition: full.php:133
$key
Definition: summary.php:34
$output
Definition: item.php:10