Hi all,

I am having a problem with switchtheme and anonymous users. If I disable normal caching, they are able to switch themes, however when enabled, only authenticated users are permitted. Is this a known limitation of switchtheme? You can see the problem in action at www.myappleguide.com

CommentFileSizeAuthor
#21 switchmodule-caching-D6.patch463 bytesAndrew Answer
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

sun’s picture

Category: task » support

Hm. Yes, that seems to be a limitation. Drupal's page caching for anonymous users statically caches all pages. Did you try the latest development snapshot already?

sun’s picture

Status: Active » Closed (won't fix)

Sorry, without further information this issue can only be marked as won't fix. Feel free to re-open this issue if you want to provide further information.

gausarts’s picture

hi,
I am in the process of proposing a CVS account, and I need to show the maintainers where my themes alive. It'll be great if it works for anonymous in normal cache. The latest dev release didn't even work for registered users, so I switched back to the stable one. At least it consistently works for the registered.

So I'd love to know if there's a plan to improve it. Thanks

bellesmanieres’s picture

Hi,
The only workaround I could find is using the ability of Switchteme to set the theme using the $_GET variable. It is'nt really satisfactory nor efficient, but I give it here for what it's worth.

Adding the following in settings.php will pass the theme parameter to all drupal generated links, making them look like example.com/content/mycontent.html?theme=theme_name.
Be aware that :

  • The main drawback is that harcoded internal links inside the content (eg href="content/mycontent.html" or href="node/17") won't work. You'll have to rewrite them, using the l() function for example. I have only a few, so that wasn't an issue for me but if you've got a lot, it could be a pain. However, all generated links (using l(), url(), theme(), etc.) will work, meaning that it should work if you use modules like Path Filter (untested).
  • You wont be able to use the Switchtheme's form. You'll have either to make your own switching link in your page passing the $_GEt parameter or use Switchtheme's random theme.
  • My themes are all named 'theme1', 'theme2', etc. You'll have to adjust this to your needs.
function custom_url_rewrite_outbound(&$path, &$options, $original_path) {

	if(isset($_GET['theme'])) {
		 $pos= strpos($_GET['theme'], 'theme'); // a quick extra test to avoid harmful cross-site parameters.
		 	if ($pos === 0) {
    			$options= array('query' => 'theme='.check_plain($_GET['theme']),);
			}
	}
}

That should be sufficient to make anonymous users keep a theme.

Going further
As I want to stick with clean urls and don't want duplicate content to arise in search engine, i make a bit of additional rewriting.

Adding the following in settings.php ...

function custom_url_rewrite_outbound(&$path, &$options, $original_path) {

	if(isset($_GET['theme'])) {
		 $pos= strpos($_GET['theme'], 'theme'); // a quick extra test to avoid harmful parameters (maybe a bit paranoid in this case ?)
		 	if ($pos === 0) {
    			$path= check_plain($_GET['theme']).'/'.$path ;
			}
	}
}

... and these lines in .htaccess, BEFORE drupal's own rewriting rules ...

  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteCond %{REQUEST_URI} !=/favicon.ico
  RewriteRule ^theme([0-9]+)/(.*)$ index.php?q=$2&theme=theme$1 [L,QSA]

... let me have url looking like 'theme2/content/mycontent.html' and exclude search engines from indexing the themed versions of the site via robots.txt.
You can see it 'in action' at http://www.pascal-morin.com

So, I know this in no way a good and proper solution, but I hope it can be of some use for a few of you.
Pascal

@sun : I'm not sure if this really the best place to post (in fact, I'm quite sure it isn't) but I couldn't think of a more suitable page. So, feel free to edit/move/delete my post. Thanks

sun’s picture

Title: Anonymous Users and Theme Switching » Allow to switch themes with page caching enabled
Version: 6.x-1.0 » 6.x-1.x-dev
Component: Miscellaneous » Code
Category: support » feature
Status: Closed (won't fix) » Active

Re-opening as feature request.

I'm not entirely sure, but my first guess would be to investigate whether we can use hook_init() to intercept the page request and assign a new theme. Probably needs a separate page cache per theme.

bellesmanieres’s picture

That would for sure increase performance to test the page request in hook_init instead (custom_url_rewrite is called +100 times / page, states the drupal doc).
I hope I'm not missing the point (I'm quite new to drupal API and do not understand well all the hook mechanism), but the main problem I see is that hook_init isn't called on cached pages, so we still have to perform url rewriting to make drupal generate a new page.
Moreover, I did'nt find a way to alter the url besides custom_url_rewrite_in/outbound (the incoming rewriting rules i made with .htaccess can be done that way too), that are not really hooks, as they need to be put in settings.php.
As you're far more experimented than me, you might have a few clues of how this can be done ? I'll try to experiment on all that next week, when I'll have more time.

gausarts’s picture

Thanks for considering this as feature request. It's a great help.

JurriaanRoelofs’s picture

subscribing

aryanto’s picture

I am waiting for this feature as well.

mikeytown2’s picture

Set a cookie, redirect to that theme (see global redirect). Whats the URL structure for different themes?

aryanto’s picture

@mikeytown2

I am not sure what your question is referring to. But if you meant the structure of the URL for each of the themes, then from what I experienced there is no different at all. The URL remains the same when I switch from one theme to another. So I am not sure if global redirect would be able to help.

mikeytown2’s picture

@aryanto
Guess this module does a POST to set the theme, If it used a GET, then it would be compatible with caching.

sun’s picture

But only if it would implement a custom_url_rewrite_outbound() handler that rewrites all following URLs to use the query string as well.

mikeytown2’s picture

You could do it ghetto. Check for a cookie in jquery; get URL, if ?theme=* isn't set right, redirect.

AndrasAcs’s picture

Version: 6.x-1.x-dev » 6.x-1.1

As far as I understand, hook_init is not called on cached pages, but hook_but is also called on cached pages. So trying to rewrite the code to use hook_boot could help. I am looking very much forward to a Switchthemes version, that can handle caching.

mikeytown2’s picture

hook_exit() would be another option. boost_exit might give some inspiration.

/**
 * Implementation of hook_exit(). Performs cleanup tasks.
 *
 * For POST requests by anonymous visitors, this adds a dummy query string
 * to any URL being redirected to using drupal_goto().
 *
 * This is pretty much a hack that assumes a bit too much familiarity with
 * what happens under the hood of the Drupal core function drupal_goto().
 *
 * It's necessary, though, in order for any session messages set on form
 * submission to actually show up on the next page if that page has been
 * cached by Boost.
 */
function boost_exit($destination = NULL) {
  // Check that hook_exit() was invoked by drupal_goto() for a POST request:
  if (!empty($destination) && $_SERVER['REQUEST_METHOD'] == 'POST') {

    // Check that we're dealing with an anonymous visitor. and that some
    // session messages have actually been set during this page request:
    global $user;
    if (empty($user->uid) && ($messages = drupal_set_message())) {
      // FIXME: call any remaining exit hooks since we're about to terminate?

      $query_parts = parse_url($destination);
      // Add a nocache parameter to query. Such pages will never be cached
      $query_parts['query'] .= (empty($query_parts['query']) ? '' : '&') . 'nocache=1';

      // Rebuild the URL with the new query string.  Do not use url() since
      // destination has presumably already been run through url().
      $destination = boost_glue_url($query_parts);

      // Do what drupal_goto() would do if we were to return to it:
      exit(header('Location: ' . $destination));
    }
  }
}

Personally I think javascript would be the simple solution. Get it working there first then try to do it in PHP.
http://plugins.jquery.com/project/Cookie
http://plugins.jquery.com/project/parseQuery

IF Cookie.theme != NULL && Query.theme != Cookie.theme THEN
  window.location = currentURL + ?theme= + Cookie.theme
ENDIF
mdixoncm’s picture

I reckon the easiest way to do this would be to use the purl module - http://drupal.org/project/purl - this will take care of all the "token" persisting for you, don't need to worry about cookies or javascript etc - lovely jubely :)

Rob C’s picture

Subscribing

Any news on this?

I'm using another approach, i patched the switchtheme module to show one single submit button with a hidden value for the theme name. I got four themes installed, but it only has to switch between the first two, created a small piece of code that checks the current theme name, gets the first two active themes and a case that switched the output to the hidden field (theme name) and submit button text (switchtheme alias from one of the first two themes).

And yes it has the same problem when caching is enabled, i need this for a new site i'm working on and i already have so much scripts that i would really like it to get this to work without an additional javascript. (aka the separate cache per theme sounds very very nice and i guess that would make the most sense? Still need to fumble the boot function somewhere...)(didn't see what purl can do, sounds promising, will look into this, or something similar to http://drupal.org/node/361832#comment-1587680 and trow away the form and just create some old fashion 'links'. :-)

RockSoup’s picture

Subscribe

Mike could you please talk a bit about how you might implement this?

I am using switchtheme and caching is not allowing the theme to change once it has been cached.

Thanks!

Andrew Answer’s picture

What about simply clear page cache when theme switched? This will fix that problem.
I found easy workaround for D6.20: add
db_query("TRUNCATE TABLE {cache_page}");
into switchtheme.module::switchtheme_switch_form_submit() function here:

 ...
  elseif (user_access('switch theme')) {
    // Save the setting in the session for anonymous users.
    $_SESSION['custom_theme'] = $form_state['values']['theme'];
    db_query("TRUNCATE TABLE {cache_page}");
  }
}
Andrew Answer’s picture

FileSize
463 bytes

I made a patch for this.

mikeytown2’s picture

Might as well run with the page cache disabled. If 10 different uses change the theme for them self's it still doesn't solve this issue. The cache from one theme will be served to another user who is set to use a different theme.

rjacobs’s picture

Hello,

Until there is a solution built-in to the module itself, I think it would be useful to note in the documentation (README and project pages) that switchtheme is incompatible with Drupal page caching. This, of course, only applies if you have the permissions set to allow anonymous users to switch themes (and expect them to be able to do so), but it seems like an outstanding incompatibility non-the-less. Noting this somewhere might save folks some troubleshooting time.

Cheers,
Ryan

mikeytown2’s picture

Just had a brain storm; details on how to do it are here
http://groups.drupal.org/node/121054#comment-411834
Does NOT work with boost; or if it does not recommend.

RockSoup’s picture

I have a solution that is working for me for this issue with much help from mikeytown2.

Using this solution means that the first time a user visits the site they will be served an uncached page. Once the first page is loaded and switchtheme does it's magic to assign the proper theme we set a cookie in the user's browser with a value of the theme name. A few lines are added to settings.php that check for that cookie and when it exists the page is served from cache. We are setting a separate cached version of the page as sun suggested in #5 above.

Here is the code added to switchtheme.module:


function switchtheme_init() {
  global $custom_theme, $user;
  drupal_add_css(drupal_get_path('module', 'switchtheme') .'/switchtheme.css');

  // If query parameter 'theme' is set, we always assign a new theme.
  if (isset($_REQUEST['theme'])) {
    $form = array('values' => array('theme' => $_REQUEST['theme']));
    switchtheme_switch_form_submit('', $form);
  }
  // Other modules may already have set $custom_theme, so we actually switch
  // the theme only if $custom_theme has not been set yet, or if we are on
  // administrative pages and admin_theme has been set to "System default"
  // ('0').  Also note that the chosen theme is stored in the global user
  // object for authenticated users; that value is automatically used across
  // sessions by Drupal core if the "select different theme" permission has
  // been granted.
  if (isset($_SESSION['custom_theme']) && (!isset($custom_theme) || $custom_theme === '0')) {
    $custom_theme = $_SESSION['custom_theme'];
  }

  if (module_exists('browscap') && variable_get('switchtheme_browser_enabled', FALSE)) {
    $browser = browscap_get_browser();
    if (isset($browser['parent'])) {
      $parent = trim($browser['parent']);
      $browser_theme = variable_get('switchtheme_browser_'. md5($parent), 'default');
      if ($browser_theme != 'default') {
        $custom_theme = $browser_theme;
      }
    }
  }
  // If no cookie is set set a cookie with the value of theme_default.
  if(empty( $_COOKIE['custom_theme'])) {
    $temp = empty($custom_theme) ? variable_get('theme_default') : $custom_theme;
    setcookie('custom_theme', $temp, time() + 60*60*24*30, ini_get('session.cookie_path'), ini_get('session.cookie_domain'));
  }

}

And the code from settings.php:


// Code for theme switching.

//  Check against themes on the site, this is a whitelist of allowed themes.

$approved_themes = array(
  'machine_name_of_desktop_theme' => TRUE,
  'machine_name_of_mobile_theme' => TRUE,
);

// Grabbing cookie if it exists.
$my_theme = $_COOKIE['custom_theme'] ;

// Check if cookie is set and is an approved theme and add string to URL to set theme. 
if (!empty($my_theme) && !empty($approved_themes[$my_theme])) {
  $_SERVER['REQUEST_URI'] .= '#' . $my_theme;
  $_SERVER['QUERY_STRING'] .= '#' . $my_theme;
  $conf['theme_default'] = $my_theme;
}
//  Cache disabled if cookie not set so switchtheme can set the cookie.
elseif (empty($my_theme)){
  $conf['cache'] = CACHE_DISABLED;
}

RockSoup’s picture

Updating issue to show only minimum required code.

Also want to mention that this code came from collaboration at the most excellent Seattle Drupal User Group.

So, one only needs to add these 5 lines to the bottom of switchtheme_init()

  // If no cookie is set set a cookie with the value of theme_default.
  if(empty( $_COOKIE['custom_theme'])) {
    $temp = empty($custom_theme) ? variable_get('theme_default') : $custom_theme;
    setcookie('custom_theme', $temp, time() + 60*60*24*30, ini_get('session.cookie_path'), ini_get('session.cookie_domain'));
  }

Here is the bare minimum for settings.php

// Check if cookie is set.
if (!empty($_COOKIE['custom_theme'])) {
  // Add theme name to page cache id.
  $_SERVER['REQUEST_URI'] .= '#' . $_COOKIE['custom_theme'];
  // Set Theme.
  $conf['theme_default'] = $_COOKIE['custom_theme'];
}
// Disable page cache if cookie not set so switchtheme can set the cookie.
else {
  $conf['cache'] = CACHE_DISABLED;
}
trante’s picture

@RockSoup thank you for patch.

I was curious for that issue for months, but didn't solve this issue in a good way.
After some research i found ThemeKey module.
It is written in info page:

And unlike other theme switching modules, ThemeKey should play well with internal and external page caches, like Boost or Varnish, even for anonymous users.

I write this if somebody like me needs Boost caching + Theme switching.

PS: ThemeKey switches themes relatyed to your written rules, on the contrary of Themeswitcher gives a manual preference.

PS2: In ThemeKey's settings, i saw this and i became suspicious. "Page Cache
Unsupported! If you enable page caching on your system, you have to ensure that any rule using this property is wrapped by another rule that excludes anonymous users, such as "user:uid > 0"."

nevosa’s picture

I've created a sandbox project that allows for theme selection for anonymous users with page caching enabled,
view it here:
Anonymous Theme
and leave comments/reviews.

mikeytown2’s picture

@nevos
hook boot only fires on a cache miss. hook exit fires after the page has been retried from the cache_page bin. See case DRUPAL_BOOTSTRAP_LATE_PAGE_CACHE: in _drupal_bootstrap(). Your sandbox won't work from what I can tell.

rjacobs’s picture

I'm not 100% on this but I believe that hook_boot will indeed always run even when caching is enabled, so long as aggressive caching is not enabled. I think the issue may be that Drupal decides what cached details to serve up before hook_boot fires, so it's hard to manipulate these details by the time we get to hook_boot.

The following links might be useful here:
http://drupal.org/node/916552
http://drupal.org/sandbox/brian_c/1413374

Sorry if I'm off-base on this, I have not been following this thread for a while and I have not looked the nevos's sandbox... I just saw comments #28 and #29, and they lead me to believe this is related to a separate issue I was researching where the above links proved very useful.

mikeytown2’s picture

hook boot will run. read the if statement incorrectly (thought I saw an and instead of an or).

nevosa’s picture

@mikeytown2
I did see the the or statement there, so I tested it some more, to find that hook_boot does actually get called.
However in testing the code - on some situations cached pages were still being served.

Perhaps I'm wrong in this, but I've added the "dynamic cache" to all the theme switching cases via,
http://drupal.org/sandbox/brian_c/1413374
and it work perfect.

But I'm still not sure the bootstrap hijacking is necessary... let me know if you have a clearer notion of this,

Thanks,

mikeytown2’s picture

The only way to do this is to put the hijack code in settings.php. Unless you do that, it wont work. See #26

rjacobs’s picture

It could be that you need to make "dynamic cache" a dependency for this new module. My understanding is that this kind of thing is exactly why that module was made. From what I've been able to read, and as noted in #26 and #33, the only real alternative is making custom mods to settings.php.

What I understand is that a cached page is calculated and retrieved from the DB (as the content to be loaded) before hook_boot, so adjusting any global cache settings in hook_boot does not stop the cached version from being prepared. Then after hook_boot, and before bootstrapping is done, the cached version is loaded. So the only way to dynamically prevent a cached version from being used, based on any logic called in hook_boot, is to hijack the rest of the bootstrap process that happens after hook_boot.

However, if you do your logic (to test if you need to dynamically disable caching) in settings.php, that will fire before the cached page is calculated and retrieved from the DB (and certainly before hook_boot), making it the only other option... though not as "modular" of an option.

Even more reading available here:
http://drupal.org/node/875152

I just wanted to convey a summary of my understanding in case that helps. The notes in the discussions leading up to brian_c's module certainly offer a better and more complete explanation.

idflood’s picture

Another method is to simply have different paths for different themes (either different subdomain, path prefix, query string, .... ). There is the purl module (http://drupal.org/project/purl) that make it relatively easy to do. This makes what is proposed in #4 much more easy to implement. Here is a first patch I've done for another module using this: http://drupal.org/files/mobile_theme_cache_purl-328675-10.patch

Since the issue is relatively similar to this here is the link: #328675: Single domain incompatible w/ Drupal 'Normal' caching. Switch theme based on domain...

nevosa’s picture

@rjacobs thank you for you comments, they helped a lot!
So I guess hijacking boot would do the trick, and the anonymous theme does seem to work well this way.

However, I am troubled by the fact that the dynamic cache is still in a sandbox mode, and it is too short to be considered a module - so I can't really set a dependency, on the other hand it wouldn't be very collaborative to just use that code inside hook boot and suggest a new module...

Do you have an idea how to advance this?

Thanks,

rjacobs’s picture

Hi nevos. If all you need to do is turn off the cache for a page load based on some conditional logic in hook_boot, then I do imagine that dynamic_cache would be the easiest "drop-in" solution. Having dynamic_cache maintained separately might also help ensure that it gets updated whenever part of Drupal core's bootstrap gets updated, otherwise you would have to monitor your module for those updates as well. In this regard keeping it separate could be an asset... though I'm not too sure if that effort will become an official project or not. I'm personally only using it in conjunction with a custom module for a single site, so my usage of it is very isolated.

I guess the real danger of the whole dynamic_cache approach is that it's dependent on another module (dynamic_cache) properly mimicking Drupal core functionality. It can probably work pretty gracefully, but only if dynamic_cache is kept up-to-date with core's bootstrap code.

Perhaps you could explore whatever is the easiest approach to turn your module into a working proof-of-concept and later also think about adapting it to work with other alternative "dynamic cache control" methods? It looks like you would be offering quite a few dynamic methods to control a theme selection at 'runtime', so the alternative of putting all that logic directly into settings.php might not fly, but the PURL method in #35 might be interesting to explore. I'm guessing that method would even allow for alternatively themed versions of pages to be cached given that they would live under their own URLs.

Just some general thoughts. I'd be curios what others think as well.

krishanchandra’s picture

Sarah_G’s picture

Regarding the code by RockSoup in #26
I've been trying to get this to work. Should I be replacing any of the variables with my unique variables? For example where it says 'theme_default' am I supposed to put in the name of my theme on my site? I know this is really old and I've been working it for over a year. I felt like I was close but the cacheing part just doesn't work all the time. Thanks for any insight.