Permalink
Browse files

feature(ajax): Adds a new elgg/Ajax AMD module with unified API

All `elgg/Ajax` methods transparently handle server-side messages/errors, as
well as return the raw value (the JSON wrapper is not exposed to the user
as in `elgg.action()`.)

A client plugin hook allows modifying the request data, and both client
and server-side hooks can modify the returned data.

You can test this by enabling the developers plugin and entering this at the
JavaScript console: `require(["developers/ajax_demo"])`

The ajax views system can now serve static views that weren't specifically
registered for ajax. This is mainly for fetching static `.html` view files.

Fixes #8323
  • Loading branch information...
mrclay committed May 6, 2015
1 parent a17f78e commit 2a132ae87749f1aec8f9f78d7106cee982e7cce9
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -83,10 +83,6 @@ System hooks
**output:after, layout**
In ``elgg_view_layout()``, filters the return value of the layout view.
**output, ajax**
Triggered in the ajax forward hook that is called for ajax requests. Allows plugins to alter the
output returned, including the forward URL, system messages, and errors.
**parameters, menu:<menu_name>**
Triggered by ``elgg_view_menu()``. Used to change menu variables (like sort order) before it is generated.
@@ -186,6 +182,30 @@ Action hooks
**forward, <reason>**
Filter the URL to forward a user to when ``forward($url, $reason)`` is called.
.. _guides/hooks-list#ajax:
Ajax
====
**ajax_response, \***
When the ``elgg/Ajax`` AMD module is used, this hook gives access to the response object
(``\Elgg\Services\AjaxResponse``) so it can be altered/extended. The hook type depends on
the method call:
================ ====================
elgg/Ajax method plugin hook type
================ ====================
action() action:<action_name>
path() path:<url_path>
view() view:<view_name>
form() form:<action_name>
================ ====================
**output, ajax**
This filters the JSON output wrapper returned to the legacy ajax API (``elgg.ajax``, ``elgg.action``, etc.).
Plugins can alter the output, forward URL, system messages, and errors. For the ``elgg/Ajax`` AMD module,
use the ``ajax_response`` hook documented above.
.. _guides/hooks-list#permission-hooks:
Permission hooks
@@ -272,6 +292,17 @@ Permission hooks
**get_sql, access**
Filters the SQL clauses used in ``_elgg_get_access_where_sql()``.
Routing
=======
**route, <identifier>**
Allows altering the parameters used to route requests. ``identifier`` is the first URL segment,
registered with ``elgg_register_page_handler()``.
**ajax_response, path:<path>**
Filters ajax responses before they're sent back to the ``elgg/Ajax`` module. This hook type will
only be used if the path did not start with "action/" or "ajax/".
.. _guides/hooks-list#views:
Views
@@ -292,6 +323,12 @@ Views
**head, page**
In ``elgg_view_page()``, filters ``$vars['head']``
**ajax_response, view:<view>**
Filters ``ajax/view/`` responses before they're sent back to the ``elgg/Ajax`` module.
**ajax_response, form:<action>**
Filters ``ajax/form/`` responses before they're sent back to the ``elgg/Ajax`` module.
Files
=====
@@ -546,3 +546,9 @@ Available hooks
**config, ckeditor**
This filters the CKEditor config object. Register for this hook in a plugin boot module. The defaults can be seen in the module ``elgg/ckeditor/config``.
**ajax_request_data, \***
This filters request data sent by the ``elgg/Ajax`` module. See :doc:`ajax` for details.
**ajax_response_data, \***
This filters the response data returned to users of the ``elgg/Ajax`` module. See :doc:`ajax` for details.
@@ -1,5 +1,6 @@
<?php
namespace Elgg;
use Elgg\Services\AjaxResponse;
/**
* WARNING: API IN FLUX. DO NOT USE DIRECTLY.
@@ -329,74 +330,81 @@ public function exists($action) {
* @access private
*/
public function ajaxForwardHook($hook, $reason, $return, $params) {
if (elgg_is_xhr()) {
// always pass the full structure to avoid boilerplate JS code.
$params = array_merge($params, array(
'output' => '',
'status' => 0,
'system_messages' => array(
'error' => array(),
'success' => array()
)
));
//grab any data echo'd in the action
$output = ob_get_clean();
//Avoid double-encoding in case data is json
$json = json_decode($output);
if (isset($json)) {
$params['output'] = $json;
} else {
$params['output'] = $output;
}
//Grab any system messages so we can inject them via ajax too
$system_messages = _elgg_services()->systemMessages->dumpRegister();
if (isset($system_messages['success'])) {
$params['system_messages']['success'] = $system_messages['success'];
}
if (isset($system_messages['error'])) {
$params['system_messages']['error'] = $system_messages['error'];
$params['status'] = -1;
}
if (!elgg_is_xhr()) {
return;
}
if ($reason == 'walled_garden') {
$reason = '403';
}
$httpCodes = array(
'400' => 'Bad Request',
'401' => 'Unauthorized',
'403' => 'Forbidden',
'404' => 'Not Found',
'407' => 'Proxy Authentication Required',
'500' => 'Internal Server Error',
'503' => 'Service Unavailable',
);
if (isset($httpCodes[$reason])) {
header("HTTP/1.1 $reason {$httpCodes[$reason]}", true);
}
// grab any data echo'd in the action
$output = ob_get_clean();
$context = array('action' => $this->currentAction);
$params = _elgg_services()->hooks->trigger('output', 'ajax', $context, $params);
// Check the requester can accept JSON responses, if not fall back to
// returning JSON in a plain-text response. Some libraries request
// JSON in an invisible iframe which they then read from the iframe,
// however some browsers will not accept the JSON MIME type.
$http_accept = _elgg_services()->request->server->get('HTTP_ACCEPT');
if (stripos($http_accept, 'application/json') === false) {
header("Content-type: text/plain");
if ($reason == 'walled_garden') {
$reason = '403';
}
$http_codes = array(
'400' => 'Bad Request',
'401' => 'Unauthorized',
'403' => 'Forbidden',
'404' => 'Not Found',
'407' => 'Proxy Authentication Required',
'500' => 'Internal Server Error',
'503' => 'Service Unavailable',
);
$ajax_api = _elgg_services()->ajax;
if ($ajax_api->isReady()) {
if (isset($http_codes[$reason])) {
$ajax_api->respondWithError($http_codes[$reason], $reason);
} else {
header("Content-type: application/json");
$ajax_api->respondFromOutput($output, "action:{$this->currentAction}");
}
echo json_encode($params);
exit;
}
// legacy XHR behavior
if (isset($http_codes[$reason])) {
header("HTTP/1.1 $reason {$http_codes[$reason]}", true);
}
// always pass the full structure to avoid boilerplate JS code.
$params = array_merge($params, array(
'output' => '',
'status' => 0,
'system_messages' => array(
'error' => array(),
'success' => array()
)
));
$params['output'] = $ajax_api->decodeJson($output);
//Grab any system messages so we can inject them via ajax too
$system_messages = _elgg_services()->systemMessages->dumpRegister();
if (isset($system_messages['success'])) {
$params['system_messages']['success'] = $system_messages['success'];
}
if (isset($system_messages['error'])) {
$params['system_messages']['error'] = $system_messages['error'];
$params['status'] = -1;
}
$context = array('action' => $this->currentAction);
$params = _elgg_services()->hooks->trigger('output', 'ajax', $context, $params);
// Check the requester can accept JSON responses, if not fall back to
// returning JSON in a plain-text response. Some libraries request
// JSON in an invisible iframe which they then read from the iframe,
// however some browsers will not accept the JSON MIME type.
$http_accept = _elgg_services()->request->server->get('HTTP_ACCEPT');
if (stripos($http_accept, 'application/json') === false) {
header("Content-type: text/plain");
} else {
header("Content-type: application/json");
}
echo json_encode($params);
exit;
}
/**
@@ -0,0 +1,61 @@
<?php
namespace Elgg\Ajax;
/**
* JSON endpoint response
*
* @since 1.12.0
* @access private
* @internal Devs should type hint the interface
*/
class Response implements \Elgg\Services\AjaxResponse {
private $ttl = 0;
private $data = null;
private $cancelled = false;
/**
* {@inheritdoc}
*/
public function setTtl($ttl = 0) {
$this->ttl = (int)max($ttl, 0);
return $this;
}
/**
* {@inheritdoc}
*/
public function getTtl() {
return $this->ttl;
}
/**
* {@inheritdoc}
*/
public function setData($data) {
$this->data = $data;
return $this;
}
/**
* {@inheritdoc}
*/
public function getData() {
return $this->data;
}
/**
* {@inheritdoc}
*/
public function cancel() {
$this->cancelled = true;
return $this;
}
/**
* {@inheritdoc}
*/
public function isCancelled() {
return $this->cancelled;
}
}
Oops, something went wrong.

0 comments on commit 2a132ae

Please sign in to comment.