1// GoatCounter: https://www.goatcounter.com 2// This file is released under the ISC license: https://opensource.org/licenses/ISC 3;(function() { 4 'use strict'; 5 6 window.goatcounter = window.goatcounter || {} 7 8 // Load settings from data-goatcounter-settings. 9 var s = document.querySelector('script[data-goatcounter]') 10 if (s && s.dataset.goatcounterSettings) { 11 try { var set = JSON.parse(s.dataset.goatcounterSettings) } 12 catch (err) { console.error('invalid JSON in data-goatcounter-settings: ' + err) } 13 for (var k in set) 14 if (['no_onload', 'no_events', 'allow_local', 'allow_frame', 'path', 'title', 'referrer', 'event'].indexOf(k) > -1) 15 window.goatcounter[k] = set[k] 16 } 17 18 var enc = encodeURIComponent 19 20 // Get all data we're going to send off to the counter endpoint. 21 window.goatcounter.get_data = function(vars) { 22 vars = vars || {} 23 var data = { 24 p: (vars.path === undefined ? goatcounter.path : vars.path), 25 r: (vars.referrer === undefined ? goatcounter.referrer : vars.referrer), 26 t: (vars.title === undefined ? goatcounter.title : vars.title), 27 e: !!(vars.event || goatcounter.event), 28 s: window.screen.width, 29 b: is_bot(), 30 q: location.search, 31 } 32 33 var rcb, pcb, tcb // Save callbacks to apply later. 34 if (typeof(data.r) === 'function') rcb = data.r 35 if (typeof(data.t) === 'function') tcb = data.t 36 if (typeof(data.p) === 'function') pcb = data.p 37 38 if (is_empty(data.r)) data.r = document.referrer 39 if (is_empty(data.t)) data.t = document.title 40 if (is_empty(data.p)) data.p = get_path() 41
42 if (rcb) data.r = rcb(data.r) 43 if (tcb) data.t = tcb(data.t) 44 if (pcb) data.p = pcb(data.p) 45 return data 46 } 47 48 // Check if a value is "empty" for the purpose of get_data(). 49 var is_empty = function(v) { return v === null || v === undefined || typeof(v) === 'function' } 50 51 // See if this looks like a bot; there is some additional filtering on the 52 // backend, but these properties can't be fetched from there. 53 var is_bot = function() { 54 // Headless browsers are probably a bot. 55 var w = window, d = document 56 if (w.callPhantom || w._phantom || w.phantom) 57 return 150 58 if (w.__nightmare) 59 return 151 60 if (d.__selenium_unwrapped || d.__webdriver_evaluate || d.__driver_evaluate) 61 return 152 62 if (navigator.webdriver) 63 return 153 64 return 0 65 } 66 67 // Object to urlencoded string, starting with a ?. 68 var urlencode = function(obj) { 69 var p = [] 70 for (var k in obj) 71 if (obj[k] !== '' && obj[k] !== null && obj[k] !== undefined && obj[k] !== false) 72 p.push(enc(k) + '=' + enc(obj[k])) 73 return '?' + p.join('&') 74 } 75 76 // Show a warning in the console. 77 var warn = function(msg) { 78 if (console && 'warn' in console) 79 console.warn('goatcounter: ' + msg) 80 } 81 82 // Get the endpoint to send requests to. 83 var get_endpoint = function() { 84 var s = document.querySelector('script[data-goatcounter]') 85 return (s && s.dataset.goatcounter) ? s.dataset.goatcounter : goatcounter.endpoint 86 } 87 88 // Get current path. 89 var get_path = function() { 90 var loc = location, 91 c = document.querySelector('link[rel="canonical"][href]') 92 if (c) { // May be relative or point to different domain. 93 var a = document.createElement('a') 94 a.href = c.href 95 if (a.hostname.replace(/^www\./, '') === location.hostname.replace(/^www\./, '')) 96 loc = a 97 } 98 return (loc.pathname + loc.search) || '/' 99 } 100 101 // Run function after DOM is loaded. 102 var on_load = function(f) { 103 if (document.body === null) 104 document.addEventListener('DOMContentLoaded', function() { f() }, false) 105 else 106 f() 107 } 108 109 // Filter some requests that we (probably) don't want to count. 110 window.goatcounter.filter = function() { 111 if ('visibilityState' in document && document.visibilityState === 'prerender') 112 return 'visibilityState' 113 if (!goatcounter.allow_frame && location !== parent.location) 114 return 'frame' 115 if (!goatcounter.allow_local && location.hostname.match(/(localhost$|^127\.|^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\.|^0\.0\.0\.0$)/)) 116 return 'localhost' 117 if (!goatcounter.allow_local && location.protocol === 'file:') 118 return 'localfile' 119 if (localStorage && localStorage.getItem('skipgc') === 't') 120 return 'disabled with #toggle-goatcounter' 121 return false 122 } 123 124 // Get URL to send to GoatCounter. 125 window.goatcounter.url = function(vars) { 126 var data = window.goatcounter.get_data(vars || {}) 127 if (data.p === null) // null from user callback. 128 return 129 data.rnd = Math.random().toString(36).substr(2, 5) // Browsers don't always listen to Cache-Control. 130 131 var endpoint = get_endpoint() 132 if (!endpoint) 133 return warn('no endpoint found') 134 135 return endpoint + urlencode(data) 136 } 137 138 // Count a hit. 139 window.goatcounter.count = function(vars) { 140 var f = goatcounter.filter() 141 if (f) 142 return warn('not counting because of: ' + f) 143 var url = goatcounter.url(vars) 144 if (!url) 145 return warn('not counting because path callback returned null') 146 147 if (!navigator.sendBeacon(url)) { 148 // This mostly fails due to being blocked by CSP; try again with an 149 // image-based fallback. 150 var img = document.createElement('img') 151 img.src = url 152 img.style.position = 'absolute' // Affect layout less. 153 img.style.bottom = '0px' 154 img.style.width = '1px' 155 img.style.height = '1px' 156 img.loading = 'eager' 157 img.setAttribute('alt', '') 158 img.setAttribute('aria-hidden', 'true') 159 160 var rm = function() { if (img && img.parentNode) img.parentNode.removeChild(img) } 161 img.addEventListener('load', rm, false) 162 document.body.appendChild(img) 163 } 164 } 165 166 // Get a query parameter. 167 window.goatcounter.get_query = function(name) { 168 var s = location.search.substr(1).split('&') 169 for (var i = 0; i < s.length; i++) 170 if (s[i].toLowerCase().indexOf(name.toLowerCase() + '=') === 0) 171 return s[i].substr(name.length + 1) 172 } 173 174 // Track click events. 175 window.goatcounter.bind_events = function() { 176 if (!document.querySelectorAll) // Just in case someone uses an ancient browser. 177 return 178 179 var send = function(elem) { 180 return function() { 181 goatcounter.count({ 182 event: true, 183 path: (elem.dataset.goatcounterClick || elem.name || elem.id || ''), 184 title: (elem.dataset.goatcounterTitle || elem.title || (elem.innerHTML || '').substr(0, 200) || ''), 185 referrer: (elem.dataset.goatcounterReferrer || elem.dataset.goatcounterReferral || ''), 186 }) 187 } 188 } 189 190 Array.prototype.slice.call(document.querySelectorAll("*[data-goatcounter-click]")).forEac
190h(function(elem) { 191 if (elem.dataset.goatcounterBound) 192 return 193 var f = send(elem) 194 elem.addEventListener('click', f, false) 195 elem.addEventListener('auxclick', f, false) // Middle click. 196 elem.dataset.goatcounterBound = 'true' 197 }) 198 } 199 200 // Add a "visitor counter" frame or image. 201 window.goatcounter.visit_count = function(opt) { 202 on_load(function() { 203 opt = opt || {} 204 opt.type = opt.type || 'html' 205 opt.append = opt.append || 'body' 206 opt.path = opt.path || get_path() 207 opt.attr = opt.attr || {width: '200', height: (opt.no_branding ? '60' : '80')} 208 209 opt.attr['src'] = get_endpoint() + 'er/' + enc(opt.path) + '.' + enc(opt.type) + '?' 210 if (opt.no_branding) opt.attr['src'] += '&no_branding=1' 211 if (opt.style) opt.attr['src'] += '&style=' + enc(opt.style) 212 if (opt.start) opt.attr['src'] += '&start=' + enc(opt.start) 213 if (opt.end) opt.attr['src'] += '&end=' + enc(opt.end) 214 215 var tag = {png: 'img', svg: 'img', html: 'iframe'}[opt.type] 216 if (!tag) 217 return warn('visit_count: unknown type: ' + opt.type) 218 219 if (opt.type === 'html') { 220 opt.attr['frameborder'] = '0' 221 opt.attr['scrolling'] = 'no' 222 } 223 224 var d = document.createElement(tag) 225 for (var k in opt.attr) 226 d.setAttribute(k, opt.attr[k]) 227 228 var p = document.querySelector(opt.append) 229 if (!p) 230 return warn('visit_count: element to append to not found: ' + opt.append) 231 p.appendChild(d) 232 }) 233 } 234 235 // Make it easy to skip your own views. 236 if (location.hash === '#toggle-goatcounter') { 237 if (localStorage.getItem('skipgc') === 't') { 238 localStorage.removeItem('skipgc', 't') 239 alert('GoatCounter tracking is now ENABLED in this browser.') 240 } 241 else { 242 localStorage.setItem('skipgc', 't') 243 alert('GoatCounter tracking is now DISABLED in this browser until ' + location + ' is loaded again.') 244 } 245 } 246 247 if (!goatcounter.no_onload) 248 on_load(function() { 249 // 1. Page is visible, count request. 250 // 2. Page is not yet visible; wait until it switches to 'visible' and count. 251 // See #487 252 if (!('visibilityState' in document) || document.visibilityState === 'visible') 253 goatcounter.count() 254 else { 255 var f = function(e) { 256 if (document.visibilityState !== 'visible') 257 return 258 document.removeEventListener('visibilitychange', f) 259 goatcounter.count() 260 } 261 document.addEventListener('visibilitychange', f) 262 } 263 264 if (!goatcounter.no_events) 265 goatcounter.bind_events() 266 }) 267})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.