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 return false; 61 } 62 63 // force indexing if we're called via cli (e.g. cron) 64 if(php_sapi_name() == 'cli') { 65 return true; 66 } 67 // check if latest indexing attempt is done after page update 68 if(file_exists($indexStateFile)) { 69 if(filemtime($indexStateFile) > filemtime($dataFile)) { 70 return false; 71 } 72 } 73 return true; 74 } 75 76 /** 77 * Save indexed state for a page 78 * 79 * @param string $id 80 * @return int 81 */ 82 private function update_indexstate($id) { 83 $indexStateFile = metaFN($id, '.elasticsearch_indexed'); 84 return file_put_contents($indexStateFile, ''); 85 } 86 87 /** 88 * Index a page 89 * 90 * @param $id 91 * @return void 92 */ 93 public function index_page($id) { 94 global $conf; 95 96 /** @var helper_plugin_elasticsearch_client $hlp */ 97 $hlp = plugin_load('helper', 'elasticsearch_client'); 98 99 $this->log('Indexing page ' . $id); 100 $indexName = $this->getConf('indexname'); 101 $documentType = $this->getConf('documenttype'); 102 $client = $hlp->connect(); 103 $index = $client->getIndex($indexName); 104 $type = $index->getType($documentType); 105 $documentId = $documentType . '_' . $id; 106 107 // @TODO check if content is empty, that means the page is deleted and 108 // has to be removed from the index. 109 110 // collect the date which should be indexed 111 $meta = p_get_metadata($id, '', true); 112 113 $data = array(); 114 $data['uri'] = $id; 115 $data['created'] = date('Y-m-d\TH:i:s\Z', $meta['date']['created']); 116 $data['modified'] = date('Y-m-d\TH:i:s\Z', $meta['date']['modified']); 117 $data['creator'] = $meta['creator']; 118 $data['title'] = $meta['title']; 119 $data['abstract'] = $meta['description']['abstract']; 120 $data['content'] = rawWiki($id); 121 122 /** @var helper_plugin_translation $trans */ 123 $trans = plugin_load('helper', 'translation'); 124 if($trans) { 125 // translation plugin available 126 $lc = $trans->getLangPart($id); 127 $data['language'] = $trans->realLC($lc); 128 } else { 129 // no translation plugin 130 $lc = ''; 131 $data['language'] = $conf['lang']; 132 } 133 134 // fetching the top namespace's title 135 $ids = explode(':', $id); 136 if($lc) { 137 $metadata_ns = p_get_metadata($ids[0] . ':' . $ids[1] . ':start', '', true); 138 } else { 139 $metadata_ns = p_get_metadata($ids[0] . ':start', '', true); 140 } 141 $data['namespace'] = $metadata_ns['title']; 142 $data['namespace'] = str_replace('*', '', $data['namespace']); 143 if(trim($data['namespace']) == '') { 144 unset($data['namespace']); 145 } 146 147 // only index if we've found a namespace 148 if(isset($data['namespace'])) { 149 $data['groups'] = $this->getPageACL($id); 150 151 // check if the document still exists to update it or add it as a new one 152 try { 153 $document = $type->getDocument($documentId); 154 $client->updateDocument($documentId, array('doc' => $data), $index->getName(), $type->getName()); 155 } catch(\Elastica\Exception\NotFoundException $e) { 156 $document = new \Elastica\Document($documentId, $data); 157 $type->addDocument($document); 158 } 159 $index->refresh(); 160 $this->update_indexstate($id); 161 } 162 } 163 164 /** 165 * Log something to the debug log 166 * 167 * @param string $txt 168 */ 169 private function log($txt) { 170 if(!$this->getConf('debug')) { 171 return; 172 } 173 if(!is_array($txt)) { 174 $logs = array($txt); 175 } else { 176 $logs = $txt; 177 } 178 foreach($logs as $entry) { 179 dbglog($entry); 180 } 181 } 182 183 /** 184 * Lists all groups that have at least read permissions on the given page 185 * 186 * @param $id 187 * @return array 188 */ 189 private function getPageACL($id) { 190 global $AUTH_ACL; 191 global $conf; 192 193 $id = cleanID($id); 194 $ns = getNS($id); 195 $perms = array(); 196 197 $matches = preg_grep('/^' . preg_quote($id, '/') . '\s+/', $AUTH_ACL); 198 if(count($matches)) { 199 foreach($matches as $match) { 200 $match = preg_replace('/#.*$/', '', $match); //ignore comments 201 $acl = preg_split('/\s+/', $match); 202 if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL! 203 if(!isset($perms[$acl[1]])) $perms[$acl[1]] = $acl[2]; 204 } 205 } 206 //still here? do the namespace checks 207 if($ns) { 208 $path = $ns . ':\*'; 209 } else { 210 $path = '\*'; //root document 211 } 212 do { 213 $matches = preg_grep('/^' . $path . '\s+/', $AUTH_ACL); 214 if(count($matches)) { 215 foreach($matches as $match) { 216 $match = preg_replace('/#.*$/', '', $match); //ignore comments 217 $acl = preg_split('/\s+/', $match); 218 if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL! 219 if(!isset($perms[$acl[1]])) $perms[$acl[1]] = $acl[2]; 220 } 221 } 222 223 //get next higher namespace 224 $ns = getNS($ns); 225 226 if($path != '\*') { 227 $path = $ns . ':\*'; 228 if($path == ':\*') $path = '\*'; 229 } else { 230 //we did this already 231 //break here 232 break; 233 } 234 } while(1); //this should never loop endless 235 $groups = array(str_replace('-', '', str_replace('@', '', strtolower(urldecode($conf['superuser']))))); 236 foreach($perms as $group => $permission) { 237 if($permission > AUTH_NONE) { 238 $groups[] = str_replace('-', '', str_replace('@', '', strtolower(urldecode($group)))); 239 $this->log(sprintf("%s = %s", $group, $permission)); 240 } 241 } 242 return $groups; 243 } 244}