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}