1/* globals JSINFO, DOKU_BASE, DokuCookie */ 2 3/** 4 * Modern Statistics Plugin 5 */ 6class StatisticsPlugin { 7 constructor() { 8 this.data = {}; 9 this.sessionTimeout = 15 * 60 * 1000; // 15 minutes 10 this.uid = this.initializeUserTracking(); 11 this.sessionId = this.getSession(); 12 } 13 14 /** 15 * Initialize the statistics plugin 16 */ 17 async init() { 18 try { 19 this.buildTrackingData(); 20 await this.logPageView(); 21 this.attachEventListeners(); 22 } catch (error) { 23 console.error('Statistics plugin initialization failed:', error); 24 } 25 } 26 27 /** 28 * Initialize user tracking with visitor cookie 29 * @returns {string} User ID (UUID) 30 */ 31 initializeUserTracking() { 32 let uid = DokuCookie.getValue('plgstats'); 33 if (!uid) { 34 uid = this.generateUUID(); 35 DokuCookie.setValue('plgstats', uid); 36 } 37 return uid; 38 } 39 40 41 /** 42 * Build tracking data object 43 */ 44 buildTrackingData() { 45 const now = Date.now(); 46 this.data = { 47 uid: this.uid, 48 ses: this.sessionId, 49 p: JSINFO.id, 50 r: document.referrer, 51 sx: screen.width, 52 sy: screen.height, 53 vx: window.innerWidth, 54 vy: window.innerHeight, 55 js: 1, 56 rnd: now 57 }; 58 } 59 60 /** 61 * Log page view based on action 62 */ 63 async logPageView() { 64 const action = JSINFO.act === 'show' ? 'v' : 's'; 65 await this.logView(action); 66 } 67 68 /** 69 * Attach event listeners for tracking 70 */ 71 attachEventListeners() { 72 // Track external link clicks 73 document.querySelectorAll('a.urlextern').forEach(link => { 74 link.addEventListener('click', this.logExternal.bind(this)); 75 }); 76 77 // Track page unload 78 window.addEventListener('beforeunload', this.logExit.bind(this)); 79 } 80 81 /** 82 * Log a view or session 83 * @param {string} action 'v' = view, 's' = session 84 */ 85 async logView(action) { 86 const params = new URLSearchParams(this.data); 87 const url = `${DOKU_BASE}lib/plugins/statistics/log.php?do=${action}&${params}`; 88 89 try { 90 // Use fetch with keepalive for better reliability 91 await fetch(url, { 92 method: 'GET', 93 keepalive: true, 94 cache: 'no-cache' 95 }); 96 } catch (error) { 97 // Fallback to image beacon for older browsers 98 const img = new Image(); 99 img.src = url; 100 } 101 } 102 103 /** 104 * Log clicks to external URLs 105 * @param {Event} event Click event 106 */ 107 logExternal(event) { 108 const params = new URLSearchParams(this.data); 109 const url = `${DOKU_BASE}lib/plugins/statistics/log.php?do=o&ol=${encodeURIComponent(event.target.href)}&${params}`; 110 111 // Use sendBeacon for reliable tracking 112 if (navigator.sendBeacon) { 113 navigator.sendBeacon(url); 114 } else { 115 // Fallback for older browsers 116 const img = new Image(); 117 img.src = url; 118 } 119 120 return true; 121 } 122 123 /** 124 * Log page exit as session info 125 */ 126 logExit() { 127 const currentSession = this.getSession(); 128 if (currentSession !== this.sessionId) { 129 return; // Session expired, don't log 130 } 131 132 const params = new URLSearchParams(this.data); 133 const url = `${DOKU_BASE}lib/plugins/statistics/log.php?do=s&${params}`; 134 135 if (navigator.sendBeacon) { 136 navigator.sendBeacon(url); 137 } 138 } 139 140 /** 141 * Get current session identifier 142 * Auto clears expired sessions and creates new ones after 15 min idle time 143 * @returns {string} Session ID 144 */ 145 getSession() { 146 const now = Date.now(); 147 148 // Load session cookie 149 let sessionData = DokuCookie.getValue('plgstatsses'); 150 let sessionId = ''; 151 152 if (sessionData) { 153 const [timestamp, id] = sessionData.split('-', 2); 154 if (now - parseInt(timestamp, 10) <= this.sessionTimeout) { 155 sessionId = id; 156 } 157 } 158 159 // Generate new session if needed 160 if (!sessionId) { 161 sessionId = this.generateUUID(); 162 } 163 164 // Update session cookie 165 DokuCookie.setValue('plgstatsses', `${now}-${sessionId}`); 166 return sessionId; 167 } 168 169 /** 170 * Generate a UUID v4 171 * @returns {string} UUID 172 */ 173 generateUUID() { 174 function s4() { 175 return Math.floor((1 + Math.random()) * 0x10000) 176 .toString(16) 177 .substring(1); 178 } 179 180 return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); 181 } 182} 183 184// Initialize when DOM is ready 185if (document.readyState === 'loading') { 186 document.addEventListener('DOMContentLoaded', () => { 187 new StatisticsPlugin().init(); 188 }); 189} else { 190 // DOM already loaded 191 new StatisticsPlugin().init(); 192} 193 194class ChartComponent extends HTMLElement { 195 connectedCallback() { 196 this.renderChart(); 197 } 198 199 renderChart() { 200 const chartType = this.getAttribute('type'); 201 const data = JSON.parse(this.getAttribute('data')); 202 203 console.log('data', data); 204 205 const canvas = document.createElement("canvas"); 206 canvas.id = "myChart"; 207 canvas.style.height = chartType === "pie" ? "200px" : "100%"; 208 canvas.style.width = chartType === "pie" ? "200px" : "100%"; 209 210 this.appendChild(canvas); 211 212 const ctx = canvas.getContext('2d'); 213 214 // basic config 215 const config = { 216 type: chartType, 217 data: data 218 }; 219 220 // percentage labels and tooltips for pie charts 221 if (chartType === "pie") { 222 // chartjs-plugin-datalabels needs to be registered 223 Chart.register(ChartDataLabels); 224 225 config.options = { 226 plugins: { 227 datalabels: { 228 formatter: (value, context) => { 229 const total = context.chart.data.datasets[0].data.reduce((a, b) => a + b, 0); 230 const percentage = ((value / total) * 100).toFixed(2) + '%'; 231 return percentage; 232 }, 233 color: '#fff', 234 } 235 } 236 }; 237 } 238 239 240 new Chart(ctx, config); 241 } 242} 243 244customElements.define('chart-component', ChartComponent); 245