Mean in green
I'm Kevin. I live in Salem, Mass with my wife and little boy and I build software.

Drupal, jQuery and $.ajax() easyness.

Tuesday Jan 05, 2010

So, I've been remiss in my blogging as of late. It's not for a lack of ideas though, so I'm going to jump right back in with something cool that I've been using a lot lately. Once again, I'm just astounded at how insanely easy jQuery makes my life. I've been reading Resig's book on the train this week and the main thing I'm learning is just how important it is to have Javascript frameworks like jQuery. There are so many nuances between major browsers - even the "modern" ones - that it would be ridiculous to try and account for them yourself when these frameworks already do such a great job.

I've written before about doing modals using the popups api and it sounds like there is some goodies in ctools. Though, sometimes you just need laser precision to accomplish one specific task to meet a clients requirement. In these cases, I'm not at all opposed to writing a quick module.

Let's say you need a really simple module that provides a link to trigger something on the back end of your site and returns a response without a page refresh. This type of thing is super common these days and jQuery makes it oh-so-simple to implement.

Read on for the code...

This module is obviously simple, but hopefully it will demonstrate some very powerful concepts. It is worth noting that the security measures here will break the functionality for anonymous users. Anonymous Drupal users do not have a constant token provided from drupal_get_token() [See: Drupal.org], so you would need an alternative security measure for this.

Here's a working example that ignores the token - so that you can at least get a glimpse of it in action without being logged in.

Your example.info file

name = Example description = An AJAX callback module core = 6.x

Your example.module file

<?php

/* * Implementation of hook_init(). / function example_init() { // Create a token that will be available to our Javascript // as Drupal.settings.exampleToken which we will then // pass to our AJAX URL for authentication. You might // want to create something a little more obscure by using // a string argument with drupal_get_token(). drupal_add_js(array('exampleToken' => drupal_get_token()), 'setting'); drupal_add_js(drupal_get_path('module', 'example') . '/example.js'); }

/* * Implementation of hook_menu(). / function example_menu() { $items = array();

// Create a Drupal page to display our AJAX link. $items['example/page'] = array( 'title' => 'My AJAX Test', 'page callback' => 'example_page_callback', 'access arguments' => array('access example page'), 'type' => MENU_NORMAL_ITEM, );

// Create a path to send our AJAX request to. $items['example/ajax'] = array( 'title' => 'My callback', 'page callback' => 'example_ajax_callback', 'access arguments' => array('access example ajax'), 'type' => MENU_CALLBACK, );

return $items; }

/* * Implementation of hook_perm(). / function example_perm() { // Restrict access to either of your new URLs. return array( 'access example page', 'access example ajax', ); }

/* * Callback function for /example/page / function example_page_callback() { return theme('example_link'); }

/* * Implementation of hook_theme(). / function example_theme() { return array( 'example_link' => array(), ); }

/* * Default implementation of theme_example_link(). / function theme_example_link() { // Simply create a link with an ID that Javascript can // find with our token available as a GET request variable. // We will use GET vs. POST request variables later on to // help distinguish the authenticity of the request. return l(t('What is the date and time?'), 'example/ajax', array( 'query' => array( 'example_token' => drupal_get_token() ), 'attributes' => array( 'id' => 'example-ajax') ) ); }

/* * Callback function for /example/ajax(). / function example_ajax_callback() { // You might do a database lookup here or perform // some other action - just make sure to mind your // security P's and Q's!

if (!$POST['from_js'] && $GET['example_token'] == drupal_get_token()) {

// If the request didn't come from Javascript,
// then you might want to provide an alternate
// script here. This can either be for security 
// reasons or so that the Javascript can degrade 
// gracefully and the link still works.
return t('It is approximately !date', array('!date' => date('M j Y h:i:s')));

} elseif ($POST['from_js'] && $POST['example_token'] == drupal_get_token()) {

// If the request did come from Javascript and 
// the POST token matches, then it is fairly safe
// to assume that it was a deliberate AJAX request.
$ret = array(
  'message' => t('It is approximately !date', array('!date' => date('M j Y h:i:s'))),
);

// Set the text/javascript headers and
// return the response in JSON format.
drupal_json($ret);
exit;

} else { // If it wasn't a legitamate request, we return // and empty value. drupal_json(array()); exit;

} } ?>

Your example.js file

// Drupal.behavors ensures that our new binding can be // applied to any new occurances of #example-ajax // created on the fly by other scripts. Drupal.behaviors.exampleAjax = function(context) {

// Bind an AJAX callback to our link var exampleAjaxLink = $('#example-ajax');

exampleAjaxLink.click(function(event) { // Get the URL without the query string - this is // so that we can distinguish between GET and POST // requests. var exampleUrl = exampleAjaxLink.attr('href').split('?');

// Prevent the default link action - we don't
// want to trigger a synchronous response.
event.preventDefault();

// Perform the ajax request - the configurations
// below can be modified to suit your needs:
// http://docs.jquery.com/Ajax/jQuery.ajax#options
$.ajax({
  type: "POST",
  url: exampleUrl[0],
  data: {
    'from_js' : true,
    'example_token' : Drupal.settings.exampleToken
  },
  dataType: "json",
  success: function (data) {
    if (data.message) {
      // Remove any messages that are already there. It might
      // be good to abstract this a bit and make a function that
      // can remove the message div. You could then also bind a
      // a link titled "Close" to that function so that the user
      // can acknowledge the message and remove it.
      $(".messages").remove();

      // The div where your messages div normally shows. This 
      // example is from this particular custom theme - change
      // #content-column to the parent selector where your
      // .messages div displays in your theme. If you use the same
      // classes as Drupal, you end up with a nicely styled response
      // right where the user is accustomed to seeing messages.
      $("#content-column").prepend('<div class="messages status">' + data.message + '</div>');
    }
  },
  error: function (xmlhttp) {
    alert('An error occured: ' + xmlhttp.status);
  }
});

}); }

Some important security notes:

  • This module is relatively benign, but you could easily use this skeleton to get yourself into trouble. Please be aware that there are major implications to using this concept.
  • You should never perform system altering updates using a AJAX requests without forcing the user to confirm the request on a subsequent page request. A CSRF attack could submit this request on your behalf without you ever being the wiser.
  • This sample module provides a few measures of protection - hook_perm() allows you to specify who can submit the request; drupal_get_token() helps to ensure that the request was submitted by a site user; using POST data helps to ensure that phony GET requests will fail. However these are not enough to thwart an experienced attacker. If you absolutely need to use AJAX to handle system updating tasks, take as many steps as possible to ensure that the request was deliberately initiated by the user.