Drupal for Privacy Buffs: Configuring Your Drupal 6 Site to Work Without Cookies for Anonymous Users

A Drupal 6 site we are currently building has a requirement to not set cookies for anonymous users. In this blog post, I describe the hack-free solution we implemented for them that prevents Drupal from setting cookies.

There are three steps we took to put this solution into place:

  1. Override Drupal's session.inc file to disable cookies and prevent writing to the sessions table.
  2. Implement a hook_form_alter to re-enable cookies when a user is trying to log in.
  3. Add a bit of javascript to remove the has_js cookie.

Step 1

Drupal makes it really easy to override its PHP session handling functions. First, copy the default include file from includes/session.inc to an accessible location of your choice -- we used sites/all/includes/mysite_session.inc. Then edit that file and make the following changes:

mysite_session.inc

@@ -24,6 +23,8 @@ function sess_read($key) {
   // Handle the case of first time visitors and clients that don't store cookies (eg. web crawlers).
   if (!isset($_COOKIE[session_name()])) {
+   // Custom modification: turn off session cookies
+   if (substr($_GET['q'], 0, 11) != 'user/reset/') {
+     ini_set('session.use_cookies', 0);
+   }
     $user = drupal_anonymous_user();
     return '';
   }
@@ -61,7 +62,12 @@ function sess_write($key, $value) {
   // the session table. This reduces memory and server load, and gives more useful
   // statistics. We can't eliminate anonymous session table rows without breaking
   // the throttle module and the "Who's Online" block.
-  if (!session_save_session() || ($user->uid == 0 && empty($_COOKIE[session_name()]) && empty($value))) {
+  // Custom modification: We won't use throttle nor "Who's Online", so we are
+  // eliminating anonymous sessions.
+  if (!session_save_session() || $user->uid == 0) {
     return TRUE;
   }

EDIT: The code above has been changed since this blog post was originally posted. Namely, we now don't turn off cookies on the user/reset path, which allows one-time logins to work correctly (and anyone visiting that page has the intention to log in anyway).

The first change we made was to the sess_read function, which by default checks to see if there's already a cookie set, and if not, considers the user anonymous. Since the user is anonymous, we can turn off all future cookie setting here by setting the session.use_cookies PHP variable to 0. The second change is to sess_write, which writes to the sessions database table. Usually, Drupal will write a new session row for a first-time anonymous user, but we modify it here to never write to the table if the user is anonymous.

Now we need to tell Drupal to use our custom session functions instead of the default ones. To do that, we just add a variable override to our settings.php file, which we can do in the section titled "Variable overrides." Simply add the following line in that section, or add it to any already existing overrides you may have:

settings.php

<?php
$conf
= array(
 
'session_inc' => './sites/all/includes/mysite_session.inc',
);
?>

And that's all we need to do to prevent session cookies from being written! Of course, the problem here is that when a user tries to log in, Drupal won't be able to start a session. So we have to let Drupal know when it should allow cookies again.

Step 2

To do this, we only need to put a simple hook_form_alter function in a custom utility module:

custommodule.module

<?php
/*
*  Implementation of hook_form_alter().
*/
function custommodule_form_alter(&$form, $form_state, $form_id) {
  if (
$form_id == 'user_login_block' || $form_id == 'user_login') {
    if (
$form_state['post']) {
     
// Someone's attempting to log in.  Let's allow session cookies.
     
ini_set('session.use_cookies', 1);
    }
  }
}
?>

EDIT: The code above has been tweaked since the blog was originally posted. We now handle one-time login above in the session file, since a form_alter on 'user_pass_reset' doesn't work reliably, due to how the one-time login form is implemented.

All we are doing here is re-enabling the PHP session.use_cookies variable when a user is submitting a user login form. You may have to add more form_id's here if you have other ways a user can log in. You could also use this technique to let a user enable cookies at their choosing when taking a particular action.

With this in place, a user will now get a cookie only when logging in (and as such we can warn them in advance). To minimize the intrusion, we could set the cookie_lifetime variable to 0 in settings.php so that cookie expires at the end of the session, but since only staff members can log in on our client's site, we did not need to do that.

Step 3

Session cookies have been taken care of, so we're all done, right? Not exactly. After implementing the above and doing a test drive, I noticed a pesky "has_js" cookie was still being set, and the guilty party was the default drupal.js file. This cookie is added by javascript as a way to let code know that the user has javascript enabled -- in core, it's used only by the batch processing API so that it can know when to show a fancy progress bar while processing batch jobs.

Unfortunately, we can't circumvent the setting of this cookie without hacking drupal.js, so our solution here was to immediately make the cookie expire using our own javascript. The first step is to add our custom remove_has_js.js function only when drupal.js is included on a page requested by an anonymous user:

template.php

<?php
function customtheme_preprocess_page(&$vars, $hook) {
  global
$user;
  if (
strpos($vars['scripts'], 'drupal.js') !== FALSE && !$user->uid) {
   
drupal_add_js(drupal_get_path('theme', 'customtheme') . '/js/remove_has_js.js');
   
$vars['scripts'] = drupal_get_js();
  }
}
?>

The remove_has_js.js file itself is fairly simple, setting the expiration date of the has_js cookie in the past:

remove_has_js.js

if (Drupal.jsEnabled) {
  // remove 'js enabled' cookie
  document.cookie = 'has_js=1; expires=Fri, 19 Nov 1978 05:00:00 GMT; path=/';
}

And that's it! Now when we browse around the site as an anonymous user, we are consistently cookie-free.

Qualifications

I should note that a consequence of this solution is that anonymous users will not have a Drupal session. There are solutions out there that replace cookies with URL sessions, but we did not want to burden our URL's with long strings and, since the site is mostly informational with user interaction handled externally, we had no great need to maintain anonymous sessions. Nevertheless, if you implement this, be sure to test all anonymous functionality on your site. This solution isn't compatible with the throttle module nor with the "Who's Online" block that comes with core, and could potentially conflict with contributed module functionality. Also, this solution doesn't prevent contributed modules from setting cookies, so be aware of that possibility as well.

On the other hand, not having to write to the sessions table could be a big plus for site performance, especially if your site gets heavy anonymous traffic.

External Cookies

Even with this solution in place, we still need to be careful of external sites setting cookies via Drupal. For example, 3rd party video providers often set a cookie even if the user does not choose to play a video. Fortunately, Advomatic's Aaron Winborn modified the Embedded Media Field module to allow the use of a thumbnail display that doesn't add the embedded code until the thumbnail is clicked, before which the user can be properly warned.

You may have to add similar functionality to other modules you are using that depend on 3rd party functionality that set cookies, or if there's a contributed module that you are relying on that doesn't have this feature, submit an issue asking for the maintainer to add it in.

zzolo wrote 5 years 9 weeks ago

Great article. Just implemented it and seems to be working well. The one suggestion I have is that if you are already creating a custom module for the form_alter hook, you might as well put the rest of the code in the module, and not the theme.

Marco Carbone wrote 5 years 9 weeks ago

Thanks, I'm glad it works for you. As for moving all the template.php stuff to the module, you could do that, but then you'd be including that script on every page, including those that don't have drupal.js, right?

zzolo wrote 5 years 9 weeks ago

Well, technically your method will get parsed on any operation that involves the theme. It will also get executed on any page and include the JS file. But the exceptions outside of those instances are few and far between. I suppose if you are only using this particular theme for the front end, it makes a little more sense.

I think it could be a little more robust and check to see if a user is logged in and if the Drupal object exists, and then remove the jsEnabled cookie. But like you said, the jsEnabled check is not used very frequently.

Most importantly, to me, is that if it is in a module, anonymous cookie usage will not be tied to a theme.

Marco Carbone wrote 5 years 9 weeks ago

Gotcha. It's such a minimal js file anyway so I think it's fine to put in the module.

David Eads wrote 5 years 9 weeks ago

Much thanks! I just implemented this (I'm working on a project where reducing the paper trail associated with anonymous commenters is a top priority) and put added the javascript in the module. With javascript aggregation turned on, the JS is truly insignificant in terms of overhead.

Khalid wrote 5 years 8 weeks ago

Marco

I used your article to create a module to do what you want, but for a totally different reason: performance.

Here is the article on reducing server load by eliminating anonymous sessions in Drupal 6.x.

John H wrote 5 years 6 weeks ago

Thanks very much for sharing this solution. I have a client who deems anonymous cookie storage a breach in one's privacy so this solution allowed them to uphold their policy.

The performance increase is also a nice residual benefit as per the previous poster.

Marco Carbone wrote 5 years 5 weeks ago

FYI, for those who want to use an image captcha with this solution, you'll have to use the 6.x-2.0 branch (which is currently in Beta5) of the captcha module.

Contact Us

About Marco Carbone

Marco Carbone is technical lead at Advomatic, and is one of the many awesome contributors to Drupal core. He lives in Brooklyn, NY.

AdvoTwitter

  • RT @MRCampaigns: Google now flags when sites aren't mobile friendly -- considered yourself warned! #Optimization #FTW! http://t.co/1qwbcoNG…
    July 22, 2014 - 10:29am
  • Link going around the office today: @amyschumer on teaching mom to use the computer. http://t.co/NRauPPppVs
    July 21, 2014 - 3:49pm
  • It's the last day to vote for #NTC15 sessions! If you're a consultant or nonprofit interested in #agile, vote here: http://t.co/EhecZ26fCk
    July 18, 2014 - 2:15pm
  • Love Super Mario Bros? How about cuteness? Advo-front-end developer Jack made probably the awesomest kid's room ever: http://t.co/WIobr5YEDQ
    July 18, 2014 - 11:15am
  • From our staff meeting today: "I saw the Rifftrax for... what's the people?...'THERE CAN ONLY BE ONE!'?" http://t.co/HwLjtcaP8A
    July 17, 2014 - 3:15pm