Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Settings PLL_COOKIE has no effect when using static caching through routing #450

Open
5 of 6 tasks
martin-braun opened this issue Feb 1, 2020 · 11 comments
Open
5 of 6 tasks

Comments

@martin-braun
Copy link

martin-braun commented Feb 1, 2020

Prerequisites

  • This is not a usage question (Those should be directed to the community supported forum, unless this is a question about Polylang Pro in which case you should use the helpdesk).
  • I have searched for similar issues in both open and closed tickets and cannot find a duplicate.
  • The issue still exists against the latest master branch of Polylang and the latest WordPress version.
  • This bug happens with only Polylang plugin active
  • This bug happens with a default WordPress theme active
  • I can reproduce this bug consistently

Introduction

This issue is a combination of the issues pll_language cookie and #248, which both got solved independently, but none of them work if you make use of all of this.

The website should have Polylang, GDPR Cookie Compliance and WP Fastest Cache installed.

The idea is to only write the polylang cookie when the user gives consent to the Strictly Necessary Cookies in the popup of the GDPR Cookie Compliance plugin. A quick simple way to do so is to put this simple one liner in wp-config.php:

if(!isset($_COOKIE['moove_gdpr_popup'])) define( 'PLL_COOKIE', false);

However, this all breaks as soon as we bring WPFC into the game. WPFC is smart. It modifies the .htaccess to bypass the backend code, entirely. So afaik it serves static HTML/JS that was generated on the first load.

So this line will never be reached, so there is no chance to write the cookie after consent was given.

Possible solution

Modern cache solutions (including WPFC) are too smart, they bypass the backend PHP altogether from the second load on. This is brilliant to get the best loading performance out of it and should not cause any issues, as long as the content on the website is not dynamic.

Polylang should move its cookie set handling entirely to JS, in all cases. However, it should provide a JS api to prevent cookie creation. If the cookie set logic happens in jQuery's ready function and if it's checking for a specific flag in the window scope, I could disallow the cookie creation before.

Here is a basic example how this could look like:

// my code:
(function() {
  var consent = JSON.parse(decodeURIComponent(("; " + document.cookie).split("; moove_gdpr_popup=").pop().split(";").shift() || "{}")); // gets the cookie with the name 'moove_gdpr_popup'
  if(!consent.strict) { // strict cookies not set or declined?
    window.PLL_COOKIE = false; // disable cookie creation from Polylang
  }
})();

// static JS code from Polylang:
jQuery(document).ready(function( $ ) {
  if(window.PLL_COOKIE !== false) {
    // cookie set logic of Polylang ...
  }
});

This code would never be required to change, thus it would work, even when this all gets cached. It would not contain any conditional JS output.

@chesio
Copy link
Contributor

chesio commented Feb 2, 2020

Hi @martin-braun,

Just a side note from fellow static cache file user: I found out that Polylang works just fine without the cookie if you have The language is set from the directory name in pretty permalinks setting active.

I actually now have define('PLL_COOKIE', false); in wp-config.php by almost all projects to avoid having to mention the cookie in site privacy policy, problems with static file caching etc.

@martin-braun
Copy link
Author

martin-braun commented Feb 2, 2020

Hi @martin-braun,

Just a side note from fellow static cache file user: I found out that Polylang works just fine without the cookie if you have The language is set from the directory name in pretty permalinks setting active.

I actually now have define('PLL_COOKIE', false); in wp-config.php by almost all projects to avoid having to mention the cookie in site privacy policy, problems with static file caching etc.

Hi @chesio,

thank you for your reply. Since the time I created this issue, this is what I do, too, and it - as far as I could observe - really works. I thought when you go to the home page without a cookie it will redirect all the time to my primary language page that is not my homepage, but it does not. It either uses localStorage with JS redirection or session with PHP redirection.

I will leave my issue open for the cases where The language is set from the directory name in pretty permalinks is not set, though, but I will absolutely go the same route as you did to avoid those issue once and for all.

Happy Sunday!

@chesio
Copy link
Contributor

chesio commented Feb 2, 2020

I thought when you go to the home page without a cookie it will redirect all the time to my primary language page that is not my homepage, but it does not.

Do you have Detect browser language setting active? In such case you should be redirected according to the language preferences set in your browser. That's expected behaviour.

@martin-braun
Copy link
Author

martin-braun commented Feb 12, 2020

Do you have Detect browser language setting active? In such case you should be redirected according to the language preferences set in your browser. That's expected behaviour.

@chesio Hi, I removed my last message to reply to this quote again. I was wrong, the first redirection was really broken due to WPFC. It seems whatever solution is implemented in Polylang, it's not working.

So, I implemented some JavaScript that is limited to be used for languages without dialect specification, so "en", "de", "it", but not "en-GB" or "de-DE".

Together with define('PLL_COOKIE', false); in the wp-config.php, this solution, that makes use of the localStorage instead, works flawlessly:

(function() {
  if(location.pathname === '/') {
    var lang = localStorage.getItem("lang");
    var avaiLangs = ["en", "de"]; // configure me, first language is default
    if(!lang) {
      var prefLangs = navigator.languages || [ navigator.language || window.navigator.userLanguage ];
      for(var i = 0; i < prefLangs.length; i++) {
        var prefLang = prefLangs[i].split('-')[0];
        for(var j = 0; j < avaiLangs.length; j++) {
          if(prefLang === avaiLangs[j]) {
            localStorage.setItem("lang", avaiLangs[j]);
            if(j > 0) {
              location.href = '/' + j ? avaiLangs[j] + '/' : '';
            }
            return;
          }
        }
      }
      localStorage.setItem("lang", avaiLangs[0]);
    } else if(lang !== avaiLangs[0]) {
      for(var i = 0; i < avaiLangs.length; i++) {
        if(avaiLangs[i] === lang) {
           location.href = '/' + lang + '/';
        }
      }
    }
  }
  jQuery(document).ready(function( $ ) {
    $(".nav .lang-item a").on("click", function() {
      localStorage.setItem("lang", this.getAttribute("hreflang").split('-')[0]);
      return true;
    });
  });
})();

This is designed to have the primary language on root (/) and any additional language in the sub language directory, such as /de/. It will only attempt a redirection on the root website. Unfortunately, it's JS only, slowing the first page load down if you visit the site the first time without having the right primary language in your browser. Still, loading a cached website twice is way faster than running the PHP of WP once.

This script needs to be implemented inline in the header of the website. I share this, so someone else in the future with the same problem can spare some time here doing the same.

@chesio
Copy link
Contributor

chesio commented Feb 12, 2020

@martin-braun If your website runs on Apache then you might implement the redirects using mod_rewrite rules - this is the fastest way.

I believe it's even possible to do so in case your default language has no /lang part in URL (using self-referer check technique).

If you're interested, I can post some examples.

@martin-braun
Copy link
Author

martin-braun commented Feb 14, 2020

@martin-braun If your website runs on Apache then you might implement the redirects using mod_rewrite rules - this is the fastest way.

I believe it's even possible to do so in case your default language has no /lang part in URL (using self-referer check technique).

If you're interested, I can post some examples.

Hi @chesio,

after deployment, my solution is not fast enough, because of the 2nd initial page load. I decided to disable "Hide URL language information for default language" in the Polylang settings, so it will always use the current language in the URL, like /en/ or /de/. This decision was good, because now I can follow your approach. It might will not remember the current language, but since I use 301 redirects, I hope the browser will cache the URL of the specified language, that was visited before.

I inserted this into the .htaccess:

# BEGIN Language Redirect

RewriteCond %{HTTP:Accept-Language} ^en [NC]
RewriteRule ^$ http://%{HTTP_HOST}/en/ [L,R=301]

RewriteCond %{HTTP:Accept-Language} ^de [NC]
RewriteRule ^$ http://%{HTTP_HOST}/de/ [L,R=301]

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-l

# END Language Redirect

@chesio
Copy link
Contributor

chesio commented Feb 18, 2020

Hi @martin-braun,

These rules are slightly more powerful:

RewriteCond %{HTTP:Accept-Language} (en|de) [NC]
RewriteRule ^$ http://%{HTTP_HOST}/%1/ [L,R=302]

RewriteRule ^$ http://%{HTTP_HOST}/en/ [L,R=302]

The first rule handles any user that has either EN or DE set as accepted language regardless of priority. For example an Italian user who speaks German as well will likely have Italian as most prefered language (ie. ^it) and still got redirected to German version.
In case the user has both EN and DE set, it favors EN - this can be changed by swapping the order of languages in (en|de) regex - the %1 backreference holds the first match.

The second rule handles users that have neither EN nor DE set as accepted language and redirects them to EN version.

Also, I would stick to 302 status code for redirect. Browser cache is not the only cache the HTTP request may encounter along its way (think of proxies etc.). 301 says permanent, which isn't true as the redirect depends on Accept-Language header. For that I also use this additional snippet:

# Tell bots, that the content is based on language
<IfModule mod_headers.c>
  Header append Vary Accept-Language
</IfModule>

@martin-braun
Copy link
Author

martin-braun commented Feb 19, 2020

Hi @chesio,

this is great. Thank you for taking your time to improve my solution. I wasn't thinking on cases where the English AND German language were missing in the Accept-Language header. So my solution would most likely not work as intended.

You are also right about the permanent redirect. It makes sense and I will make use of it, definitely.

Thank you!

@KZeni
Copy link

KZeni commented Jul 14, 2021

There have been too many topics on this feature included in Polylang having issues when there are 2 alternative approaches that work better & more reliably with caching (JS-based & .htaccess-based like this latest suggestion.) I somehow missed this one when I discussed this issue further at #294 (comment) (and also first mentioned in an older #236 (comment)).

It seems strange that the option offered by Polylang is the least friendly when it comes to one of the major ways websites make their homepage & other pages load as quickly as possible when there are 2 other ways that wouldn't have that issue just to have it said addressing it is not possible (even with caching plugins adding support effectively making the page load slowly at least once [when it ideally should always be fast]) per the method being used now has an inherent limitation (hence why one or both of these other approaches should be offered) while then just providing custom code to be manually implemented on people's sites rather than implementing it officially a-la the limited PHP-based option provided now (definitely not ideal.)

@esginmurat
Copy link

esginmurat commented Jan 23, 2022

Hi @martin-braun,

These rules are slightly more powerful:

RewriteCond %{HTTP:Accept-Language} (en|de) [NC]
RewriteRule ^$ http://%{HTTP_HOST}/%1/ [L,R=302]

RewriteRule ^$ http://%{HTTP_HOST}/en/ [L,R=302]

The first rule handles any user that has either EN or DE set as accepted language regardless of priority. For example an Italian user who speaks German as well will likely have Italian as most prefered language (ie. ^it) and still got redirected to German version. In case the user has both EN and DE set, it favors EN - this can be changed by swapping the order of languages in (en|de) regex - the %1 backreference holds the first match.

The second rule handles users that have neither EN nor DE set as accepted language and redirects them to EN version.

Also, I would stick to 302 status code for redirect. Browser cache is not the only cache the HTTP request may encounter along its way (think of proxies etc.). 301 says permanent, which isn't true as the redirect depends on Accept-Language header. For that I also use this additional snippet:

# Tell bots, that the content is based on language
<IfModule mod_headers.c>
  Header append Vary Accept-Language
</IfModule>

Thanks @chesio

However, there is one problem. "Hide URL language information for default language" option is active. No grammar is added to the URL when the default language code is active. However, a second language is also added. (For example, such as /de/, /it/, /fr/). How can I update the above code in this case.? Thank you.

@chesio
Copy link
Contributor

chesio commented Feb 17, 2022

Hi @esginmurat,

short answer - you have to use self-referer check, see my answer here #450 (comment).


Long answer: That's a quite tricky setup and to be honest I always do my best to discourage my clients and colleagues from setting up our multi-language projects this way. I would recommend you to do the same ;-)

The main problem is that when someone visits the root URL - let's say www.example.com, you have to distinguish these two visits:

  1. Someone visits the website for the first time and should be (optionally) redirected to proper language version based on his language preference configured in browser.
  2. Someone visits the homepage of main language version - for example by clicking a logo or switching a language.

You may already see the problem: what if someone has browser configured to prefer one of the secondary languages on your website (= gets redirected due to 1.), but he actually wants to read your website in its main language (= should stay on home page due to 2.)?

An example:

  1. The website has en as main language with homepage at www.example.com and de as secondary language with homepage at www.example.com/de.
  2. User - (let's call him Fritz) has browser configured to prefer content in German (de).

Now when Fritz visits www.example.com the first time, he gets redirected to www.example.com/de due to his browser configuration. But Fritz would like to read the website in English, so he immediately clicks on GB/US flag in German version and goes back to www.example.com where... he gets redirected to www.example.com/de due to his browser configuration. Obviously, Fritz is not happy.

One way how to solve this problem is... to set a cookie when user switches between languages and include the cookie in redirect decision making. That's why Polylang sets the PLL_COOKIE :-)

The other way is to check whether the user comes to your root URL from outside world or your own website - the referrer check might help to distinguish these two cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants