Context Mobile Detect does not work with page cache enabled.

The attached patch solves this, but the user has to add a line to settings.php as the page cache is checked before hook_boot() and context based CIDs (#1303010: Page cache only uses URL as cache ID, not HTTP Accept headers or language) never made it into core and I was not in the mood to write a cache backend just for that ...

The patch is not against the module, but needs to be applied with -p6 I think.

Comments

fabianx’s picture

Status: Needs review » Fixed
kevinquillen’s picture

Does this work with Varnish and Nginx?

I've had issues with those using PHP to detect user agent and had to resort to Javascript to do it.

fabianx’s picture

It does, but you need to tweak the settings in Varnish / NGINX to do a vary based on Cookie and not strip the device cookie and to never deliver a cached version, if the cookie is not set.

The README has a little more explanation on that.

kevinquillen’s picture

Ah, yeah. On hosted setups like Acquia, you can't touch that stuff.

I can share some of my JS code with you if you like, and offer to swap between PHP or JS detection for the redirect from config.

fabianx’s picture

That sounds like a good plan! :-)

kevinquillen’s picture

I am utilizing jQuery cookie plugin to achieve this. As you may or may not know, this plugin is not loaded for an anonymous user. To make that happen, first it needs to load it. In a custom module (or Context Mobile Detect patch??) I add this to hook_init:

if (user_is_anonymous()) {
  drupal_add_js('misc/jquery.cookie.js', 'file');
}

The Drupal behavior, mobileDetection. This fires on every page and sets a JS cookie, and swaps back and forth (if the user clicked View Full Site or vice versa). You could easily replace the SITE_DOMAIN etc with values supplied by drupal_add_js settings array (taken from an admin setting UI?):

Drupal.behaviors.mobileDetection = {
        attach: function (context, settings) {
            if (/Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent)) {
                // first time visit, set the cookie and redirect
                if ($.cookie('device') === null) {
                    $.cookie('device', 'mobile', { domain: 'SITE_DOMAIN', path: '/' });
                    window.location.replace("MOBILE_URL");
                }

                // user clicked 'view mobile site' link, set the cookie and redirect
                if( /(device=mobile)/i.test(window.location.search) ) {
                    $.cookie('device', 'mobile', { domain: 'SITE_DOMAIN', path: '/' });
                    window.location.replace("MOBILE_URL");
                }

                // user clicked 'view full site' link, set the cookie and redirect
                if ( /(device=desktop)/i.test(window.location.search) ) {
                    $.cookie('device', 'desktop', { domain: 'SITE_DOMAIN', path: '/' });
                    window.location.replace("DESKTOP_URL");
                }

                if ($.cookie('device') === 'desktop' && window.location.search === "" && window.location.host === 'MOBILE_URL') {
                    window.location.replace("DESKTOP_URL");
                }

                if ($.cookie('device') === 'mobile' && window.location.search === "" && window.location.host === 'DESKTOP_URL') {
                    window.location.replace("MOBILE_URL");
                }
            }
        }
    }

One note though, I am using a subdomain, and that subdomain has its own theme. I found (the hard way) that this stuff does not work very well on a single domain with two themes.

Leveraging JS was the only way to sidestep Drupal caching and server based caching. We are using this on a Acquia hosted Drupal 7 site with Varnish and works quite well.

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

RAFA3L’s picture

I found a problem...

When some page is viewed from a desktop browser the cache file is generated fine, for example "_.html", but if I open the same page from a mobile device this load the same cached "_.html" file and not generate the mobile version.

But if I open for the first time some page from a mobile device this generate the correct cache file, for example "_device=1&device_type=blackberry.html"

The problem is when the page is opened first from a desktop browser.

I'm using the Boost module.

fabianx’s picture

For this to work with boost, you'll need to add some special rules to boost to not cache the first request if the "device" cookie is not set - similar to how we do it with varnish.

norhusz’s picture

RAFA3L!

At me the same problem. Did you manage to untie it?
What kind of rules is it necessary to write into the .htaccess file?

Thank you!

RAFA3L’s picture

norhusz’s picture

Works great. Thanks a lot!

Regards,
norhusz

nerdcore’s picture

Fabianx has suggested in #3 that Varnish can be modified to deal with this Device cookie thing properly, and indicated that there is additional information in the README...

What am I supposed to do with this?

It gets a little more tricky -- if Varnish is used. The VCL rules need to be changed as follows:
* Disable caching for the current request if no "device" cookie was sent.
* Do not strip "device" cookie
* Vary cache based on cookie content (default).

I'm sure a couple lines of VCL is all this is required for this, but can we get some actual VCL down here on this issue for those of us trying to set up context_mobile_detect behind Varnish?

Will this be sufficient?

sub vcl_recv {
  if (!req.http.Cookie ~ "^Device$") {
    return (pass);
  }
}

Any advice greatly appreciated.

kevinquillen’s picture

#6 is how I bypassed Varnish without having to touch VCL, if you are using a subdomain for mobile.

fabianx’s picture

#13: Yes, I think this VCL is sufficient as long as you make sure that your code does additionally:

* Get value of device cookie via regexp
* Strip all cookies like normally (assuming lullabot config)
* If req.http.Cookie == '' {
** Add device cookie back to req.htto.Cookie
** return (cache); // the difference of pass ...
* }

I will also need to implement this next week, so I'll report back then :-).

nerdcore’s picture

I'm seeing these HTTP headers coming from my web server and unsure how to mitigate their use with an external cache such as Varnish. It seems to me that one or all of these are causing pages not to cache. I'm unsure how to proceed and would appreciate some feedback (personal data removed):

Set-Cookie: device=3; expires=Thu, 04-Apr-2013 20:31:44 GMT; path=/; domain=.EXAMPLE.COM; httponly
Set-Cookie: device_type=0; expires=Thu, 04-Apr-2013 20:31:44 GMT; path=/; domain=.EXAMPLE.COM; httponly
Set-Cookie: pagestyle=standard; expires=Fri, 04-Apr-2014 18:31:45 GMT; path=/
Set-Cookie: SESSKEY=VALUE; expires=Sat, 27-Apr-2013 22:05:05 GMT; path=/; domain=.EXAMPLE.COM;

mgifford’s picture

The README.txt really needs some work. From the sounds of it in #3, this is supposed to be described in the readme, but it's just not..

We really need to get down to the specifics. Saying "add some special rules" isn't as useful as a snippit of config to paste in. I'll compare it to what's in Lullabot's guide http://www.lullabot.com/articles/varnish-multiple-web-servers-drupal

@kevinquillen thanks for the extra JS, but we should be able to do this within Boost or Varnish.

@Fabianx what did you do with your install?

mgifford’s picture

Status: Closed (fixed) » Active

device & device_type are definitely set as part of this module, but the other two probably aren't related:

setcookie("device", $data['device'], REQUEST_TIME+7200, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
setcookie("device_type", $data['device_type'], REQUEST_TIME+7200, $params['path'], $params['domain'], $params['secure'], $params['httponly']);

Mostly looking at setting this issue to active.

    # Remove the device & device_type cookies from Context Mobile Detect
    set req.http.Cookie = regsuball(req.http.Cookie, "device=[^;]+(; )?", "");
    set req.http.Cookie = regsuball(req.http.Cookie, "device_type=[^;]+(; )?", "");

Getting more specifics for Varnish 3 would be great.

EDIT: Useful reference https://www.varnish-cache.org/docs/3.0/tutorial/devicedetection.html

fabianx’s picture

Component: Code » Documentation
Status: Active » Needs work

Yes, docs really need work.

I was assigned to other work in the mean time so I did not yet get to update the docs for CMD with Varnish.

klausi’s picture

StatusFileSize
new3.59 KB
new5.91 KB

So here are 2 Varnish config files I'm currently experimenting with.

* Device detection is done in Varnish with https://github.com/varnish/varnish-devicedetect/
* The X-UA-Device header created by that is normalized down to mobile, tablet or pc.
* Varnish uses the Vary header internally to cache 3 versions for each device category per page.
* Before delivering the page the Vary header is changed to User-Agent, so that the X-UA-Device variation is not visible to any other external caches.
* The request to the backend (to the web server like Apache) is updated with a device cookie. Which means that the mobile detection library is never invoked in PHP, if the site is accessed solely through Varnish.

Feedback welcome!

fabianx’s picture

Wow, looks great!

fabianx’s picture

Some more work is needed here for #0:

Currently we do:

@@ -40,12 +40,53 @@ function context_mobile_detect_context_page_condition() {
+function _context_mobile_detect_add_query_string($str) {
+  if (isset($_SERVER['REQUEST_URI'])) {
+    if (strpos($_SERVER['REQUEST_URI'], '?') === FALSE) {

But we should do:

function _context_mobile_detect_add_query_string($str) {
  $_SERVER['REQUEST_URI'] = request_uri(); // Overrides request uri with itself
  // ...
  if (strpos($_SERVER['REQUEST_URI'], '?') === FALSE) {

That gives support for all webservers automatically :-D.

fadgadget’s picture

For this to work with boost, you'll need to add some special rules to boost to not cache the first request if the "device" cookie is not set - similar to how we do it with varnish.

Sorry Fabianx would you, or anyone else, know what rules i should be adding to boost module to use this module?

thanks

ckng’s picture

Here are the config we are using, showing the related config only, the rest are generally based on Lullabot version.
Please test this out, feedback/improvement are welcomed.

sub vcl_recv {
 
  # Duplicate varnish default here, as we are caching with cookie
  if (req.restarts == 0) {
    if (req.http.x-forwarded-for) {
      set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip;
    }
    else {
      set req.http.X-Forwarded-For = client.ip;
    }
  }
  if (req.request != "GET" &&
    req.request != "HEAD" &&
    req.request != "PUT" &&
    req.request != "POST" &&
    req.request != "TRACE" &&
    req.request != "OPTIONS" &&
    req.request != "DELETE") {
    /* Non-RFC2616 or CONNECT which is weird. */
    return (pipe);
  }
  if (req.request != "GET" && req.request != "HEAD") {
    /* We only deal with GET and HEAD by default */
    return (pass);
  }
 
  # [... misc config snipped ...]

  # Always cache the following file types for all users.
  if (req.url ~ "\.(png|gif|jpeg|jpg|ico|swf|css|js|html|htm|txt|woff|ttf|eot|svg)(\?.*)?$") {
    unset req.http.Cookie;
  }
 
  # Remove all cookies that Drupal doesn't need to know about. ANY remaining
  # cookie will cause the request to pass-through to Apache. For the most part
  # we always set the NO_CACHE cookie after any POST request, disabling the
  # Varnish cache temporarily. The session cookie allows all authenticated users
  # to pass through as long as they're logged in.
  if (req.http.Cookie) {
    set req.http.Cookie = ";" + req.http.Cookie;
    set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
    # make a copy for mobile context
    set req.http.tempCookie = req.http.Cookie;
    set req.http.Cookie = regsuball(req.http.Cookie, ";(SESS[a-z0-9]+|NO_CACHE)=", "; \1=");
    set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
    set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");
 
    # we only interested in mobile context "device" cookie
    set req.http.tempCookie = regsuball(req.http.tempCookie, ";(device)=", "; \1=");
    set req.http.tempCookie = regsuball(req.http.tempCookie, ";[^ ][^;]*", "");
    set req.http.tempCookie = regsuball(req.http.tempCookie, "^[; ]+|[; ]+$", "");
 
    if (req.http.Cookie == "") {
      # If there are no remaining cookies, remove the cookie header. If there
      # aren't any cookie headers, Varnish's default behavior will be to cache
      # the page.
      unset req.http.Cookie;
 
      # no mobile context, disable caching
      if (req.http.tempCookie == "") {
        unset req.http.tempCookie;
        return(pass);
      }
      else {
        # pass mobile context cookie, and cache it
        # note there are no other cookies like session or NO_CACHE here
        set req.http.Cookie = req.http.tempCookie;
        return(lookup);
      }
    }
    else {
      # If there is any cookies left (a session or NO_CACHE cookie), do not
      # cache the page. Pass it on to Apache directly.
      return (pass);
    }
  }
}
 
# Routine used to determine the cache key if storing/retrieving a cached page.
sub vcl_hash {
  # Include cookie in cache hash.
  # Only for mobile context cookie
  if (req.http.Cookie == req.http.tempCookie) {
    set req.http.Cookie = req.http.tempCookie;
    hash_data(req.http.Cookie);
  }
}

shaneod’s picture

Issue summary: View changes

Using standard Drupal Caching. I've included the line from the read me but it is still throwing off the contexts off on different devices depending when i have caching for anon users enabled.

As soon as i switch caching off it works perfectly. Any ideas?

*** Edit - my mistake - My Settings.php file had not uploaded - now working with caching place.

paolomainardi’s picture

Attached a patch, which does the following things:

1) Parse request headers if available:

 $req_device = variable_get('context_mobile_detect_req_header_device', 'HTTP_X_UA_DEVICE');
  $req_vendor = variable_get('context_mobile_detect_req_header_type', 'HTTP_X_UA_VENDOR');
  if (!empty($_SERVER[$req_device])
    && !empty($_SERVER[$req_vendor])) {
    $device = $_SERVER[$req_device];
    $vendor = $_SERVER[$req_vendor];
    if ($device !== 'desktop') {
      $data['device'] = ($device === 'smartphone' ? 1 : 2);
      $data['device_type'] = (isset($types[$vendor]) ? $types[$vendor] : 'generic');
    }
    return $data;
  }

VCL Example: https://www.drupal.org/node/2638156#comment-10684950

2) Use cookies when req headers not available, as already does with a bit of refactoring.
3) Make _context_mobile_detect_detect() and _context_mobile_detect_devices_types() drupal_static powered.

mgifford’s picture

Status: Needs work » Needs review