Commit f42bca3b authored by Dries's avatar Dries
Browse files

- Patch #599804 by effulgentsia, catch: unify page, AJAX 'path', and AJAX...

- Patch #599804 by effulgentsia, catch: unify page, AJAX 'path', and AJAX 'callback' callbacks. Oh my, this is the beginning of something big.
parent dbac31e0
......@@ -291,30 +291,60 @@ function ajax_form_callback() {
$callback = $triggering_element['#ajax']['callback'];
}
if (!empty($callback) && function_exists($callback)) {
$html = $callback($form, $form_state);
// If the returned value is a string, assume it is HTML, add the status
// messages, and create a command object to return automatically. We want
// the status messages inside the new wrapper, so that they get replaced
// on subsequent AJAX calls for the same wrapper.
if (is_string($html)) {
$commands = array();
$commands[] = ajax_command_replace(NULL, $html);
$commands[] = ajax_command_prepend(NULL, theme('status_messages'));
}
// Otherwise, $html is supposed to be an array of commands, suitable for
// Drupal.ajax, so we pass it on as is. In this situation, the callback is
// doing something fancy, so let it decide how to handle status messages
// without second guessing it.
else {
$commands = $html;
}
return $callback($form, $form_state);
}
}
ajax_render($commands);
/**
* Package and send the result of a page callback to the browser as an AJAX response.
*
* @param $page_callback_result
* The result of a page callback. Can be one of:
* - NULL: to indicate no content.
* - An integer menu status constant: to indicate an error condition.
* - A string of HTML content.
* - A renderable array of content.
*/
function ajax_deliver($page_callback_result) {
$commands = array();
if (!isset($page_callback_result)) {
// Simply delivering an empty commands array is sufficient. This results
// in the AJAX request being completed, but nothing being done to the page.
}
elseif (is_int($page_callback_result)) {
switch ($page_callback_result) {
case MENU_NOT_FOUND:
$commands[] = ajax_command_alert(t('The requested page could not be found.'));
break;
case MENU_ACCESS_DENIED:
$commands[] = ajax_command_alert(t('You are not authorized to access this page.'));
break;
// Return a 'do nothing' command if there was no callback.
ajax_render(array());
case MENU_SITE_OFFLINE:
$commands[] = ajax_command_alert(filter_xss_admin(variable_get('maintenance_mode_message',
t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal'))))));
break;
}
}
elseif (is_array($page_callback_result) && isset($page_callback_result['#type']) && ($page_callback_result['#type'] == 'ajax_commands')) {
// Complex AJAX callbacks can return a result that contains a specific
// set of commands to send to the browser.
if (isset($page_callback_result['#ajax_commands'])) {
$commands = $page_callback_result['#ajax_commands'];
}
}
else {
// Like normal page callbacks, simple AJAX callbacks can return html
// content, as a string or renderable array, to replace what was previously
// there in the wrapper. In this case, in addition to the content, we want
// to add the status messages, but inside the new wrapper, so that they get
// replaced on subsequent AJAX calls for the same wrapper.
$html = is_string($page_callback_result) ? $page_callback_result : drupal_render($page_callback_result);
$commands[] = ajax_command_replace(NULL, $html);
$commands[] = ajax_command_prepend(NULL, theme('status_messages'));
}
ajax_render($commands);
}
/**
......
......@@ -665,75 +665,39 @@ function drupal_goto($path = '', array $query = array(), $fragment = NULL, $http
}
/**
* Generates a site offline message.
* Deliver a "site is under maintenance" message to the browser.
*
* Page callback functions wanting to report a "site offline" message should
* return MENU_SITE_OFFLINE instead of calling drupal_site_offline(). However,
* functions that are invoked in contexts where that return value might not
* bubble up to menu_execute_active_handler() should call drupal_site_offline().
*/
function drupal_site_offline() {
drupal_maintenance_theme();
drupal_add_http_header('503 Service unavailable');
drupal_set_title(t('Site under maintenance'));
print theme('maintenance_page', array('content' => filter_xss_admin(variable_get('maintenance_mode_message',
t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal')))))));
drupal_deliver_page(MENU_SITE_OFFLINE);
}
/**
* Generates a 404 error if the request can not be handled.
* Deliver a "page not found" error to the browser.
*
* Page callback functions wanting to report a "page not found" message should
* return MENU_NOT_FOUND instead of calling drupal_not_found(). However,
* functions that are invoked in contexts where that return value might not
* bubble up to menu_execute_active_handler() should call drupal_not_found().
*/
function drupal_not_found() {
drupal_add_http_header('404 Not Found');
watchdog('page not found', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);
// Keep old path for reference, and to allow forms to redirect to it.
if (!isset($_GET['destination'])) {
$_GET['destination'] = $_GET['q'];
}
$path = drupal_get_normal_path(variable_get('site_404', ''));
if ($path && $path != $_GET['q']) {
// Custom 404 handler. Set the active item in case there are tabs to
// display, or other dependencies on the path.
menu_set_active_item($path);
$return = menu_execute_active_handler($path);
}
if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
// Standard 404 handler.
drupal_set_title(t('Page not found'));
$return = t('The requested page could not be found.');
}
drupal_set_page_content($return);
$page = element_info('page');
print drupal_render_page($page);
drupal_deliver_page(MENU_NOT_FOUND);
}
/**
* Generates a 403 error if the request is not allowed.
* Deliver a "access denied" error to the browser.
*
* Page callback functions wanting to report an "access denied" message should
* return MENU_ACCESS_DENIED instead of calling drupal_access_denied(). However,
* functions that are invoked in contexts where that return value might not
* bubble up to menu_execute_active_handler() should call drupal_access_denied().
*/
function drupal_access_denied() {
drupal_add_http_header('403 Forbidden');
watchdog('access denied', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);
// Keep old path for reference, and to allow forms to redirect to it.
if (!isset($_GET['destination'])) {
$_GET['destination'] = $_GET['q'];
}
$path = drupal_get_normal_path(variable_get('site_403', ''));
if ($path && $path != $_GET['q']) {
// Custom 403 handler. Set the active item in case there are tabs to
// display or other dependencies on the path.
menu_set_active_item($path);
$return = menu_execute_active_handler($path);
}
if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
// Standard 403 handler.
drupal_set_title(t('Access denied'));
$return = t('You are not authorized to access this page.');
}
print drupal_render_page($return);
drupal_deliver_page(MENU_ACCESS_DENIED);
}
/**
......@@ -2591,6 +2555,182 @@ function l($text, $path, array $options = array()) {
return '<a href="' . check_plain(url($path, $options)) . '"' . drupal_attributes($options['attributes']) . '>' . ($options['html'] ? $text : check_plain($text)) . '</a>';
}
/**
* Deliver a page callback result to the browser in the format appropriate.
*
* This function is most commonly called by menu_execute_active_handler(), but
* can also be called by error conditions such as drupal_not_found(),
* drupal_access_denied(), and drupal_site_offline().
*
* When a user requests a page, index.php calls menu_execute_active_handler()
* which calls the 'page callback' function registered in hook_menu(). The page
* callback function can return one of:
* - NULL: to indicate no content
* - an integer menu status constant: to indicate an error condition
* - a string of HTML content
* - a renderable array of content
* Returning a renderable array rather than a string of HTML is preferred,
* because that provides modules with more flexibility in customizing the final
* result.
*
* When the page callback returns its constructed content to
* menu_execute_active_handler(), this functions gets called. The purpose of
* this function is to determine the most appropriate 'delivery callback'
* function to route the content to. The delivery callback function then
* sends the content to the browser in the needed format. The default delivery
* callback is drupal_deliver_html_page() which delivers the content as an HTML
* page, complete with blocks in addition to the content. This default can be
* overridden on a per menu item basis by setting 'delivery callback' in
* hook_menu(), hook_menu_alter(), or hook_menu_active_handler_alter().
* Additionally, modules may use hook_page_delivery_callback_alter() to specify
* a different delivery callback to use for the page request.
*
* For example, the same page callback function can be used for an HTML
* version of the page and an AJAX version of the page. The page callback
* function just needs to decide what content is to be returned and the
* delivery callback function will send it as an HTML page or an AJAX
* response, as appropriate.
*
* In order for page callbacks to be reusable in different delivery formats,
* they should not issue any "print" or "echo" statements, but instead just
* return content.
*
* @param $page_callback_result
* The result of a page callback. Can be one of:
* - NULL: to indicate no content.
* - An integer menu status constant: to indicate an error condition.
* - A string of HTML content.
* - A renderable array of content.
* @param $default_delivery_callback
* (Optional) If given, it is the name of a delivery function most likely
* to be appropriate for the page request as determined by the calling
* function (e.g., menu_execute_active_handler()). If not given, it is
* determined from the menu router information of the current page. In either
* case, modules have a final chance to alter which function is called.
*
* @see menu_execute_active_handler()
* @see hook_menu()
* @see hook_menu_alter()
* @see hook_menu_active_handler_alter()
* @see hook_page_delivery_callback_alter()
*/
function drupal_deliver_page($page_callback_result, $default_delivery_callback = NULL) {
if (!isset($default_delivery_callback) && ($router_item = menu_get_item())) {
drupal_alter('menu_active_handler', $router_item);
$default_delivery_callback = $router_item['delivery_callback'];
}
$delivery_callback = !empty($default_delivery_callback) ? $default_delivery_callback : 'drupal_deliver_html_page';
// Give modules a final chance to alter the delivery callback used. This is
// for modules that need to decide which delivery callback to use based on
// information made available during page callback execution and for pages
// without router items.
drupal_alter('page_delivery_callback', $delivery_callback);
if (function_exists($delivery_callback)) {
$delivery_callback($page_callback_result);
}
else {
// If a delivery callback is specified, but doesn't exist as a function,
// something is wrong, but don't print anything, since it's not known
// what format the response needs to be in.
watchdog('delivery callback not found', check_plain($delivery_callback) . ': ' . check_plain($_GET['q']), NULL, WATCHDOG_ERROR);
}
}
/**
* Package and send the result of a page callback to the browser as a normal
* HTML page.
*
* @param $page_callback_result
* The result of a page callback. Can be one of:
* - NULL: to indicate no content.
* - An integer menu status constant: to indicate an error condition.
* - A string of HTML content.
* - A renderable array of content.
*
* @see drupal_deliver_page
*/
function drupal_deliver_html_page($page_callback_result) {
// Menu status constants are integers; page content is a string or array.
if (is_int($page_callback_result)) {
// @todo: Break these up into separate functions?
switch ($page_callback_result) {
case MENU_NOT_FOUND:
// Print a 404 page.
drupal_add_http_header('404 Not Found');
watchdog('page not found', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);
// Keep old path for reference, and to allow forms to redirect to it.
if (!isset($_GET['destination'])) {
$_GET['destination'] = $_GET['q'];
}
$path = drupal_get_normal_path(variable_get('site_404', ''));
if ($path && $path != $_GET['q']) {
// Custom 404 handler. Set the active item in case there are tabs to
// display, or other dependencies on the path.
menu_set_active_item($path);
$return = menu_execute_active_handler($path, FALSE);
}
if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
// Standard 404 handler.
drupal_set_title(t('Page not found'));
$return = t('The requested page could not be found.');
}
drupal_set_page_content($return);
$page = element_info('page');
print drupal_render_page($page);
break;
case MENU_ACCESS_DENIED:
// Print a 403 page.
drupal_add_http_header('403 Forbidden');
watchdog('access denied', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);
// Keep old path for reference, and to allow forms to redirect to it.
if (!isset($_GET['destination'])) {
$_GET['destination'] = $_GET['q'];
}
$path = drupal_get_normal_path(variable_get('site_403', ''));
if ($path && $path != $_GET['q']) {
// Custom 403 handler. Set the active item in case there are tabs to
// display or other dependencies on the path.
menu_set_active_item($path);
$return = menu_execute_active_handler($path, FALSE);
}
if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
// Standard 403 handler.
drupal_set_title(t('Access denied'));
$return = t('You are not authorized to access this page.');
}
print drupal_render_page($return);
break;
case MENU_SITE_OFFLINE:
// Print a 503 page.
drupal_maintenance_theme();
drupal_add_http_header('503 Service unavailable');
drupal_set_title(t('Site under maintenance'));
print theme('maintenance_page', array('content' => filter_xss_admin(variable_get('maintenance_mode_message',
t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal')))))));
break;
}
}
elseif (isset($page_callback_result)) {
// Print anything besides a menu constant, assuming it's not NULL or
// undefined.
print drupal_render_page($page_callback_result);
}
// Perform end-of-request tasks.
drupal_page_footer();
}
/**
* Perform end-of-request tasks.
*
......
......@@ -401,29 +401,56 @@ function menu_get_item($path = NULL, $router_item = NULL) {
}
/**
* Execute the page callback associated with the current path
*/
function menu_execute_active_handler($path = NULL) {
* Execute the page callback associated with the current path.
*
* @param $path
* The drupal path whose handler is to be be executed. If set to NULL, then
* the current path is used.
* @param $deliver
* (optional) A boolean to indicate whether the content should be sent to the
* browser using the appropriate delivery callback (TRUE) or whether to return
* the result to the caller (FALSE).
*/
function menu_execute_active_handler($path = NULL, $deliver = TRUE) {
if (_menu_site_is_offline()) {
return MENU_SITE_OFFLINE;
$page_callback_result = MENU_SITE_OFFLINE;
}
// Rebuild if we know it's needed, or if the menu masks are missing which
// occurs rarely, likely due to a race condition of multiple rebuilds.
if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) {
menu_rebuild();
}
if ($router_item = menu_get_item($path)) {
if ($router_item['access']) {
if ($router_item['file']) {
require_once DRUPAL_ROOT . '/' . $router_item['file'];
else {
// Rebuild if we know it's needed, or if the menu masks are missing which
// occurs rarely, likely due to a race condition of multiple rebuilds.
if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) {
menu_rebuild();
}
if ($router_item = menu_get_item($path)) {
// hook_menu_alter() lets modules control menu router information that
// doesn't depend on the details of a particular page request.
// Here, we want to give modules a chance to use request-time information
// to make alterations just for this request.
drupal_alter('menu_active_handler', $router_item, $path);
if ($router_item['access']) {
if ($router_item['file']) {
require_once DRUPAL_ROOT . '/' . $router_item['file'];
}
$page_callback_result = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
}
else {
$page_callback_result = MENU_ACCESS_DENIED;
}
return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
}
else {
return MENU_ACCESS_DENIED;
$page_callback_result = MENU_NOT_FOUND;
}
}
return MENU_NOT_FOUND;
// Deliver the result of the page callback to the browser, or if requested,
// return it raw, so calling code can do more processing.
if ($deliver) {
$default_delivery_callback = (isset($router_item) && $router_item) ? $router_item['delivery_callback'] : NULL;
drupal_deliver_page($page_callback_result, $default_delivery_callback);
}
else {
return $page_callback_result;
}
}
/**
......@@ -935,6 +962,7 @@ function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) {
'access_arguments',
'page_callback',
'page_arguments',
'delivery_callback',
'title',
'title_callback',
'title_arguments',
......@@ -1118,6 +1146,7 @@ function menu_tree_page_data($menu_name, $max_depth = NULL) {
'access_arguments',
'page_callback',
'page_arguments',
'delivery_callback',
'title',
'title_callback',
'title_arguments',
......@@ -2833,6 +2862,10 @@ function _menu_router_build($callbacks) {
$item['file path'] = $parent['file path'];
}
}
// Same for delivery callbacks.
if (!isset($item['delivery callback']) && isset($parent['delivery callback'])) {
$item['delivery callback'] = $parent['delivery callback'];
}
// Same for theme callbacks.
if (!isset($item['theme callback']) && isset($parent['theme callback'])) {
$item['theme callback'] = $parent['theme callback'];
......@@ -2858,6 +2891,7 @@ function _menu_router_build($callbacks) {
'access callback' => '',
'page arguments' => array(),
'page callback' => '',
'delivery callback' => '',
'block callback' => '',
'title arguments' => array(),
'title callback' => 't',
......@@ -2905,6 +2939,7 @@ function _menu_router_save($menu, $masks) {
'access_arguments',
'page_callback',
'page_arguments',
'delivery_callback',
'fit',
'number_parts',
'tab_parent',
......@@ -2932,6 +2967,7 @@ function _menu_router_save($menu, $masks) {
'access_arguments' => serialize($item['access arguments']),
'page_callback' => $item['page callback'],
'page_arguments' => serialize($item['page arguments']),
'delivery_callback' => $item['delivery callback'],
'fit' => $item['_fit'],
'number_parts' => $item['_number_parts'],
'tab_parent' => $item['tab_parent'],
......
......@@ -19,25 +19,4 @@
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
$return = menu_execute_active_handler();
// Menu status constants are integers; page content is a string or array.
if (is_int($return)) {
switch ($return) {
case MENU_NOT_FOUND:
drupal_not_found();
break;
case MENU_ACCESS_DENIED:
drupal_access_denied();
break;
case MENU_SITE_OFFLINE:
drupal_site_offline();
break;
}
}
elseif (isset($return)) {
// Print anything besides a menu constant, assuming it's not NULL or undefined.
print drupal_render_page($return);
}
drupal_page_footer();
menu_execute_active_handler();
......@@ -165,6 +165,7 @@ function book_menu() {
);
$items['book/js/form'] = array(
'page callback' => 'book_form_update',
'delivery callback' => 'ajax_deliver',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
'file' => 'book.pages.inc',
......@@ -1214,7 +1215,7 @@ function book_menu_subtree_data($link) {
$menu_router_alias = $query->join('menu_router', 'm', 'm.path = ml.router_path');
$book_alias = $query->join('book', 'b', 'ml.mlid = b.mlid');
$query->fields($book_alias);
$query->fields($menu_router_alias, array('load_functions', 'to_arg_functions', 'access_callback', 'access_arguments', 'page_callback', 'page_arguments', 'title', 'title_callback', 'title_arguments', 'type'));
$query->fields($menu_router_alias, array('load_functions', 'to_arg_functions', 'access_callback', 'access_arguments', 'page_callback', 'page_arguments', 'delivery_callback', 'title', 'title_callback', 'title_arguments', 'type'));
$query->fields('ml');
$query->condition('menu_name', $link['menu_name']);
for ($i = 1; $i <= MENU_MAX_DEPTH && $link["p$i"]; ++$i) {
......
......@@ -255,5 +255,8 @@ function book_form_update() {
$commands[] = ajax_command_replace(NULL, drupal_render($form['book']['plid']));
}
ajax_render($commands);
// @todo: We could and should just return $form['book']['plid'] and skip the
// ajax_command_replace() above. But for now, this provides a test case of
// returning an AJAX commands array.
return array('#type' => 'ajax_commands', '#ajax_commands' => $commands);
}
......@@ -364,7 +364,7 @@ function comment_permalink($comment) {
drupal_add_link(array('rel' => 'canonical', 'href' => url('node/' . $node->nid)));
// Return the node view, this will show the correct comment in context.
return menu_execute_active_handler('node/' . $node->nid);
return menu_execute_active_handler('node/' . $node->nid, FALSE);
}
drupal_not_found();
}
......
......@@ -17,11 +17,13 @@ function file_menu() {
$items['file/ajax'] = array(
'page callback' => 'file_ajax_upload',
'delivery callback' => 'ajax_deliver',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
$items['file/progress'] = array(
'page callback' => 'file_ajax_progress',
'delivery callback' => 'ajax_deliver',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
......
......@@ -44,7 +44,7 @@ function menu_overview_form($form, &$form_state, $menu) {
global $menu_admin;
$form['#attached']['css'] = array(drupal_get_path('module', 'menu') . '/menu.css');
$sql = "
SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, m.description, ml.*
SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.delivery_callback, m.title, m.title_callback, m.title_arguments, m.type, m.description, ml.*
FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
WHERE ml.menu_name = :menu
ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC";
......
......@@ -163,6 +163,10 @@
* instead.
* - "page arguments": An array of arguments to pass to the page callback
* function, with path component substitution as described above.
* - "delivery callback": The function to call to package the result of the
* page callback function and send it to the browser. Defaults to
* drupal_deliver_html_page() unless a value is inherited from a parent menu
* item.
* - "access callback": A function returning a boolean value that determines
* whether the user has access rights to this menu item. Defaults to
* user_access() unless a value is inherited from a parent menu item.
......
......@@ -382,9 +382,7 @@ function _poll_choice_form($key, $chid = NULL, $value = '', $votes = 0, $weight
* Menu callback for AHAH additions. Render the new poll choices.
*/
function poll_choice_js($form, $form_state) {
$choice_form = $form['choice_wrapper']['choice'];
return drupal_render($choice_form);
return $form['choice_wrapper']['choice'];
}
/**
......
......@@ -2506,6 +2506,80 @@ function hook_date_formats_alter(&$formats) {
}
}
/**
* Alters the router item for the active menu handler.
*
* Called by menu_execute_active_handler() to allow modules to alter the
* information that will be used to handle the page request. Only use this
* hook if an alteration specific to the page request is needed. Otherwise
* use hook_menu_alter().
*
* @param $router_item
* An array with the following keys:
* - access: Boolean. Whether the user is allowed to see this page.
* - file: A path to a file to include prior to invoking the page callback.
* - page_callback: The function to call to build the page content.
* - page_arguments: Arguments to pass to the page callback.
* - delivery_callback: The function to call to deliver the result of the
* page callback to the browser.
* @param $path
* The drupal path that was used for retrieving the router item.
*
* @see menu_execute_active_handler()
* @see hook_menu()