xref: /plugin/fuzzysearch/action.php (revision 66fcc8eae76a77d88286dc2434b78514d875fb8c)
1*66fcc8eaSedwardlab<?php
2*66fcc8eaSedwardlabif (!defined('DOKU_INC')) die();
3*66fcc8eaSedwardlab
4*66fcc8eaSedwardlabclass action_plugin_fuzzysearch extends DokuWiki_Action_Plugin {
5*66fcc8eaSedwardlab    private function getCacheFile() {
6*66fcc8eaSedwardlab        $user = $this->getCurrentUser();
7*66fcc8eaSedwardlab        if (!$user) return null;
8*66fcc8eaSedwardlab        $userHash = md5($user);
9*66fcc8eaSedwardlab        return DOKU_INC . 'data/cache/fuzzysearch_pages_' . $userHash . '.json';
10*66fcc8eaSedwardlab    }
11*66fcc8eaSedwardlab
12*66fcc8eaSedwardlab    private function getCacheMetaFile() {
13*66fcc8eaSedwardlab        $user = $this->getCurrentUser();
14*66fcc8eaSedwardlab        if (!$user) return null;
15*66fcc8eaSedwardlab        $userHash = md5($user);
16*66fcc8eaSedwardlab        return DOKU_INC . 'data/cache/fuzzysearch_pages_' . $userHash . '.meta.json';
17*66fcc8eaSedwardlab    }
18*66fcc8eaSedwardlab
19*66fcc8eaSedwardlab    public function register(Doku_Event_Handler $controller) {
20*66fcc8eaSedwardlab        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax_call');
21*66fcc8eaSedwardlab        $controller->register_hook('INDEXER_VERSION_GET', 'BEFORE', $this, 'update_cache_on_change');
22*66fcc8eaSedwardlab        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'load_scripts');
23*66fcc8eaSedwardlab    }
24*66fcc8eaSedwardlab
25*66fcc8eaSedwardlab    public function handle_ajax_call(Doku_Event &$event, $param) {
26*66fcc8eaSedwardlab        if ($event->data === 'fuzzysearch_pages') {
27*66fcc8eaSedwardlab            $event->preventDefault();
28*66fcc8eaSedwardlab            $event->stopPropagation();
29*66fcc8eaSedwardlab
30*66fcc8eaSedwardlab            if (!$this->isLoggedIn()) {
31*66fcc8eaSedwardlab                $this->redirectToLogin();
32*66fcc8eaSedwardlab                exit;
33*66fcc8eaSedwardlab            }
34*66fcc8eaSedwardlab
35*66fcc8eaSedwardlab            $cacheFile = $this->getCacheFile();
36*66fcc8eaSedwardlab            $this->ensureCacheExists();
37*66fcc8eaSedwardlab
38*66fcc8eaSedwardlab            if (file_exists($cacheFile)) {
39*66fcc8eaSedwardlab                header('Content-Type: application/json');
40*66fcc8eaSedwardlab                readfile($cacheFile);
41*66fcc8eaSedwardlab            } else {
42*66fcc8eaSedwardlab                header('HTTP/1.1 500 Internal Server Error');
43*66fcc8eaSedwardlab                echo json_encode(['error' => 'Cache generation failed']);
44*66fcc8eaSedwardlab            }
45*66fcc8eaSedwardlab            exit;
46*66fcc8eaSedwardlab        }
47*66fcc8eaSedwardlab    }
48*66fcc8eaSedwardlab
49*66fcc8eaSedwardlab    public function update_cache_on_change(Doku_Event &$event, $param) {
50*66fcc8eaSedwardlab        if ($this->isLoggedIn()) {
51*66fcc8eaSedwardlab            $this->ensureCacheExists(true);
52*66fcc8eaSedwardlab        }
53*66fcc8eaSedwardlab    }
54*66fcc8eaSedwardlab
55*66fcc8eaSedwardlab    public function load_scripts(Doku_Event &$event, $param) {
56*66fcc8eaSedwardlab        error_log('FuzzySearch: Loading scripts');
57*66fcc8eaSedwardlab        // Load Fuse.js
58*66fcc8eaSedwardlab        $fuseSrc = DOKU_BASE . 'lib/plugins/fuzzysearch/fuse.min.js';
59*66fcc8eaSedwardlab        if (!in_array($fuseSrc, array_column($event->data['script'], 'src'))) {
60*66fcc8eaSedwardlab            $event->data['script'][] = [
61*66fcc8eaSedwardlab                'type' => 'text/javascript',
62*66fcc8eaSedwardlab                'src' => $fuseSrc,
63*66fcc8eaSedwardlab                '_data' => '',
64*66fcc8eaSedwardlab                'defer' => 'defer'
65*66fcc8eaSedwardlab            ];
66*66fcc8eaSedwardlab            error_log('FuzzySearch: Fuse.js added');
67*66fcc8eaSedwardlab        }
68*66fcc8eaSedwardlab
69*66fcc8eaSedwardlab        // Load search bar script
70*66fcc8eaSedwardlab        $scriptSrc = DOKU_BASE . 'lib/plugins/fuzzysearch/script.js';
71*66fcc8eaSedwardlab        if (!in_array($scriptSrc, array_column($event->data['script'], 'src'))) {
72*66fcc8eaSedwardlab            $event->data['script'][] = [
73*66fcc8eaSedwardlab                'type' => 'text/javascript',
74*66fcc8eaSedwardlab                'src' => $scriptSrc,
75*66fcc8eaSedwardlab                '_data' => '',
76*66fcc8eaSedwardlab                'defer' => 'defer'
77*66fcc8eaSedwardlab            ];
78*66fcc8eaSedwardlab            error_log('FuzzySearch: script.js added');
79*66fcc8eaSedwardlab        }
80*66fcc8eaSedwardlab
81*66fcc8eaSedwardlab        // Load editor enhancement script
82*66fcc8eaSedwardlab        $editorSrc = DOKU_BASE . 'lib/plugins/fuzzysearch/editor.js';
83*66fcc8eaSedwardlab        if (!in_array($editorSrc, array_column($event->data['script'], 'src'))) {
84*66fcc8eaSedwardlab            $event->data['script'][] = [
85*66fcc8eaSedwardlab                'type' => 'text/javascript',
86*66fcc8eaSedwardlab                'src' => $editorSrc,
87*66fcc8eaSedwardlab                '_data' => '',
88*66fcc8eaSedwardlab                'defer' => 'defer'
89*66fcc8eaSedwardlab            ];
90*66fcc8eaSedwardlab            error_log('FuzzySearch: editor.js added');
91*66fcc8eaSedwardlab        }
92*66fcc8eaSedwardlab    }
93*66fcc8eaSedwardlab
94*66fcc8eaSedwardlab    private function ensureCacheExists($forceUpdate = false) {
95*66fcc8eaSedwardlab        $cacheFile = $this->getCacheFile();
96*66fcc8eaSedwardlab        $cacheMetaFile = $this->getCacheMetaFile();
97*66fcc8eaSedwardlab        if (!$cacheFile || !$cacheMetaFile) return;
98*66fcc8eaSedwardlab
99*66fcc8eaSedwardlab        $meta = $this->loadCacheMeta($cacheMetaFile);
100*66fcc8eaSedwardlab        $lastModified = $this->getLastPageModificationTime();
101*66fcc8eaSedwardlab
102*66fcc8eaSedwardlab        if (!$forceUpdate && file_exists($cacheFile) && isset($meta['last_updated']) && $meta['last_updated'] >= $lastModified) {
103*66fcc8eaSedwardlab            return;
104*66fcc8eaSedwardlab        }
105*66fcc8eaSedwardlab
106*66fcc8eaSedwardlab        $pages = $this->generatePageList();
107*66fcc8eaSedwardlab        $jsonData = json_encode($pages);
108*66fcc8eaSedwardlab        $metaData = ['last_updated' => time()];
109*66fcc8eaSedwardlab
110*66fcc8eaSedwardlab        if (!is_dir(dirname($cacheFile))) {
111*66fcc8eaSedwardlab            mkdir(dirname($cacheFile), 0755, true);
112*66fcc8eaSedwardlab        }
113*66fcc8eaSedwardlab
114*66fcc8eaSedwardlab        file_put_contents($cacheFile, $jsonData);
115*66fcc8eaSedwardlab        file_put_contents($cacheMetaFile, json_encode($metaData));
116*66fcc8eaSedwardlab    }
117*66fcc8eaSedwardlab
118*66fcc8eaSedwardlab    private function generatePageList() {
119*66fcc8eaSedwardlab        if (!$this->isLoggedIn()) {
120*66fcc8eaSedwardlab            $this->redirectToLogin();
121*66fcc8eaSedwardlab            exit;
122*66fcc8eaSedwardlab        }
123*66fcc8eaSedwardlab
124*66fcc8eaSedwardlab        $dir = DOKU_INC . 'data/pages/';
125*66fcc8eaSedwardlab        $page_list = $this->getPageList($dir);
126*66fcc8eaSedwardlab        $pages = [];
127*66fcc8eaSedwardlab        foreach ($page_list as $file) {
128*66fcc8eaSedwardlab            $id = pathID($file);
129*66fcc8eaSedwardlab            if (auth_quickaclcheck($id) >= AUTH_READ) {
130*66fcc8eaSedwardlab                $title = p_get_first_heading($id) ?: noNS($id);
131*66fcc8eaSedwardlab                $pages[] = ['id' => $id, 'title' => $title];
132*66fcc8eaSedwardlab            }
133*66fcc8eaSedwardlab        }
134*66fcc8eaSedwardlab        return $pages;
135*66fcc8eaSedwardlab    }
136*66fcc8eaSedwardlab
137*66fcc8eaSedwardlab    private function getPageList($dir, $base = '') {
138*66fcc8eaSedwardlab        $files = [];
139*66fcc8eaSedwardlab        $items = dir($dir);
140*66fcc8eaSedwardlab        while (false !== ($entry = $items->read())) {
141*66fcc8eaSedwardlab            if ($entry === '.' || $entry === '..') continue;
142*66fcc8eaSedwardlab            $path = $dir . $entry;
143*66fcc8eaSedwardlab            if (is_dir($path) && auth_quickaclcheck($base . $entry . ':') >= AUTH_READ) {
144*66fcc8eaSedwardlab                $files = array_merge($files, $this->getPageList($path . '/', $base . $entry . ':'));
145*66fcc8eaSedwardlab            } elseif (preg_match('/\.txt$/', $entry)) {
146*66fcc8eaSedwardlab                $files[] = $base . substr($entry, 0, -4);
147*66fcc8eaSedwardlab            }
148*66fcc8eaSedwardlab        }
149*66fcc8eaSedwardlab        $items->close();
150*66fcc8eaSedwardlab        return $files;
151*66fcc8eaSedwardlab    }
152*66fcc8eaSedwardlab
153*66fcc8eaSedwardlab    private function getLastPageModificationTime() {
154*66fcc8eaSedwardlab        $dir = DOKU_INC . 'data/pages/';
155*66fcc8eaSedwardlab        $latest = 0;
156*66fcc8eaSedwardlab        $this->scanDirForLatest($dir, $latest);
157*66fcc8eaSedwardlab        return $latest;
158*66fcc8eaSedwardlab    }
159*66fcc8eaSedwardlab
160*66fcc8eaSedwardlab    private function scanDirForLatest($dir, &$latest) {
161*66fcc8eaSedwardlab        $items = dir($dir);
162*66fcc8eaSedwardlab        while (false !== ($entry = $items->read())) {
163*66fcc8eaSedwardlab            if ($entry === '.' || $entry === '..') continue;
164*66fcc8eaSedwardlab            $path = $dir . $entry;
165*66fcc8eaSedwardlab            if (is_dir($path)) {
166*66fcc8eaSedwardlab                $this->scanDirForLatest($path . '/', $latest);
167*66fcc8eaSedwardlab            } elseif (preg_match('/\.txt$/', $entry)) {
168*66fcc8eaSedwardlab                $mtime = filemtime($path);
169*66fcc8eaSedwardlab                if ($mtime > $latest) $latest = $mtime;
170*66fcc8eaSedwardlab            }
171*66fcc8eaSedwardlab        }
172*66fcc8eaSedwardlab        $items->close();
173*66fcc8eaSedwardlab    }
174*66fcc8eaSedwardlab
175*66fcc8eaSedwardlab    private function loadCacheMeta($file) {
176*66fcc8eaSedwardlab        if (file_exists($file)) {
177*66fcc8eaSedwardlab            return json_decode(file_get_contents($file), true) ?: [];
178*66fcc8eaSedwardlab        }
179*66fcc8eaSedwardlab        return [];
180*66fcc8eaSedwardlab    }
181*66fcc8eaSedwardlab
182*66fcc8eaSedwardlab    private function isLoggedIn() {
183*66fcc8eaSedwardlab        return !empty($_SERVER['REMOTE_USER']);
184*66fcc8eaSedwardlab    }
185*66fcc8eaSedwardlab
186*66fcc8eaSedwardlab    private function getCurrentUser() {
187*66fcc8eaSedwardlab        return $_SERVER['REMOTE_USER'] ?? null;
188*66fcc8eaSedwardlab    }
189*66fcc8eaSedwardlab
190*66fcc8eaSedwardlab    private function redirectToLogin() {
191*66fcc8eaSedwardlab        global $ID;
192*66fcc8eaSedwardlab        $loginUrl = wl('', ['do' => 'login', 'id' => $ID], true, '&');
193*66fcc8eaSedwardlab        header("Location: $loginUrl");
194*66fcc8eaSedwardlab    }
195*66fcc8eaSedwardlab}