xref: /plugin/elasticsearch/action/indexing.php (revision 9a2288a153ca3399fabc000d567179a2e410eefe)
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