Elgg  Version 3.0
ResponseFactory.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Http;
4 
5 use Elgg\Ajax\Service as AjaxService;
8 use ElggEntity;
16 
24 
28  private $request;
29 
33  private $ajax;
34 
38  private $hooks;
39 
43  private $transport;
44 
48  private $response_sent = false;
49 
53  private $headers;
54 
58  private $events;
59 
69  public function __construct(Request $request, PluginHooksService $hooks, AjaxService $ajax, ResponseTransport $transport, EventsService $events) {
70  $this->request = $request;
71  $this->hooks = $hooks;
72  $this->ajax = $ajax;
73  $this->transport = $transport;
74  $this->events = $events;
75 
76  $this->headers = new ResponseHeaderBag();
77  }
78 
87  public function setHeader($name, $value, $replace = true) {
88  $this->headers->set($name, $value, $replace);
89  }
90 
99  public function setCookie(\ElggCookie $cookie) {
100  if (!$this->events->trigger('init:cookie', $cookie->name, $cookie)) {
101  return false;
102  }
103 
104  $symfony_cookie = new Cookie(
105  $cookie->name,
106  $cookie->value,
107  $cookie->expire,
108  $cookie->path,
109  $cookie->domain,
110  $cookie->secure,
111  $cookie->httpOnly
112  );
113 
114  $this->headers->setCookie($symfony_cookie);
115  return true;
116  }
117 
124  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  $headers_list = headers_list();
128  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  return $this->headers;
141  }
142 
153  public function prepareResponse($content = '', $status = 200, array $headers = []) {
154  $header_bag = $this->getHeaders();
155  $header_bag->add($headers);
156 
157  $response = new Response($content, $status, $header_bag->all());
158 
159  return $this->finalizeResponsePreparation($response, $header_bag);
160  }
161 
172  public function prepareRedirectResponse($url, $status = 302, array $headers = []) {
173  $header_bag = $this->getHeaders();
174  $header_bag->add($headers);
175 
176  $response = new SymfonyRedirectResponse($url, $status, $header_bag->all());
177 
178  return $this->finalizeResponsePreparation($response, $header_bag);
179  }
180 
191  public function prepareJsonResponse($content = '', $status = 200, array $headers = []) {
192  $header_bag = $this->getHeaders();
193  $header_bag->add($headers);
194 
201  $header_bag->remove('Content-Type');
202 
203  $response = new JsonResponse($content, $status, $header_bag->all());
204 
205  return $this->finalizeResponsePreparation($response, $header_bag);
206  }
207 
217  private function finalizeResponsePreparation(Response $response, ResponseHeaderBag $headers) {
218  // Cookies aren't part of the headers, need to copy manualy
219  foreach ($headers->getCookies() as $cookie) {
220  $response->headers->setCookie($cookie);
221  }
222 
223  $response->prepare($this->request);
224 
225  return $response;
226  }
227 
234  public function send(Response $response) {
235 
236  if ($this->response_sent) {
237  if ($this->response_sent !== $response) {
238  _elgg_services()->logger->error('Unable to send the following response: ' . PHP_EOL
239  . (string) $response . PHP_EOL
240  . 'because another response has already been sent: ' . PHP_EOL
241  . (string) $this->response_sent);
242  }
243  } else {
244  if (!$this->events->triggerBefore('send', 'http_response', $response)) {
245  return false;
246  }
247 
248  $request = $this->request;
249  $method = $request->getRealMethod() ? : 'GET';
250  $path = $request->getElggPath();
251 
252  _elgg_services()->logger->notice("Responding to {$method} {$path}");
253  if (!$this->transport->send($response)) {
254  return false;
255  }
256 
257  $this->events->triggerAfter('send', 'http_response', $response);
258  $this->response_sent = $response;
259 
260  $this->closeSession();
261  }
262 
263  return $this->response_sent;
264  }
265 
271  public function getSentResponse() {
272  return $this->response_sent;
273  }
274 
283  public function respond(ResponseBuilder $response) {
284 
285  $response_type = $this->parseContext();
286  $response = $this->hooks->trigger('response', $response_type, $response, $response);
287  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  if ($response->isNotModified()) {
293  return $this->send($this->prepareResponse('', ELGG_HTTP_NOT_MODIFIED));
294  }
295 
296  $is_xhr = $this->request->isXmlHttpRequest();
297 
298  $is_action = false;
299  if (0 === strpos($response_type, 'action:')) {
300  $is_action = true;
301  }
302 
303  if ($is_action && $response->getForwardURL() === null) {
304  // actions must always set a redirect url
305  $response->setForwardURL(REFERRER);
306  }
307 
308  if ($response->getForwardURL() === REFERRER) {
309  $response->setForwardURL($this->request->headers->get('Referer'));
310  }
311 
312  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  if ($this->isAction() || (!$response->isClientError() && !$response->isServerError())) {
316  $response->setStatusCode(ELGG_HTTP_FOUND);
317  }
318  }
319 
320  if ($is_xhr && ($is_action || $this->ajax->isAjax2Request())) {
321  if (!$this->ajax->isAjax2Request()) {
322  // xhr actions using legacy ajax API should return 200 with wrapped data
323  $response->setStatusCode(ELGG_HTTP_OK);
324  }
325 
326  // Actions always respond with JSON on xhr calls
327  $headers = $response->getHeaders();
328  $headers['Content-Type'] = 'application/json; charset=UTF-8';
329  $response->setHeaders($headers);
330 
331  if ($response->isOk()) {
332  $response->setContent($this->wrapAjaxResponse($response->getContent(), $response->getForwardURL()));
333  }
334  }
335 
336  $content = $this->stringify($response->getContent());
337  $status_code = $response->getStatusCode();
338  $headers = $response->getHeaders();
339 
340  if ($response->isRedirection()) {
341  $redirect_url = $response->getForwardURL();
342  return $this->redirect($redirect_url, $status_code);
343  }
344 
345  if ($this->ajax->isReady() && $response->isSuccessful()) {
346  return $this->respondFromContent($content, $status_code, $headers);
347  }
348 
349  if ($response->isClientError() || $response->isServerError() || $response instanceof ErrorResponse) {
350  return $this->respondWithError($content, $status_code, $headers);
351  }
352 
353  return $this->respondFromContent($content, $status_code, $headers);
354  }
355 
365  public function respondWithError($error, $status_code = ELGG_HTTP_BAD_REQUEST, array $headers = []) {
366  if ($this->ajax->isReady()) {
367  return $this->send($this->ajax->respondWithError($error, $status_code));
368  }
369 
370  if ($this->isXhr()) {
371  // xhr calls to non-actions (e.g. ajax/view or ajax/form) need to receive proper HTTP status code
372  return $this->send($this->prepareResponse($error, $status_code, $headers));
373  }
374 
375  $forward_url = $this->getSiteRefererUrl();
376 
377  if (!$this->isAction()) {
378  $params = [
379  'current_url' => current_page_url(),
380  'forward_url' => $forward_url,
381  ];
382  // For BC, let plugins serve their own error page
383  // @see elgg_error_page_handler
384  $forward_reason = (string) $status_code;
385 
386  $this->hooks->trigger('forward', $forward_reason, $params, $forward_url);
387 
388  if ($this->response_sent) {
389  // Response was sent from a forward hook
390  return $this->response_sent;
391  }
392 
393  if (elgg_view_exists('resources/error')) {
394  $params['type'] = $forward_reason;
395  if (!elgg_is_empty($error)) {
396  $params['params']['error'] = $error;
397  }
398  $error_page = elgg_view_resource('error', $params);
399  } else {
400  $error_page = $error;
401  }
402 
403  return $this->send($this->prepareResponse($error_page, $status_code));
404  }
405 
407  return $this->send($this->prepareRedirectResponse($forward_url));
408  }
409 
419  public function respondFromContent($content = '', $status_code = ELGG_HTTP_OK, array $headers = []) {
420 
421  if ($this->ajax->isReady()) {
422  $hook_type = $this->parseContext();
423  // $this->ajax->setStatusCode($status_code);
424  return $this->send($this->ajax->respondFromOutput($content, $hook_type));
425  }
426 
427  return $this->send($this->prepareResponse($content, $status_code, $headers));
428  }
429 
437  public function wrapAjaxResponse($content = '', $forward_url = null) {
438 
439  if (!$this->ajax->isAjax2Request()) {
441  }
442 
443  $content = $this->stringify($content);
444 
445  if ($forward_url === REFERRER) {
446  $forward_url = $this->getSiteRefererUrl();
447  }
448 
449  $params = [
450  'value' => '',
451  'current_url' => current_page_url(),
452  'forward_url' => elgg_normalize_url($forward_url),
453  ];
454 
455  $params['value'] = $this->ajax->decodeJson($content);
456 
457  return $this->stringify($params);
458  }
459 
468 
469  $content = $this->stringify($content);
470 
471  if ($forward_url === REFERRER) {
472  $forward_url = $this->getSiteRefererUrl();
473  }
474 
475  // always pass the full structure to avoid boilerplate JS code.
476  $params = [
477  'output' => '',
478  'status' => 0,
479  'system_messages' => [
480  'error' => [],
481  'success' => []
482  ],
483  'current_url' => current_page_url(),
484  'forward_url' => elgg_normalize_url($forward_url),
485  ];
486 
487  $params['output'] = $this->ajax->decodeJson($content);
488 
489  // Grab any system messages so we can inject them via ajax too
490  $system_messages = _elgg_services()->systemMessages->dumpRegister();
491 
492  if (isset($system_messages['success'])) {
493  $params['system_messages']['success'] = $system_messages['success'];
494  }
495 
496  if (isset($system_messages['error'])) {
497  $params['system_messages']['error'] = $system_messages['error'];
498  $params['status'] = -1;
499  }
500 
501  $response_type = $this->parseContext();
502  list($service, $name) = explode(':', $response_type);
503  $context = [
504  $service => $name,
505  ];
506  $params = $this->hooks->trigger('output', 'ajax', $context, $params);
507 
508  return $this->stringify($params);
509  }
510 
519  public function redirect($forward_url = REFERRER, $status_code = ELGG_HTTP_FOUND) {
521 
522  if ($forward_url === REFERRER) {
523  $forward_url = $this->getSiteRefererUrl();
524  }
525 
527 
528  // allow plugins to rewrite redirection URL
529  $params = [
530  'current_url' => current_page_url(),
531  'forward_url' => $forward_url,
532  'location' => $location,
533  ];
534 
535  $forward_reason = (string) $status_code;
536 
537  $forward_url = $this->hooks->trigger('forward', $forward_reason, $params, $forward_url);
538 
539  if ($this->response_sent) {
540  // Response was sent from a forward hook
541  // Clearing handlers to void infinite loops
542  return $this->response_sent;
543  }
544 
545  if ($forward_url === REFERRER) {
546  $forward_url = $this->getSiteRefererUrl();
547  }
548 
549  if (!is_string($forward_url)) {
550  throw new InvalidParameterException("'forward', '$forward_reason' hook must return a valid redirection URL");
551  }
552 
554 
555  switch ($status_code) {
556  case 'system':
557  case 'csrf':
558  $status_code = ELGG_HTTP_OK;
559  break;
560  case 'admin':
561  case 'login':
562  case 'member':
563  case 'walled_garden':
564  default :
565  $status_code = (int) $status_code;
566  if (!$status_code || $status_code < 100 || $status_code > 599) {
567  $status_code = ELGG_HTTP_SEE_OTHER;
568  }
569  break;
570  }
571 
572  if ($this->isXhr()) {
573  if ($status_code < 100 || ($status_code >= 300 && $status_code <= 399) || $status_code > 599) {
574  // We only want to preserve OK and error codes
575  // Redirect responses should be converted to OK responses as this is an XHR request
576  $status_code = ELGG_HTTP_OK;
577  }
578  $output = ob_get_clean();
579  if (!$this->isAction() && !$this->ajax->isAjax2Request()) {
580  // legacy ajax calls are always OK
581  // actions are wrapped by ResponseFactory::respond()
582  $status_code = ELGG_HTTP_OK;
584  }
585 
586  $response = new OkResponse($output, $status_code, $forward_url);
587  $headers = $response->getHeaders();
588  $headers['Content-Type'] = 'application/json; charset=UTF-8';
589  $response->setHeaders($headers);
590  return $this->respond($response);
591  }
592 
593  if ($this->isAction()) {
594  // actions should always redirect on non xhr-calls
595  if (!is_int($status_code) || $status_code < 300 || $status_code > 399) {
596  $status_code = ELGG_HTTP_SEE_OTHER;
597  }
598  }
599 
600  $response = new OkResponse('', $status_code, $forward_url);
601  if ($response->isRedirection()) {
602  return $this->send($this->prepareRedirectResponse($forward_url, $status_code));
603  }
604  return $this->respond($response);
605  }
606 
611  public function parseContext() {
612 
613  $segments = $this->request->getUrlSegments();
614 
615  $identifier = array_shift($segments);
616  switch ($identifier) {
617  case 'ajax' :
618  $page = array_shift($segments);
619  if ($page === 'view') {
620  $view = implode('/', $segments);
621  return "view:$view";
622  } else if ($page === 'form') {
623  $form = implode('/', $segments);
624  return "form:$form";
625  }
626  array_unshift($segments, $page);
627  break;
628 
629  case 'action' :
630  $action = implode('/', $segments);
631  return "action:$action";
632  }
633 
634  array_unshift($segments, $identifier);
635  $path = implode('/', $segments);
636  return "path:$path";
637  }
638 
643  public function isXhr() {
644  return $this->request->isXmlHttpRequest();
645  }
646 
651  public function isAction() {
652  if (0 === strpos($this->parseContext(), 'action:')) {
653  return true;
654  }
655  return false;
656  }
657 
665  public function normalize($content = '') {
666  if ($content instanceof ElggEntity) {
667  $content = (array) $content->toObject();
668  }
669  if (is_array($content)) {
670  foreach ($content as $key => $value) {
671  $content[$key] = $this->normalize($value);
672  }
673  }
674  return $content;
675  }
676 
686  public function stringify($content = '') {
687  $content = $this->normalize($content);
688  if (empty($content) || (is_object($content) && is_callable($content, '__toString'))) {
689  return (string) $content;
690  }
691  if (is_scalar($content)) {
692  return $content;
693  }
694  return json_encode($content, ELGG_JSON_ENCODING);
695  }
696 
703  public function setTransport(ResponseTransport $transport) {
704  $this->transport = $transport;
705  }
706 
712  protected function getSiteRefererUrl() {
713  $unsafe_url = $this->request->headers->get('Referer');
714  $safe_url = elgg_normalize_site_url($unsafe_url);
715  if ($safe_url !== false) {
716  return $safe_url;
717  }
718 
719  return '';
720  }
721 
729  protected function makeSecureForwardUrl($url) {
731  if (!preg_match('/^(http|https|ftp|sftp|ftps):\/\//', $url)) {
732  return elgg_get_site_url();
733  }
734 
735  return $url;
736  }
737 
748  protected function closeSession() {
749  $session = elgg_get_session();
750  if ($session->isStarted()) {
751  $session->save();
752  }
753  }
754 }
elgg_view_exists($view, $viewtype= '', $recurse=true)
Returns whether the specified view exists.
Definition: views.php:205
WARNING: API IN FLUX.
HTTP response builder interface.
$action
Definition: full.php:111
Elgg HTTP request.
Definition: Request.php:17
respondWithError($error, $status_code=ELGG_HTTP_BAD_REQUEST, array $headers=[])
Send error HTTP response.
$context
Definition: add.php:8
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
if(!$entity->delete()) $forward_url
Definition: delete.php:30
getStatusCode()
Returns status code.
$params
Saves global plugin settings.
Definition: save.php:13
isRedirection()
Check if response is redirection.
setCookie(\ElggCookie $cookie)
Set a cookie, but allow plugins to customize it first.
elgg_normalize_url($url)
Definition: output.php:186
getContent()
Returns response body.
elgg_view_resource($name, array $vars=[])
Render a resource view.
Definition: views.php:423
redirect($forward_url=REFERRER, $status_code=ELGG_HTTP_FOUND)
Prepares a redirect response.
const ELGG_HTTP_OK
Definition: constants.php:60
isAction()
Check if the requested path is an action.
getHeaders($remove_existing=true)
Get headers set to apply to all responses.
Error response builder.
elgg_get_session()
Gets Elgg&#39;s session object.
Definition: sessions.php:20
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:232
$path
Definition: details.php:89
Events service.
current_page_url()
Returns the current page&#39;s complete URL.
Definition: input.php:94
stringify($content= '')
Stringify/serialize response data.
isOk()
Check if response is OK.
catch(LoginException $e) if($request->isXhr()) $output
Definition: login.php:56
getSentResponse()
Returns a response that was sent to the client.
Models the Ajax API service.
Definition: Service.php:19
wrapAjaxResponse($content= '', $forward_url=null)
Wraps response content in an Ajax2 compatible format.
elgg_is_empty($value)
Check if a value isn&#39;t empty, but allow 0 and &#39;0&#39;.
Definition: input.php:206
$page
Definition: admin.php:26
HTTP response transport interface.
$error
Bad request error.
Definition: 400.php:6
isServerError()
Check if response is server error.
if(!$owner||!$owner->canEdit()) if(!$owner->hasIcon('master')) if(!$owner->saveIconFromElggFile($owner->getIcon('master'), 'icon', $coords)) $view
Definition: crop.php:30
elgg ajax
Wrapper function for jQuery.ajax which ensures that the url being called is relative to the elgg site...
Definition: ajax.js:19
const REFERRER
Definition: constants.php:42
__construct(Request $request, PluginHooksService $hooks, AjaxService $ajax, ResponseTransport $transport, EventsService $events)
Constructor.
$header
Definition: numentities.php:30
setTransport(ResponseTransport $transport)
Replaces response transport.
prepareRedirectResponse($url, $status=302, array $headers=[])
Creates a redirect response.
isXhr()
Check if the request is an XmlHttpRequest.
const ELGG_JSON_ENCODING
Default JSON encoding.
Definition: constants.php:121
isSuccessful()
Check if response is successful.
$url
Definition: default.php:33
wrapLegacyAjaxResponse($content= '', $forward_url=REFERRER)
Wraps content for compability with legacy Elgg ajax calls.
const ELGG_HTTP_FOUND
Definition: constants.php:72
setHeader($name, $value, $replace=true)
Sets headers to apply to all responses being sent.
elgg_get_site_url()
Get the URL for the current (or specified) site, ending with "/".
getSiteRefererUrl()
Ensures the referer header is a site url.
if($container instanceof ElggGroup &&$container->guid!=elgg_get_page_owner_guid()) $key
Definition: summary.php:55
const ELGG_HTTP_NOT_MODIFIED
Definition: constants.php:74
const ELGG_HTTP_BAD_REQUEST
Definition: constants.php:79
isClientError()
Check if response is client error.
$value
Definition: debugging.php:7
makeSecureForwardUrl($url)
Ensure the url has a valid protocol for browser use.
prepareJsonResponse($content= '', $status=200, array $headers=[])
Creates an JSON response.
setForwardURL($forward_url=REFERRER)
Sets redirect URL.
$content
Set robots.txt action.
Definition: set_robots.php:6
send(Response $response)
Send a response.
parseContext()
Parses response type to be used as plugin hook type.
$location
Definition: default.php:42
class
Definition: placeholder.php:21
_elgg_services()
Get the global service provider.
Definition: elgglib.php:1292
setHeaders(array $headers=[])
Sets additional response headers.
const ELGG_HTTP_SEE_OTHER
Definition: constants.php:73
$form
List all unvalidated users in the admin area.
Definition: unvalidated.php:9
setContent($content= '')
Sets response body.
getHeaders()
Returns additional response headers.
respondFromContent($content= '', $status_code=ELGG_HTTP_OK, array $headers=[])
Send OK response.
respond(ResponseBuilder $response)
Send HTTP response.
Response builder.
Definition: OkResponse.php:10
closeSession()
Closes the session.
elgg ElggEntity
Definition: ElggEntity.js:15
prepareResponse($content= '', $status=200, array $headers=[])
Creates an HTTP response.
normalize($content= '')
Normalizes content into serializable data by walking through arrays and objectifying Elgg entities...
getForwardURL()
Returns redirect URL.
setStatusCode($status_code=ELGG_HTTP_OK)
Sets response HTTP status code.