xref: /plugin/statistics/script.js (revision f0a4ccee9985c9b288636fd60e7968a2d5af6789)
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