Commit 2c444d41 authored by japerry's avatar japerry

ctools 1.14

parent 0e836c69
......@@ -445,6 +445,26 @@ function ctools_forms() {
return $forms;
}
/**
* Implements hook_form_alter().
*/
function ctools_form_alter(&$form, $form_state, $form_id) {
$form['#after_build'][] = 'ctools_ajax_form_after_build';
}
/**
* #after_build callback: Mark the $form['#action'] as a trusted URL for Ajax.
*/
function ctools_ajax_form_after_build($form, $form_state) {
$settings = array(
'CToolsUrlIsAjaxTrusted' => array(
$form['#action'] => TRUE,
),
);
drupal_add_js($settings, 'setting');
return $form;
}
/**
* Implementation of hook_file_download()
*
......
......@@ -457,6 +457,10 @@ function ctools_ajax_command_submit($selector) {
* to the AJAX requester.
*/
function ctools_ajax_render($commands = array()) {
// Although ajax_deliver() does this, some contributed and custom modules
// render AJAX responses without using that delivery callback.
ctools_ajax_set_verification_header();
$js_files = array();
$settings = ctools_process_js_files($js_files, 'header');
$settings += ctools_process_js_files($js_files, 'footer');
......@@ -521,6 +525,25 @@ function ctools_ajax_render($commands = array()) {
exit;
}
/**
* Sets a response header for ajax.js to trust the response body.
*
* It is not safe to invoke JS commands within user-uploaded files, so this
* header protects against those being invoked.
*
* @see Drupal.ajax.ajax.options.success()
*/
function ctools_ajax_set_verification_header() {
$set = &ctools_static(__FUNCTION__);
if (!isset($set)) {
// User-uploaded files cannot set any response headers, so the token value
// does not need to be hard to guess.
drupal_set_header('X-Drupal-Ajax-Token: 1');
$set = TRUE;
}
}
/**
* Send an error response back via AJAX and immediately exit.
*/
......
......@@ -348,7 +348,15 @@ function ctools_content_editable($type, $subtype, $conf) {
return FALSE;
}
if ($function = ctools_plugin_get_function($subtype, 'check editable')) {
$function = FALSE;
if (!empty($subtype['check editable'])) {
$function = ctools_plugin_get_function($subtype, 'check editable');
}
elseif (!empty($type['check editable'])) {
$function = ctools_plugin_get_function($type, 'check editable');
}
if ($function) {
return $function($type, $subtype, $conf);
}
......
......@@ -11,6 +11,7 @@
Drupal.CTools.AJAX.commandCache = Drupal.CTools.AJAX.commandCache || {};
Drupal.CTools.AJAX.scripts = {};
Drupal.CTools.AJAX.css = {};
Drupal.settings.CToolsUrlIsAjaxTrusted = Drupal.settings.CToolsUrlIsAjaxTrusted || {};
/**
* Success callback for an ajax request.
......@@ -39,7 +40,7 @@
// If we are currently fetching, or if we have fetched this already which is
// ideal for things like pagers, where the previous page might already have
// been seen in the cache.
if ($this.hasClass('ctools-fetching') || Drupal.CTools.AJAX.commandCache[old_url]) {
if ($this.hasClass('ctools-fetching') || !Drupal.CTools.AJAX.urlIsLocal(old_url) || Drupal.CTools.AJAX.commandCache[old_url]) {
return false;
}
......@@ -50,20 +51,26 @@
$objects.addClass('ctools-fetching');
try {
var url = Drupal.CTools.AJAX.urlReplaceNojs(url);
$.ajax({
var ajaxOptions = {
type: "POST",
url: url,
data: { 'js': 1, 'ctools_ajax': 1},
global: true,
success: function (data) {
if (!Drupal.CTools.AJAX.isAjaxResponseTrusted(ajaxOptions.cXhr, url)) {
return this.error(ajaxOptions.cXhr);
}
Drupal.CTools.AJAX.commandCache[old_url] = data;
$objects.addClass('ctools-cache-warmed').trigger('ctools-cache-warm', [data]);
},
beforeSend: Drupal.CTools.AJAX.beforeSend,
complete: function() {
$objects.removeClass('ctools-fetching');
},
dataType: 'json'
});
};
$.ajax(ajaxOptions);
}
catch (err) {
$objects.removeClass('ctools-fetching');
......@@ -105,6 +112,9 @@
}
var url = $(this).attr('href');
if (!Drupal.CTools.AJAX.urlIsLocal(url)) {
return false;
}
$(this).addClass('ctools-ajaxing');
try {
url = Drupal.CTools.AJAX.urlReplaceNojs(url);
......@@ -113,7 +123,8 @@
url: url,
data: { 'js': 1, 'ctools_ajax': 1},
global: true,
success: Drupal.CTools.AJAX.respond,
success: Drupal.CTools.AJAX.success,
beforeSend: Drupal.CTools.AJAX.beforeSend,
error: function(xhr) {
Drupal.CTools.AJAX.handleErrors(xhr, url);
},
......@@ -154,7 +165,8 @@
url: url,
data: { 'js': 1, 'ctools_ajax': 1},
global: true,
success: Drupal.CTools.AJAX.respond,
success: Drupal.CTools.AJAX.success,
beforeSend: Drupal.CTools.AJAX.beforeSend,
error: function(xhr) {
Drupal.CTools.AJAX.handleErrors(xhr, url);
},
......@@ -178,6 +190,27 @@
return false;
};
/**
* Helper method to stash the xhr object so it is available in the success callback.
*/
Drupal.CTools.AJAX.beforeSend = function(xhr) {
this.cXhr = xhr;
};
/**
* Respond wrapper that checks security of the request.
*/
Drupal.CTools.AJAX.success = function(data) {
if (data !== null && !Drupal.CTools.AJAX.isAjaxResponseTrusted(this.cXhr, this.url)) {
return this.error(this.cXhr);
}
Drupal.CTools.AJAX.respond(data);
};
Drupal.CTools.AJAX.isAjaxResponseTrusted = function(xhr, url) {
return Drupal.settings.CToolsUrlIsAjaxTrusted[url] || (Drupal.CTools.AJAX.urlIsLocal(url) && xhr.getResponseHeader('X-Drupal-Ajax-Token') === '1');
};
/**
* Event handler to submit an AJAX form.
*
......@@ -201,7 +234,12 @@
url: url,
data: { 'js': 1, 'ctools_ajax': 1},
global: true,
success: Drupal.CTools.AJAX.respond,
success: function(data) {
Drupal.CTools.AJAX.success.apply(ajaxOptions, [data]);
},
beforeSend: function (xhr) {
Drupal.CTools.AJAX.beforeSend.apply(ajaxOptions, [xhr]);
},
error: function(xhr) {
Drupal.CTools.AJAX.handleErrors(xhr, url);
},
......@@ -216,8 +254,14 @@
// the submit to support this and use the proper response.
if ($form.attr('enctype') == 'multipart/form-data') {
$form.append('<input type="hidden" name="ctools_multipart" value="1">');
ajaxIframeOptions = {
success: Drupal.CTools.AJAX.iFrameJsonRespond,
var ajaxIframeOptions = {
success: function(data) {
if (!Drupal.CTools.AJAX.isAjaxResponseTrusted(ajaxOptions.cXhr, url)) {
return this.error(ajaxOptions.cXhr);
}
Drupal.CTools.AJAX.iFrameJsonRespond(data);
},
iframe: true
};
ajaxOptions = $.extend(ajaxOptions, ajaxIframeOptions);
......@@ -281,8 +325,8 @@
var url = Drupal.CTools.AJAX.findURL(this);
$(this).addClass('ctools-ajaxing');
var object = $(this);
var form_id = $(object).parents('form').get(0).id;
var $object = $(this);
var form_id = $object.parents('form').get(0).id;
try {
if (url) {
url = Drupal.CTools.AJAX.urlReplaceNojs(url);
......@@ -291,7 +335,8 @@
url: url,
data: {'ctools_changed': $(this).val(), 'js': 1, 'ctools_ajax': 1 },
global: true,
success: Drupal.CTools.AJAX.respond,
success: Drupal.CTools.AJAX.success,
beforeSend: Drupal.CTools.AJAX.beforeSend,
error: function(xhr) {
Drupal.CTools.AJAX.handleErrors(xhr, url);
},
......@@ -305,7 +350,7 @@
});
}
else {
if ($(object).hasClass('ctools-ajax-submit-onchange')) {
if ($object.hasClass('ctools-ajax-submit-onchange')) {
$('form#' + form_id).submit();
}
return false;
......@@ -337,7 +382,7 @@
}
url += $(this).val();
});
return url;
return Drupal.CTools.AJAX.urlIsLocal(url) ? url : '/';
};
Drupal.CTools.AJAX.getPath = function (link) {
......@@ -419,9 +464,10 @@
// Build a list of css files already loaded:
$('link:not(.ctools-temporary-css)').each(function () {
if ($(this).attr('type') == 'text/css') {
var link = Drupal.CTools.AJAX.getPath($(this).attr('href'));
if (link) {
Drupal.CTools.AJAX.css[link] = $(this).attr('href');
var href = $(this).attr('href');
var link = Drupal.CTools.AJAX.getPath(href);
if (link && Drupal.CTools.AJAX.urlIsLocal(href)) {
Drupal.CTools.AJAX.css[link] = href;
}
}
});
......@@ -525,7 +571,7 @@
/**
* Replacing 'nojs' with 'ajax' in the URL allows for an easy method to let
* the server detect when it needs to degrade gracefully.
* the server detect when it needs to degrade gracefully.
* There are five scenarios to check for:
* 1. /nojs/
* 2. /nojs$ - The end of a URL string.
......@@ -537,7 +583,81 @@
* E.g.: path/nojs#myfragment
*/
Drupal.CTools.AJAX.urlReplaceNojs = function(url) {
return url.replace(/\/nojs(\/|$|\?|&|#)/g, '/ajax$1');
var new_url = url.replace(/\/nojs(\/|$|\?|&|#)/g, '/ajax$1');
// If the 'nojs' version of the URL is trusted, also trust the 'ajax'
// version.
if (Drupal.settings.CToolsUrlIsAjaxTrusted[url]) {
Drupal.settings.CToolsUrlIsAjaxTrusted[new_url] = true;
}
return new_url;
};
/**
* Returns the passed in URL as an absolute URL.
*
* @param url
* The URL string to be normalized to an absolute URL.
*
* @return
* The normalized, absolute URL.
*
* @see https://github.com/angular/angular.js/blob/v1.4.4/src/ng/urlUtils.js
* @see https://grack.com/blog/2009/11/17/absolutizing-url-in-javascript
* @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L53
*/
Drupal.CTools.AJAX.absoluteUrl = function (url) {
var urlParsingNode = document.createElement('a');
// Decode the URL first; this is required by IE <= 6. Decoding non-UTF-8
// strings may throw an exception.
try {
url = decodeURIComponent(url);
} catch (e) {}
urlParsingNode.setAttribute('href', url);
// IE <= 7 normalizes the URL when assigned to the anchor node similar to
// the other browsers.
return urlParsingNode.cloneNode(false).href;
};
/**
* Returns true if the URL is within Drupal's base path.
*
* @param url
* The URL string to be tested.
*
* @return
* Boolean true if local, or false if the url may be external or have a scheme.
*
* @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L58
*/
Drupal.CTools.AJAX.urlIsLocal = function (url) {
// Always use browser-derived absolute URLs in the comparison, to avoid
// attempts to break out of the base path using directory traversal.
var absoluteUrl = Drupal.CTools.AJAX.absoluteUrl(url);
var protocol = location.protocol;
// Consider URLs that match this site's base URL but use HTTPS instead of HTTP
// as local as well.
if (protocol === 'http:' && absoluteUrl.indexOf('https:') === 0) {
protocol = 'https:';
}
var baseUrl = protocol + '//' + location.host + Drupal.settings.basePath.slice(0, -1);
// Decoding non-UTF-8 strings may throw an exception.
try {
absoluteUrl = decodeURIComponent(absoluteUrl);
} catch (e) {}
try {
baseUrl = decodeURIComponent(baseUrl);
} catch (e) {}
// The given URL matches the site's base URL, or has a path under the site's
// base URL.
return absoluteUrl === baseUrl || absoluteUrl.indexOf(baseUrl + '/') === 0;
};
/**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment