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 const MIME_DOKUWIKI = 'text/dokuwiki'; 16 const DOCTYPE_PAGE = 'page'; 17 const DOCTYPE_MEDIA = 'media'; 18 19 /** 20 * Registers a callback function for a given event 21 * 22 * @param Doku_Event_Handler $controller DokuWiki's event controller object 23 * @return void 24 */ 25 public function register(Doku_Event_Handler $controller) { 26 $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'handle_tpl_content_display'); 27 $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'handle_delete'); 28 $controller->register_hook('MEDIA_UPLOAD_FINISH', 'AFTER', $this, 'handle_media_upload'); 29 $controller->register_hook('MEDIA_DELETE_FILE', 'BEFORE', $this, 'handle_media_delete'); 30 } 31 32 /** 33 * Add pages to index 34 * 35 * @param Doku_Event $event event object by reference 36 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 37 * handler was registered] 38 * @return void 39 */ 40 public function handle_tpl_content_display(Doku_Event &$event, $param) { 41 global $ID, $INFO; 42 $logs = array(); 43 $logs[] = 'BEGIN content display'; 44 $logs[] = metaFN($ID, '.elasticsearch_indexed'); 45 $logs[] = wikiFN($ID); 46 $logs[] = wikiFN($INFO['id']); 47 $logs[] = $this->needs_indexing($ID) ? 'needs indexing' : 'no indexing needed'; 48 $logs[] = 'END content display'; 49 $this->log($logs); 50 if($this->needs_indexing($ID)) { 51 $this->index_page($ID); 52 } 53 } 54 55 /** 56 * Update index on media upload 57 * 58 * @param Doku_Event $event 59 * @param $param 60 * @throws Exception 61 */ 62 public function handle_media_upload(Doku_Event $event, $param) 63 { 64 $this->index_file($event->data[2]); 65 } 66 67 /** 68 * Remove pages from index 69 * 70 * @param Doku_Event $event event object by reference 71 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 72 * handler was registered] 73 * @return void 74 */ 75 public function handle_delete(Doku_Event &$event, $param) { 76 if($event->data[3]) return; // is old revision stuff 77 if(!empty($event->data[0][1])) return; // page still exists 78 // still here? delete from index 79 $this->delete_page($event->data[2]); 80 } 81 82 /** 83 * Check if the page $id has changed since the last indexing. 84 * 85 * @param string $id 86 * @return boolean 87 */ 88 protected function needs_indexing($id) { 89 $indexStateFile = metaFN($id, '.elasticsearch_indexed'); 90 $dataFile = wikiFN($id); 91 92 // no data file -> no indexing 93 if(!file_exists($dataFile)) { 94 // page does not exist but has a state file, try to remove from index 95 if(file_exists($indexStateFile)) { 96 $this->delete_page($id); 97 } 98 return false; 99 } 100 101 // force indexing if we're called via cli (e.g. cron) 102 if(php_sapi_name() == 'cli') { 103 return true; 104 } 105 // check if latest indexing attempt is done after page update 106 if(file_exists($indexStateFile)) { 107 if(filemtime($indexStateFile) > filemtime($dataFile)) { 108 return false; 109 } 110 } 111 return true; 112 } 113 114 /** 115 * @param array $data 116 */ 117 protected function write_index($data) 118 { 119 /** @var helper_plugin_elasticsearch_client $hlp */ 120 $hlp = plugin_load('helper', 'elasticsearch_client'); 121 122 $indexName = $this->getConf('indexname'); 123 $documentType = $this->getConf('documenttype'); 124 $client = $hlp->connect(); 125 $index = $client->getIndex($indexName); 126 $type = $index->getType($documentType); 127 $documentId = $data['doctype'] . '_' . $data['uri']; 128 129 // check if the document still exists to update it or add it as a new one 130 try { 131 $client->updateDocument($documentId, ['doc' => $data], $index->getName(), $type->getName()); 132 } catch (\Elastica\Exception\NotFoundException $e) { 133 $document = new \Elastica\Document($documentId, $data); 134 $type->addDocument($document); 135 } catch (\Elastica\Exception\ResponseException $e) { 136 if ($e->getResponse()->getStatus() == 404) { 137 $document = new \Elastica\Document($documentId, $data); 138 $type->addDocument($document); 139 } else { 140 throw $e; 141 } 142 } catch (Exception $e) { 143 msg( 144 'Something went wrong on indexing please try again later or ask an admin for help.<br /><pre>' . 145 hsc(get_class($e) . ' ' . $e->getMessage()) . '</pre>', 146 -1 147 ); 148 return; 149 } 150 $index->refresh(); 151 $this->update_indexstate($data['uri']); 152 } 153 154 /** 155 * Save indexed state for a page or a media file 156 * 157 * @param string $id 158 * @param string $doctype 159 * @return int 160 */ 161 protected function update_indexstate($id, $doctype = self::DOCTYPE_PAGE) { 162 $indexStateFile = ($doctype === self::DOCTYPE_MEDIA) ? 163 mediaMetaFN($id, '.elasticsearch_indexed') : 164 metaFN($id, '.elasticsearch_indexed'); 165 return io_saveFile($indexStateFile, ''); 166 } 167 168 /** 169 * Remove the given document from the index 170 * 171 * @param $id 172 */ 173 public function delete_page($id) { 174 /** @var helper_plugin_elasticsearch_client $hlp */ 175 $hlp = plugin_load('helper', 'elasticsearch_client'); 176 $indexName = $this->getConf('indexname'); 177 $documentType = $this->getConf('documenttype'); 178 $client = $hlp->connect(); 179 $index = $client->getIndex($indexName); 180 $type = $index->getType($documentType); 181 $documentId = self::DOCTYPE_PAGE . '_' . $id; 182 183 try { 184 $type->deleteById($documentId); 185 $index->refresh(); 186 $this->log($documentId.' deleted '); 187 } catch(Exception $e) { 188 // we ignore this 189 $this->log($documentId.' not deleted '.$e->getMessage()); 190 } 191 192 // delete state file 193 @unlink(metaFN($id, '.elasticsearch_indexed')); 194 } 195 196 /** 197 * Index a page 198 * 199 * @param $id 200 * @return void 201 */ 202 public function index_page($id) { 203 global $conf; 204 205 /** @var helper_plugin_elasticsearch_acl $hlpAcl */ 206 $hlpAcl = plugin_load('helper', 'elasticsearch_acl'); 207 208 $this->log('Indexing page ' . $id); 209 210 // collect the date which should be indexed 211 $meta = p_get_metadata($id, '', METADATA_RENDER_UNLIMITED); 212 213 $data = array(); 214 $data['uri'] = $id; 215 $data['created'] = date('Y-m-d\TH:i:s\Z', $meta['date']['created']); 216 $data['modified'] = date('Y-m-d\TH:i:s\Z', $meta['date']['modified']); 217 $data['user'] = $meta['user']; 218 $data['title'] = $meta['title']; 219 $data['abstract'] = $meta['description']['abstract']; 220 $data['content'] = rawWiki($id); 221 $data['mime'] = self::MIME_DOKUWIKI; 222 $data['doctype'] = self::DOCTYPE_PAGE; 223 224 /** @var helper_plugin_translation $trans */ 225 $trans = plugin_load('helper', 'translation'); 226 if($trans) { 227 // translation plugin available 228 $lc = $trans->getLangPart($id); 229 $data['language'] = $trans->realLC($lc); 230 } else { 231 // no translation plugin 232 $lc = ''; 233 $data['language'] = $conf['lang']; 234 } 235 236 $data['namespace'] = getNS($id); 237 if(trim($data['namespace']) == '') { 238 unset($data['namespace']); 239 } 240 241 $fullACL = $hlpAcl->getPageACL($id); 242 $queryACL = $hlpAcl->splitRules($fullACL); 243 $data = array_merge($data, $queryACL); 244 $this->write_index($data); 245 } 246 247 /** 248 * Index a file 249 * 250 * @param string $fileId 251 * @return void 252 * @throws Exception 253 */ 254 public function index_file($fileId) { 255 global $conf; 256 257 $this->log('Indexing file ' . $fileId); 258 259 $docparser = new \helper_plugin_elasticsearch_docparser(); 260 261 $file = mediaFN($fileId); 262 $data = $docparser->parse($file); 263 $data['uri'] = $fileId; 264 $data['doctype'] = self::DOCTYPE_MEDIA; 265 $data['modified'] = date('Y-m-d\TH:i:s\Z', filemtime($file)); 266 $data['namespace'] = getNS($fileId); 267 if(trim($data['namespace']) == '') { 268 unset($data['namespace']); 269 } 270 271 /** @var helper_plugin_elasticsearch_acl $hlpAcl */ 272 $hlpAcl = plugin_load('helper', 'elasticsearch_acl'); 273 274 $fullACL = $hlpAcl->getPageACL($fileId); 275 $queryACL = $hlpAcl->splitRules($fullACL); 276 $data = array_merge($data, $queryACL); 277 278 $this->write_index($data); 279 } 280 281 /** 282 * Log something to the debug log 283 * 284 * @param string|string[] $txt 285 */ 286 protected function log($txt) { 287 if (!$this->getConf('debug')) { 288 return; 289 } 290 $logs = (array)$txt; 291 292 foreach ($logs as $entry) { 293 dbglog($entry); 294 } 295 } 296} 297