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