1 /*
  2  * Copyright 2018 Leo Sutic <leo@sutic​.nu>
  3  * 
  4  * Licensed under the Apache License, Version 2​.0 (the "License");
  5  * you may not use this file except in compliance with the License.
  6  * You may obtain a copy of the License at
  7  * 
  8  *     http://www​.apache​.org​/licenses​/LICENSE-2​.0
  9  * 
 10  * Unless required by applicable law or agreed to in writing, software
 11  * distributed under the License is distributed on an "AS IS" BASIS,
 12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  * See the License for the specific language governing permissions and
 14  * limitations under the License.
 15  */
 16 
 17 /**
 18  * A reference to the cookie consent notice manager.
 19  *
 20  * @namespace A cookie consent notice manager. The user can be in three states:
 21  * no consent, implicit consent and explicit consent. No consent is the state the
 22  * user starts in on first visiting the website. At that point a cookie is set
 23  * that will indicate implicit consent if the user returns. When the user returns,
 24  * they are considered to have given implicit consent. If the user then dismisses
 25  * the cookie notification they move to explicit consent.
 26  *
 27  * <p>The actions to take when the user is in either of the consenting states 
 28  * are determined by functions <code>push</code>ed onto two lists: 
 29  * <code>self​.cookie​_consent​_implicit</code> and <code>self​.cookie​_consent​_explicit</code>.
 30  *
 31  * <p>You therefore start by creating these lists as needed, if they don't exist. For
 32  * example, the following creates the list of actions to take in case of implicit consent:
 33  *
 34  * <pre><code>self​.cookie​_consent​_implicit = self​.cookie​_consent​_implicit || [];</code></pre>
 35  *
 36  * You then push functions onto the lists:
 37  *
 38  * <pre><code>self​.cookie​_consent​_implicit​.push(function () {
 39  *     console​.log("Action taken on implicit consent.");
 40  * })</code></pre>
 41  *
 42  * Finally, you call <code>cookie​_consent​.initialize()</code> to start the manager.
 43  *
 44  * <p><b>Note</b>: explicit consent implies implicit consent. The function above will
 45  * also be called for explicit consent.
 46  *
 47  * <h2>UI Elements</h2>
 48  *
 49  * The notice is assumed to be an initially invisible (<code>display: none</code>) element
 50  * in the page with the id <code>cookie-consent</code>. In it you should put the notice text
 51  * and a button that, when clicked, calls <code>cookie​_consent​.consent()</code>.
 52  *
 53  * @example
 54  * <!-- 
 55  * Load Google Analytics if the user has given implicit consent.
 56  * -->
 57  * <head>
 58  *   <script>
 59  *   function manage​_cookie​_consent() {
 60  *     // Set up what happens in case of implicit consent
 61  *     self​.cookie​_consent​_implicit = self​.cookie​_consent​_implicit || [];
 62  *     self​.cookie​_consent​_implicit​.push(function () {
 63  *       // Implicit consent means we load Google Analytics
 64  *       var script = document​.createElement ("script");
 65  *       script​.async = true;
 66  *       script​.src = "https://www​.googletagmanager​.com​/gtag​/js?id=YOUR​_ID​_HERE";
 67  *       document​.body​.appendChild (script);
 68  *       
 69  *       window​.dataLayer = window​.dataLayer || [];
 70  *       function gtag(){dataLayer​.push(arguments);}
 71  *       gtag('js', new Date());
 72  *              
 73  *       gtag('config', 'YOUR​_ID​_HERE');
 74  *     });
 75  *
 76  *     // Do the cookie detection
 77  *     cookie​_consent​.initialize ();
 78  *   }
 79  *   </script>
 80  *   <script async="async" defer="defer" 
 81  *       src="cookie-consent​.js">/* */</script>
 82  * </head>
 83  * <body onload="manage​_cookie​_consent()">
 84  *   <div id="cookie-consent" style="display:none;">
 85  *     <p>
 86  *       example​.com uses cookies to make 
 87  *       your browsing experience better. By using 
 88  *       the site you agree to our use of cookies. 
 89  *       <button 
 90  *         type="button" 
 91  *         onclick="cookie​_consent​.consent()">
 92  *         OK
 93  *       </button>
 94  *     </p>
 95  *   </div>
 96  *   .
 97  *   .
 98  *   .
 99  * </body>
100  */
101 cookie​_consent = {
102     /**
103      * The ID of the element that shows the cookie consent notice.
104      *
105      * @type String
106      */
107     ELEMENT​_ID : "cookie-consent",
108     
109     /**
110      * The name of the consent cookie, where we store the user's implicit or explicit consent.
111      *
112      * @type String
113      */
114     COOKIE​_NAME : "cookie-consent",
115     
116     /**
117      * Expiration of the consent cookie, in days.
118      *
119      * @type int
120      */
121     COOKIE​_EXPIRATION : 360,
122     
123     /**
124      * Enable implicit consent. This will set a first-party session cookie
125      * on the first page view. On the second page view, the user is considered
126      * to have given implicit consent.
127      *
128      * @type boolean
129      */
130     IMPLICIT​_CONSENT : true,
131     
132     /**
133      * State of the user's consent on page load. Either:
134      * <ul>
135      * <li><code>null</code> - no consent given, implicit or explicit
136      * <li><code>"implicit"</code> - implicit consent given, by continuing to use the
137      * website after being shown the notice.
138      * <li><code>"explicit"</code> - explicit consent given by clicking on the "OK" button
139      * in the notice and dismissing it.
140      * <li><code>"rejected"</code> - consent explicitly rejected (via DNT)
141      * </ul>
142      *
143      * @type String
144      */
145     state : null,
146     
147     /** 
148      * Call to initialize the cookie consent object.
149      */
150     initialize : function () {
151         var element = document​.getElementById(this​.ELEMENT​_ID);
152         var state = this​.getCookie(this​.COOKIE​_NAME);
153         this​.state = state;
154         if (state == null) {
155             element​.style​.display = "block";
156             // First party session cookies are OK
157             if (this​.IMPLICIT​_CONSENT) {
158                 this​.setCookie (this​.COOKIE​_NAME, "implicit", false);
159             }
160         } else if (window​.doNotTrack == "1" || navigator​.doNotTrack == "1") {
161             this​.state = "rejected";
162         } else if (state == "implicit") {
163             element​.style​.display = "block";
164             this​.runQueue ("cookie​_consent​_implicit");
165         } else {
166             this​.runQueue ("cookie​_consent​_implicit");
167             this​.runQueue ("cookie​_consent​_explicit");
168         }
169     },
170     
171     /** 
172      * Utility function to run one of the implicit or explicit event handlers.
173      * 
174      * @param {String} name of the event handler.
175      * @private
176      */
177     runQueue : function (name) {
178         var q = self[name];
179         if (q != null && q​.cookie​_consent​_instant​_queue != true) {
180             for (var i = 0; i < q​.length; ++i) {
181                 q[i]();
182             }
183             self[name] = {
184                 cookie​_consent​_instant​_queue : true,
185                 push : function (f) {
186                     f();
187                 }
188             };
189         }
190     },
191     
192     /**
193      * Give explicit consent. This should be called from the <code>onclick</code> event
194      * handler in the "OK" button in the cookie consent notice.
195      */
196     consent : function () {
197         document​.getElementById(this​.ELEMENT​_ID).style​.display = "none";
198         
199         var state = this​.getCookie(this​.COOKIE​_NAME);
200         
201         this​.setCookie (this​.COOKIE​_NAME, "explicit", true);
202         
203         if (state == null || state == "implicit") {
204             this​.runQueue ("cookie​_consent​_implicit");
205         }
206         this​.runQueue ("cookie​_consent​_explicit");
207         window​.dispatchEvent(new Event('resize'));
208     },
209     
210     /**
211      * Retracts consent and optionally reloads the current document.
212      *
213      * @param {boolean} reload reload the current document
214      */
215     retractConsent : function (reload) {
216         this​.clearCookie (this​.COOKIE​_NAME);
217         if (reload) {
218             location​.reload ();
219         }
220     },
221     
222     /**
223      * Retrieves a cookie.
224      *
225      * @private
226      * @param {String} key the cookie name
227      * @type String
228      * @returns the cookie value, if it exists, <code>null</code> otherwise.
229      */
230     getCookie : function (key) {
231         var cookies = document​.cookie​.split (";");
232         for (var i = 0; i < cookies​.length; ++i) {
233             var kv = cookies[i].split("=");
234             kv[0] = kv[0].replace(/^\s*/, "").replace(/\s*$/, "");
235             if (kv[0] == key) {
236                 return kv[1].replace(/^\s*/, "").replace(/\s*$/, "");
237             }
238         }
239         return null;
240     },
241     
242     /**
243      * Sets a cookie.
244      *
245      * @private
246      * @param {String} key the cookie name
247      * @param {String} value the cookie value
248      */
249     setCookie : function (key, value, persist) {
250         var d = new Date();
251         d​.setTime (d​.getTime() + (this​.COOKIE​_EXPIRATION * 24 * 60 * 60 * 1000));
252         var expires = persist ? ";expires=" + d​.toUTCString () : "";
253         
254         document​.cookie = key + "=" + value + expires + ";path=/";
255     },
256     
257     /**
258      * Sets a cookie.
259      *
260      * @private
261      * @param {String} key the cookie name
262      * @param {String} value the cookie value
263      */
264     clearCookie : function (key) {
265         var d = new Date(0);
266         var expires = ";expires=" + d​.toUTCString ();
267         
268         document​.cookie = key + "=" + expires + ";path=/";
269     }
270 };
271 
272