1<?php 2/** 3 * DokuWiki Plugin elasticsearch (Action Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Kieback&Peter IT <it-support@kieback-peter.de> 7 * @author Andreas Gohr <gohr@cosmocode.de> 8 */ 9 10// must be run within Dokuwiki 11if(!defined('DOKU_INC')) die(); 12 13class action_plugin_elasticsearch_indexing extends DokuWiki_Action_Plugin { 14 15 /** 16 * Registers a callback function for a given event 17 * 18 * @param Doku_Event_Handler $controller DokuWiki's event controller object 19 * @return void 20 */ 21 public function register(Doku_Event_Handler $controller) { 22 $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'handle_tpl_content_display'); 23 $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'handle_delete'); 24 } 25 26 /** 27 * Add pages to index 28 * 29 * @param Doku_Event $event event object by reference 30 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 31 * handler was registered] 32 * @return void 33 */ 34 public function handle_tpl_content_display(Doku_Event &$event, $param) { 35 global $ID, $INFO; 36 $logs = array(); 37 $logs[] = 'BEGIN content display'; 38 $logs[] = metaFN($ID, '.elasticsearch_indexed'); 39 $logs[] = wikiFN($ID); 40 $logs[] = wikiFN($INFO['id']); 41 $logs[] = $this->needs_indexing($ID) ? 'needs indexing' : 'no indexing needed'; 42 $logs[] = 'END content display'; 43 $this->log($logs); 44 if($this->needs_indexing($ID)) { 45 $this->index_page($ID); 46 } 47 } 48 49 /** 50 * Remove pages from index 51 * 52 * @param Doku_Event $event event object by reference 53 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 54 * handler was registered] 55 * @return void 56 */ 57 public function handle_delete(Doku_Event &$event, $param) { 58 if($event->data[3]) return; // is old revision stuff 59 if(!empty($event->data[0][1])) return; // page still exists 60 // still here? delete from index 61 $this->delete_page($event->data[2]); 62 } 63 64 /** 65 * Check if the page $id has changed since the last indexing. 66 * 67 * @param string $id 68 * @return boolean 69 */ 70 private function needs_indexing($id) { 71 $indexStateFile = metaFN($id, '.elasticsearch_indexed'); 72 $dataFile = wikiFN($id); 73 74 // no data file -> no indexing 75 if(!file_exists($dataFile)) { 76 // page does not exist but has a state file, try to remove from index 77 if(file_exists($indexStateFile)) { 78 $this->delete_page($id); 79 } 80 return false; 81 } 82 83 // force indexing if we're called via cli (e.g. cron) 84 if(php_sapi_name() == 'cli') { 85 return true; 86 } 87 // check if latest indexing attempt is done after page update 88 if(file_exists($indexStateFile)) { 89 if(filemtime($indexStateFile) > filemtime($dataFile)) { 90 return false; 91 } 92 } 93 return true; 94 } 95 96 /** 97 * Save indexed state for a page 98 * 99 * @param string $id 100 * @return int 101 */ 102 private function update_indexstate($id) { 103 $indexStateFile = metaFN($id, '.elasticsearch_indexed'); 104 return io_saveFile($indexStateFile, ''); 105 } 106 107 /** 108 * Remove the given document from the index 109 * 110 * @param $id 111 */ 112 public function delete_page($id) { 113 /** @var helper_plugin_elasticsearch_client $hlp */ 114 $hlp = plugin_load('helper', 'elasticsearch_client'); 115 $indexName = $this->getConf('indexname'); 116 $documentType = $this->getConf('documenttype'); 117 $client = $hlp->connect(); 118 $index = $client->getIndex($indexName); 119 $type = $index->getType($documentType); 120 $documentId = $documentType . '_' . $id; 121 122 try { 123 $type->deleteById($documentId); 124 $index->refresh(); 125 $this->log($documentId.' deleted '); 126 } catch(Exception $e) { 127 // we ignore this 128 $this->log($documentId.' not deleted '.$e->getMessage()); 129 } 130 131 // delete state file 132 @unlink(metaFN($id, '.elasticsearch_indexed')); 133 } 134 135 /** 136 * Index a page 137 * 138 * @param $id 139 * @return void 140 */ 141 public function index_page($id) { 142 global $conf; 143 144 /** @var helper_plugin_elasticsearch_client $hlp */ 145 $hlp = plugin_load('helper', 'elasticsearch_client'); 146 147 $this->log('Indexing page ' . $id); 148 $indexName = $this->getConf('indexname'); 149 $documentType = $this->getConf('documenttype'); 150 $client = $hlp->connect(); 151 $index = $client->getIndex($indexName); 152 $type = $index->getType($documentType); 153 $documentId = $documentType . '_' . $id; 154 155 // collect the date which should be indexed 156 $meta = p_get_metadata($id, '', METADATA_RENDER_UNLIMITED); 157 158 $data = array(); 159 $data['uri'] = $id; 160 $data['created'] = date('Y-m-d\TH:i:s\Z', $meta['date']['created']); 161 $data['modified'] = date('Y-m-d\TH:i:s\Z', $meta['date']['modified']); 162 $data['creator'] = $meta['creator']; 163 $data['title'] = $meta['title']; 164 $data['abstract'] = $meta['description']['abstract']; 165 $data['content'] = rawWiki($id); 166 167 /** @var helper_plugin_translation $trans */ 168 $trans = plugin_load('helper', 'translation'); 169 if($trans) { 170 // translation plugin available 171 $lc = $trans->getLangPart($id); 172 $data['language'] = $trans->realLC($lc); 173 } else { 174 // no translation plugin 175 $lc = ''; 176 $data['language'] = $conf['lang']; 177 } 178 179 $data['namespace'] = getNS($id); 180 if(trim($data['namespace']) == '') { 181 unset($data['namespace']); 182 } 183 184 // only index if we've found a namespace 185 if(isset($data['namespace']) || !$this->getConf('realmonly')) { 186 $data['groups'] = $this->getPageACL($id); 187 188 // check if the document still exists to update it or add it as a new one 189 try { 190 $document = $type->getDocument($documentId); 191 $client->updateDocument($documentId, array('doc' => $data), $index->getName(), $type->getName()); 192 } catch(\Elastica\Exception\NotFoundException $e) { 193 $document = new \Elastica\Document($documentId, $data); 194 $type->addDocument($document); 195 } catch(Exception $e) { 196 msg('Something went wrong on indexing please try again later or ask an admin for help.<br /><pre>' . hsc($e->getMessage()) . '</pre>', -1); 197 return; 198 } 199 $index->refresh(); 200 $this->update_indexstate($id); 201 } else { 202 $this->log("$id has no realm (top namespace) - not indexed"); 203 $this->update_indexstate($id); 204 } 205 } 206 207 /** 208 * Log something to the debug log 209 * 210 * @param string $txt 211 */ 212 private function log($txt) { 213 if(!$this->getConf('debug')) { 214 return; 215 } 216 if(!is_array($txt)) { 217 $logs = array($txt); 218 } else { 219 $logs = $txt; 220 } 221 foreach($logs as $entry) { 222 dbglog($entry); 223 } 224 } 225 226 /** 227 * Lists all groups that have at least read permissions on the given page 228 * 229 * @param $id 230 * @return array 231 */ 232 private function getPageACL($id) { 233 // FIXME why is global $AUTH_ACL null??? 234 auth_setup(); 235 236 global $AUTH_ACL; 237 global $conf; 238 239 $id = cleanID($id); 240 $ns = getNS($id); 241 $perms = array(); 242 243 $matches = preg_grep('/^' . preg_quote($id, '/') . '\s+/', $AUTH_ACL); 244 if(count($matches)) { 245 foreach($matches as $match) { 246 $match = preg_replace('/#.*$/', '', $match); //ignore comments 247 $acl = preg_split('/\s+/', $match); 248 if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL! 249 if(!isset($perms[$acl[1]])) $perms[$acl[1]] = $acl[2]; 250 } 251 } 252 //still here? do the namespace checks 253 if($ns) { 254 $path = $ns . ':\*'; 255 } else { 256 $path = '\*'; //root document 257 } 258 do { 259 $matches = preg_grep('/^' . $path . '\s+/', $AUTH_ACL); 260 if(count($matches)) { 261 foreach($matches as $match) { 262 $match = preg_replace('/#.*$/', '', $match); //ignore comments 263 $acl = preg_split('/\s+/', $match); 264 if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL! 265 if(!isset($perms[$acl[1]])) $perms[$acl[1]] = $acl[2]; 266 } 267 } 268 269 //get next higher namespace 270 $ns = getNS($ns); 271 272 if($path != '\*') { 273 $path = $ns . ':\*'; 274 if($path == ':\*') $path = '\*'; 275 } else { 276 //we did this already 277 //break here 278 break; 279 } 280 } while(1); //this should never loop endless 281 $groups = array(str_replace('-', '', str_replace('@', '', strtolower(urldecode($conf['superuser']))))); 282 foreach($perms as $group => $permission) { 283 if($permission > AUTH_NONE) { 284 $groups[] = str_replace('-', '', str_replace('@', '', strtolower(urldecode($group)))); 285 $this->log(sprintf("%s = %s", $group, $permission)); 286 } 287 } 288 return $groups; 289 } 290} 291