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