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