Ruby-on-Rails: Cookie-Detection

Ich schreibe hier viel über WordPress, verbringe meine Arbeitstage im Moment aber zu einem Großteil mit Ruby-on-Rails. Damit das hier endlich auch mal vorkommt, habe ich mir mal ein interessantes Problem herausgegriffen, das kürzlich zu lösen war: erkennen ob der Besucher Cookies aktiviert hat oder nicht.

In dem betroffenen Projekt basiert die Session auf Cookies. Ohne Cookies keine Session, und ohne Session kein Login und keine Registrierung. Von Haus aus ging das leider still und leise schief. Man logt sich ein, das System setzt den Cookie und schickt einen zur internen Startseite. Dort fehlt aber der Cookie, also kommt man zurück zur Loginseite. Kein Hinweis, wieso der Login nicht geklappt hat. Hier sollte dem Nutzer ein verständlicher Hinweis angezeigt werden.

Eine kurze Google-Suche ergab in erster Linie diesen vergleichsweise alten Blog-Beitrag sowie diese Anleitung von 2009. Letzteres sah sehr gut aus, war aber noch nicht ganz, was wir brauchten. Insbesondere wird hier bei abgeschalteten Cookies die Warnung auf einer extra Seite angezeigt (/cookie-test). Schöner ist es dagegen, wenn das auf der jeweils betroffenen Seite als Flash-Message passiert.

Ich habe das also wie folgt ein wenig umgebaut. Die Hauptarbeit erledigt lib/cookie_detection.rb:

# -*- coding: utf-8 -*-
# Detects if cookies are present (only GET requests, not for bots).
# If cookies are disabled, shows a flash message.
# Usage:
# * save this file at a location in your rails app where it gets required
#   (e.g. RAILS_ROOT/lib)
# * application_controller:
#   include CookieDetection
# * relevant controllers:
#   before_filter :cookies_required
#
# Adapted from here: http://clearcove.ca/2009/09/rails-cookie-detection/
module CookieDetection
  # Checks if cookies are turned on in the user's browser.
  def cookies_required
    # check if anything needs to be done at all
    if !cookies.entries.empty? ||                       # skip if a cookie is set
       (request && request.request_method != 'GET') ||  # skip non-GET requests
       is_megatron?(request.env["HTTP_USER_AGENT"])     # skip requests from bots
      return true
    end

    # set a flash message on the second call
    if params[:cookie_test].present?
      logger.warn("=== cookies are disabled")
      flash[:alert] = 'Sie benötigen Cookies um sich einzuloggen oder zu registrieren. Bitte stellen Sie sicher, dass Ihr Browser Cookies akzeptiert.'
      return true
    end

    # otherwise set a cookie and redirect to the current URL + parameter
    cookies["cookie_test"] = Time.now
    url = request.url + (request.url.index('?').present? ? '&' : '?') + 'cookie_test=' + Time.now.to_i.to_s
    redirect_to(url)
  end

  # from: http://gurge.com/blog/2007/01/08/turn-off-rails-sessions-for-robots/
  # added bingbot + a generic bot for new bots
  def is_megatron?(user_agent)
    user_agent =~ /\b(Baidu|Gigabot|Googlebot|libwww-perl|lwp-trivial|msnbot|bingbot|SiteUptime|Slurp|WordPress|ZIBB|ZyBorg|bot)\b/i
  end
end

Getestet mit: Ruby 1.9.3 / Rails 3.2

Das Ganze muss nun noch in die gewünschten Controller eingebaut werden. Man kann das Module einfach in den ApplicationsController einbinden und dann den Filter nur in den gewünschten Controllern aufrufen. In meinem Fall waren aber beide Controller Devise-Controller, die also nicht vom ApplicationController erben. Deswegen direkt z.B. im SessionsController:

class SessionsController < ::Devise::SessionsController
  include CookieDetection
  before_filter :cookies_required

  # ...
end

Der Filter funktioniert so: Beim Aufruf einer vom SessionsController generierten Seite (hier die Loginseite) wird die cookies_required-Methode aufgerufen. Diese prüft ob der Request einen Cookie enthält (dann sind wir fertig), kein GET-Request ist (der Cookie-Test basiert auf einem Redirect, Post-Daten gehen dabei verloren, deswegen macht das dafür nicht viel Sinn) oder wahrscheinlich von einem Bot kommt (die haben in der Regel keine Cookies aktiviert, sollen aber nicht die Warnung indexieren). Den Bot-Detection-Code habe ich samt des etwas exzentrischen Methoden-Namens von hier geborgt und um einige aktuellere Bots erweitert.

Ist all dies nicht der Fall heißt das entweder, dass der Besucher die fragliche Seite als erste Seite seines Besuchs aufgerufen hat (sonst hätte er einen Session-Cookie – vielleicht kommt er über ein Bookmark?) oder dass sein Browser keine Cookies akzeptiert. Um diese beiden Fälle unterscheiden zu können, wird ein spezieller Cookie gesetzt und dann auf die aktuelle Adresse redirected. Dabei wird der aktuellen Adresse ein Parameter cookie_test=Timestamp angefügt, damit wir nicht in einer Endlosschleife landen.

Der Browser des Besuchers empfängt den Redirect samt Cookie. Hat der Browser Cookies aktiviert, ruft er die Seite erneut auf und sendet dabei den Cookie mit. Dann sind wir fertig. Hat der Browser keine Cookies aktiviert, landen wir wieder in cookies_required. Hier wird im Mittelteil auf den cookie_test-Parameter geprüft. Ist er vorhanden, wird eine Flash-Message gesetzt und dann abgebrochen.

Der Nutzer sieht damit beim ersten Aufruf der Login-Seite die Warnung, dass er Cookies akzeptieren muss, um die Seite zu benutzen. Das ist vor allem auch bei der Registrierung wichtig, wo man sich ärgern würde, wenn man viele Daten eingegeben hat und dann erst erfährt, dass die Registrierung mit dem aktuellen Browser so gar nicht geht (im IE ist es ja teilweise ein ganz schöner Krampf, das umzustellen). Da wir POST-Requests von der Prüfung ausgenommen haben und den Fehler auch nicht auf einer eigenen Seite anzeigen, kann der Nutzer nun natürlich trotzdem das Formular ausfüllen und abschicken. Dann hat er aber Pech und wird früher oder später wieder per Redirect am Ausgangsort landen, wo die gleiche Routine anspringt und ihm die Cookie-Warnung anzeigt.

Cookie-Warnmeldung

Schön wäre es nun noch, das Ganze in einem Rspec/Capybara-Test grundlegend zu prüfen. Leider hat eine kurze Recherche ergeben, dass das wohl aktuell nicht ohne größere Verrenkungen geht. Wenn jemandem ein einfacher Weg einfällt, das zu machen, würde ich mich über einen Hinweis in den Kommentaren sehr freuen.

P.S.: Das Original-Cookie-Detection-Rezept macht zu Lizenzen des Codes keine Angaben. Von mir aus könnt ihr diese neue Version als Public Domain betrachten und sie nach Belieben verwenden.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Bitte beachte die Kommentarregeln: 1) Kein Spam, und bitte höflich bleiben. 2) Ins Namensfeld gehört ein Name. Gerne ein Pseudonym, aber bitte keine Keywords. 3) Keine kommerziellen Links, außer es hat Bezug zum Beitrag. mehr Details...

So, noch mal kurz drüber schauen und dann nichts wie ab damit. Vielen Dank fürs Kommentieren! :-)