1<?php
2/**
3 * DokuWiki Plugin pagestats (Helper Component)
4 * Common functionality for page stats calculation
5 */
6
7if (!defined('DOKU_INC')) die();
8
9class helper_plugin_pagestats extends DokuWiki_Plugin {
10    private $cache = null;
11    private $cacheTime = 3600; // Cache standardmäßig für 1 Stunde
12
13    /**
14     * Constructor - reads configuration
15     */
16    public function __construct() {
17        // Get cache lifetime from configuration (if set)
18        $cacheTime = $this->getConf('cacheTime');
19        if (is_numeric($cacheTime)) {
20            $this->cacheTime = (int)$cacheTime;
21        }
22
23        // Wenn die Cache-Zeit geändert wurde, Cache löschen
24        $optionChanged = false;
25        if (isset($_POST['config']['plugin']['pagestats']['cacheTime']) &&
26            $_POST['config']['plugin']['pagestats']['cacheTime'] != $this->cacheTime) {
27            $optionChanged = true;
28        }
29
30        // Wenn Namensräume ausschließen geändert wurde, Cache löschen
31        if (isset($_POST['config']['plugin']['pagestats']['excludeNamespaces'])) {
32            $optionChanged = true;
33        }
34
35        if ($optionChanged) {
36            $this->clearCache();
37        }
38    }
39
40    /**
41     * Calculate all stats at once to avoid multiple directory scans
42     *
43     * @return array Associative array with all statistics
44     */
45    public function getStats() {
46        // Check if we have cached values
47        $cache = $this->loadCache();
48        if ($cache !== null) {
49            return $cache;
50        }
51
52        // Calculate all stats
53        try {
54            $dataPathPages = DOKU_INC . 'data/pages';
55            $dataPathMedia = DOKU_INC . 'data/media';
56
57            $excludeNamespaces = array_map('trim', explode(',', $this->getConf('excludeNamespaces')));
58
59            $stats = [
60                'PAGESTATSPAGE' => 0,
61                'PAGESTATSMB' => 0,
62                'MEDIASTATSPAGE' => 0,
63                'MEDIASTATSMB' => 0
64            ];
65
66            // Pages stats
67            list($count, $size) = $this->calculateStats($dataPathPages, 'txt', $excludeNamespaces);
68            $stats['PAGESTATSPAGE'] = $count;
69            $stats['PAGESTATSMB'] = round($size / (1024 * 1024), 2);
70
71            // Media stats
72            list($count, $size) = $this->calculateStats($dataPathMedia, '', $excludeNamespaces);
73            $stats['MEDIASTATSPAGE'] = $count;
74            $stats['MEDIASTATSMB'] = round($size / (1024 * 1024), 2);
75
76            // Cache the results
77            $this->saveCache($stats);
78
79            return $stats;
80        } catch (Exception $e) {
81            $this->log('pagestats', 'Error calculating stats: '.$e->getMessage(), DOKU_INC.'pagestats_error.log');
82            return [
83                'PAGESTATSPAGE' => 0,
84                'PAGESTATSMB' => 0,
85                'MEDIASTATSPAGE' => 0,
86                'MEDIASTATSMB' => 0
87            ];
88        }
89    }
90
91    /**
92     * Calculate both count and size in one iteration
93     *
94     * @param string $path Directory path
95     * @param string $extension File extension to filter, empty for all
96     * @param array $excludeNamespaces Namespaces to exclude
97     * @return array [count, size]
98     */
99    private function calculateStats($path, $extension, $excludeNamespaces = []) {
100        if (!is_dir($path)) return [0, 0];
101
102        $count = 0;
103        $totalSize = 0;
104
105        try {
106            $iterator = new RecursiveIteratorIterator(
107                new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS)
108            );
109
110            foreach ($iterator as $file) {
111                // Skip if not a file or doesn't match extension
112                if (!$file->isFile() || ($extension !== '' && $file->getExtension() !== $extension)) {
113                    continue;
114                }
115
116                // Skip if in excluded namespace
117                $relativePath = str_replace($path, '', $file->getPathname());
118                $shouldExclude = false;
119
120                foreach ($excludeNamespaces as $ns) {
121                    if (empty($ns)) continue;
122                    if (strpos($relativePath, '/'.$ns.'/') !== false) {
123                        $shouldExclude = true;
124                        break;
125                    }
126                }
127
128                if ($shouldExclude) continue;
129
130                // Count and add size
131                $count++;
132                $totalSize += $file->getSize();
133            }
134        } catch (Exception $e) {
135            // Log error but continue with what we have
136            $this->log('pagestats', 'Error scanning directory: '.$e->getMessage(), DOKU_INC.'pagestats_error.log');
137        }
138
139        return [$count, $totalSize];
140    }
141
142    /**
143     * Save stats to cache
144     *
145     * @param array $stats The stats to cache
146     */
147    private function saveCache($stats) {
148        if ($this->cacheTime <= 0) return; // Caching disabled
149
150        $cacheFile = $this->getCacheFilename();
151        $data = [
152            'time' => time(),
153            'stats' => $stats
154        ];
155
156        io_saveFile($cacheFile, serialize($data));
157    }
158
159    /**
160     * Load stats from cache if available and not expired
161     *
162     * @return array|null Stats or null if cache invalid/expired
163     */
164    private function loadCache() {
165        if ($this->cacheTime <= 0) return null; // Caching disabled
166        if ($this->cache !== null) return $this->cache; // Already loaded
167
168        $cacheFile = $this->getCacheFilename();
169
170        if (!file_exists($cacheFile)) return null;
171
172        $data = unserialize(io_readFile($cacheFile, false));
173
174        if (!$data || !isset($data['time']) || !isset($data['stats'])) {
175            return null;
176        }
177
178        // Check if cache expired
179        if (time() - $data['time'] > $this->cacheTime) {
180            return null;
181        }
182
183        $this->cache = $data['stats'];
184        return $this->cache;
185    }
186
187    /**
188     * Get the cache filename
189     *
190     * @return string Full path to cache file
191     */
192    private function getCacheFilename() {
193        return DOKU_INC . 'data/cache/pagestats.cache';
194    }
195
196    /**
197     * Simple logging function
198     *
199     * @param string $plugin Plugin name
200     * @param string $message Log message
201     * @param string $file Log file
202     */
203    private function log($plugin, $message, $file) {
204        $time = date('Y-m-d H:i:s');
205        $logline = "[$time] [$plugin] $message\n";
206        io_saveFile($file, $logline, true);
207    }
208
209    /**
210     * Clear the cache (e.g. if called from admin)
211     */
212    public function clearCache() {
213        $cacheFile = $this->getCacheFilename();
214        if (file_exists($cacheFile)) {
215            @unlink($cacheFile);
216        }
217        $this->cache = null;
218    }
219}