Cookie consent in the EU

According to the so-called Cookie Law (ePrivacy Directive) and GDPR, cookies can not be stored on an EU citizen's device without consent. There are plenty of articles which go in-depth on how to interpret the sometime ambigious regulations. This article will not do that, but provide a solution on how to implement this.

From the many articles out there, it's clear that that asking cookie consent for EU citizen is almost always mandatory. Almost always? From what I've understood there are three exemptions: Essential, performance and functionality cookies, but even those are best limited in time as much as possbile.

One can easily find some available plugins online to process the cookie consent, but for those who rather do it themselves, this article provides a start-to-finish guide on how to code your own cookie consent functionality in PHP. Of course we also need HTML and CSS for this. Below is a list of elements this guide takes into account, which I feel provides a regulation compliant solution:

  • The code only affects EU-citizens
  • At the first visit, only an essential, time limited cookie is placed. If consent is given, all cookies are loaded according to the preferences.
  • There is a clearly visible banner which asks for user consent, gained by continuing to use the website.
  • Link to a Cookie Policy page, in which the visitor is informed about the
  • Allow the user to change their preferences.

PHP setcookie()

To keep track of the user preferences and cookie consent we will set a cookie. This cookie will contain the consent information and I therefor deem it essential. I do however, limit this cookie in time to 15 minutes. To do this we'll use PHP's setcookie() and it's parameters:

setcookie(name,value,expires,path,domain)

Let's take a look at the parameters and values we'll be using:

  • Value: a string value.
    • We'll store the consent and preferences in a dictionary and use  json_encode($cookies) and json_decode($_COOKIE['cookies'],True). do encode/decode the dictionary to a string representation which can be stored in the cookie. In the decode function we give as second parameter True, such that it decodes to a PHP associative array (dictionary).
  • Expires:Time (in seconds) when (not until!) the cookie expires.
    • time()+60*15 (15 minutes)  before we have consent, time()+60*60*24*90 (90 days) after consent.
  • Path: Which path on domain the cookie applies to.
    • "/", as we want it to work everywhere on our domain.
  • Domain: The domain the cookie is applicable to.
    • "mountcreo.com", so that all subdomains (including www.) are included (for localhost you can just put localhost)

Take into account that setcookie() must be called before anything is outputted. This means we'll call in in the header section of our html page. In my case all my pages start by calling setup.php, so I set my cookies there.

Setting the cookie at first (and second) visit

In this section, we'll use our "cookies" cookie to keep track of the user's consent. As we'll now set the actual cookie, all the code in this section should be placed before anything is outputted. First we'll check whether or not we'll already know the user's preferences by checking if the cookie exists:

if(!isset($_COOKIE['cookies'])){
	// First time visitor
}else{
	// We'll get the user preferences
}

Let's start with the first time visitor. As we do not want to bother users who don't need to give consent, a geographic check is done. For this we feed the user's IP to https://www.geoplugin.com/. The returned array can be decoded, after which the country code can be found. Note that finding the IP is not foolproof. $_SERVER['HTTP_X_FORWARDED_FOR']can be easily set by the user, so beware of its contents. $_SERVER['REMOTE_ADDR'] will not always return the IP of the user, for example when a proxy is set. On localhost the latter will also output ::1, so you might want to temporarily set $ip yourself.

if(!isset($_COOKIE['cookies'])){
	// First time visitor
	// Get the user countrycode by ip
	if(!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
		$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
	} else {
		$ip = $_SERVER['REMOTE_ADDR'];
	}
	$geo = json_decode(file_get_contents('http://www.geoplugin.net/json.gp?ip='.$ip),True);
	$country_codesEU=['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DE', 'DK', 'EE', 'EL', 'ES', 'FI', 'FR', 'GB', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK'];
}

My website has two (external) cookies for which I need consent; the Google Analytics and Google Adsense cookies. As the user will be able to change their preferences, in our cookie we'll store the consent status, Analytic preference and Adsense preference. Based on the user's country code, we'll might have to show consent and set the two external cookies as not-to-load (value 0). Else we can just set the the consent values we want and don't have to bother the user with the consent.

if(!isset($_COOKIE['cookies'])){
	// First time visitor
	...
	if ($geo['geoplugin_countryCode'] == Null || in_array($geo['geoplugin_countryCode'],$country_codesEU)){
		// User is in the EU or we do not know where he is from.
		// Ask for cookies consent, if within 15 minutes the user comes back, they accept
		$show_consent = True;	
		$cookies = ['consent'=>0,'analytic' => 0, 'ads' => 0];
		$cookies_string = json_encode($cookies);
		setcookie("cookies",$cookies_string,time() + (60*15),'/','mountcreo.com');
	}else{
		// The user is not in the EU, so we can set cookies
		$show_consent =  False;
		$cookies = ['consent'=>1,'analytic' => 1, 'ads' => 1];
		$cookies_string = json_encode($cookies);
		setcookie("cookies",$cookies_string,time() + (60*60*24*90),'/','mountcreo.com'); // Set cookie for 90 days
	}
}else{
	// We'll get the user preferences
}

If the user is in the EU, we set the consent value to 0. Next time the user visits the website we can decode the set cookie to get the user's preferences. As said, we consider revisiting the website within 15 minutes as website consent. So, if the consent value is still 0, we will now set the consent and external cookies to 1. We do however add an extra if statement which checks if the current page is the Cookie Policy page (my page is also called cookies). This takes into account that a user's second website loading might be the Cookie Policy page, in which case we want to allow the user to set their preference and not load the external cookies yet.

if(!isset($_COOKIE['cookies'])){
	// First time visitor
	...
}else{
	// We'll get the user preferences
	$show_consent = False; // Don't show the popup	
	$cookies = json_decode($_COOKIE['cookies'],True);

	// If consent == 'asking', the user continued on the website and has accepted
	if($cookies['consent']==0){
		$cookies = ['consent'=>1,'analytic' => 1, 'ads' => 1];
		$cookies_string = json_encode($cookies);
		setcookie("cookies",$cookies_string,time() + (60*60*24*90),'/','mountcreo.com'); // Set cookie for 90 days
		$page = $_GET['page']; // Or whatever you reference your pages with
		if($page == "cookies"){ // user's second visit is the cookies page
			$cookies = ['consent'=>1,'analytic' => 0, 'ads' => 0];
		}
	}	
}

If for some reason we would call $_COOKIES['cookies'] again now, we'll see that we still get the old values. We would first have to refresh the page. Luckily, in the next section we can use our $cookies variable as reference. This one does contain the correct values.

Cookie banner & loading external cookies

By using the values stored in $consent, you can now load whichever external service (and their cookies) accoring to the user's preferences. It's a simple IF-statement. We also know if we still have to ask for consent. For that latter I've decided to show a cookie consent box, instead of a banner at the top/bottom of the page. While we're at it, I added a little HTML/CSS hack which allows us to close the banner without the use of Javascript (although there is no real reason not to use JS). In our body we put following PHP/HTML, which uses the previously set $show_consent value to only load when needed.

<?php if($show_consent == True){ ?>
	<input type="checkbox" id="close_cookie"></input>
	<div id="cookie_consent_popup">
		<h1>Cookies</h1>
		<label for="close_cookie" id="close_cookie_box">X</label>
		<p>Mount CREO uses cookies to store preferences, analyse traffic and provide personalized ads. Read more about the used cookies and disable them on our <a href="cookies" title="Cookie Policy">Cookie page</a>. By clicking 'OK', 'X' or continuing using our site, you consent to the use of cookies unless you disabled them.<p>
		<label for="close_cookie" id="ok_cookie_box">OK</label>
	</div>
<?php }?>

In our pop-up we add a paragraph, which describes which  cookies we're using and why, it also gives a link to our Cookie Policy, saying the user can change his preferences. Then it says how consent is given.

The checkbox and labels are needed for the HTML/CSS closing hack, but it won't be shown by setting the CSS display:none;. We'll use the labels to enable the checkbox and use the :checked state of the checkbox to hide the pop-up. As I'm not using a banner, but a box, I give it a position:fixed; attribute. The two labels are then positioned absolutely inside the pop-up, giving for our CSS:

#close_cookie{display:none;}
	#close_cookie:checked + #cookie_consent_popup{display:none;}	
#cookie_consent_popup{
	position:fixed;
	bottom:30px;left:30px;
	width:400px;
	height:180px;
	background-color:#fbb63e;
	padding:20px;
	 z-index:2;
}
	#cookie_consent_popup h1{
		font-size:1.2em;
	}
		#cookie_consent_popup h1:before{
			content:"";
			padding:0;
		}
	#cookie_consent_popup p{
		font-size:0.7em;
	}
	#cookie_consent_popup #close_cookie_box{
		position:absolute;
		top:20px;right:20px;
		cursor:pointer;
		font-size:1.3em;
	}
	#cookie_consent_popup #ok_cookie_box{
		position:absolute;
		bottom:20px;right:20px;
		cursor:pointer;
		font-size:1.6em;
		padding:10px 20px;
		font-weight:700;
		color:white;
	}

I also added z-index:2; to the pop-up, such that it displays above my other content. The result is shown in the demo.

The Cookie Policy page

The Cookie Policy page should contain the following information:

  • The categories of cookies used and an explanation what the category does.
  • A way to see which preferences are currently set, for which I used checkboxes.
  • The possibility to change the preferences, for which we'll use a form. The required cookies can not be unset.

The previously set $cookies was used correctly load cookies. For the first two visits to the website, this is different from correctly showing the prefences. For example the case a user first visits the website and lands on the cookie page: We do not yet want to load the cookies, but we do want to show the preferences that will be used in his next visit, unless the user changes it. We now have two cases:

  1. For the Cookie Policy page, being it the first or second loaded page for us means we do not have consent yet. In this case $_COOKIES['cookies'] either does not exist (first visit) or has the consent value '0' or '1', depending if the user is from the EU or not. In this case we want to load show our standard values as preferences, being everything enabled.
  2. The consent value is 1, so we want to load the preferences the user has set.

Recall that setcookie() will not change the $_COOKIES untill the page is loaded. We therefor still have access to the old $_COOKIES values. The following code should be put in our body tag, after we loaded our cookie. For simplicity, just call the text inside of our form, giving use following HTML:

<form method="post" action="change-cookies-execute.php">
<!-- Your checkboxes and content here -->
	<?php
	if(!isset($_COOKIE['cookies']) || json_decode($_COOKIE['cookies'])['consent'],True) == 0){
	$cookies = ['consent'=>1,'analytic' => 1, 'ads' => 1];
	}else{
	$cookies = json_decode($_COOKIE['cookies'],True);
	}
	?>
</form>

I'll not show the code for the whole HTML/CSS setup of the Cookie Policy page. For this website, I used flex-boxes to create a layout and used the previously-mentioned checkbox/label hack to create CSS sliding buttons to set the preference. Using the 'analytics' and 'ads' key of $cookies, we know which values to set. Our required cookies can not be unset. The checkboxes are of course part of the form, for which a submit button is added. Recall that setcookie() can only be used before anything is outputted. Therefor we set the action attribute of our form to an external execute page.

All that is left is to add some code to the change-cookies-execute.php page. It's always nice to do a little input-check first. I pass three values (submit, analytic cookie checkbox and ads cookie checkbox), so I check the length or $_POST and check if my analytic and ads value are either 0 or 1:

<?php
if(count($_POST)==3 && in_array($_POST['analytics'],[0,1]) && in_array($_POST['ads'],[0,1])){
	$cookies = ['consent'=>$_POST['required'],'analytic' => $_POST['analytics'], 'ads' => $_POST['ads']];
	$cookies_string = json_encode($cookies);
	setcookie("cookies",$cookies_string,time() + (60*60*24*90),'/','mountcreo.com'); // Set cookie for 90 days
	header("Location:../cookies?saved=1"); exit(); 	
}else{
	header("Location:../cookies?saved=0"); exit(); 
}
?>

I then change the cookie values and bring the user back to our Cookie Policy page. I also add a saved value, so I can tell the user if the preferences were changed succesfully or not. We now have a fully-working, regulation-complaint cookie consent functionality on our website.

Which cookies does my website have?

Now we have our functionality working, it is a good time to check which cookies are loading on our website. The easiest way is to check the cookies in our browser.

For Chrome (2019), paste chrome://settings/siteData in your browser or click the three dots and the top-right and go to settings. Scroll down, click on advanced,  and find Site Settings.  Next, click Cookies and See all cookies and site data. Click on your domain and you'll find the loaded cookies. By deleting 'cookies', you can now easily remove our own cookie and check the website's functionality.