xref: /plugin/elasticsearch/action/indexing.php (revision f3e6b112ec339840410d0e60e308f13c65596972) !
1<?php
2
3use dokuwiki\Extension\Event;
4
5/**
6 * DokuWiki Plugin elasticsearch (Action Component)
7 *
8 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
9 * @author  Kieback&Peter IT <it-support@kieback-peter.de>
10 * @author  Andreas Gohr <gohr@cosmocode.de>
11 */
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', 'AFTER', $this, 'handle_media_delete');
30    }
31
32    /**
33     * Add pages to index
34     *
35     * @param Doku_Event $event event object by reference
36     * @return void
37     */
38    public function handle_tpl_content_display(Doku_Event $event) {
39        global $ID, $INFO;
40        $logs   = [];
41        $logs[] = 'BEGIN content display';
42        $logs[] = metaFN($ID, '.elasticsearch_indexed');
43        $logs[] = wikiFN($ID);
44        $logs[] = wikiFN($INFO['id']);
45        $logs[] = $this->needs_indexing($ID) ? 'needs indexing' : 'no indexing needed';
46        $logs[] = 'END content display';
47        $this->log($logs);
48        if ($this->needs_indexing($ID)) {
49            $this->index_page($ID);
50        }
51    }
52
53    /**
54     * Update index on media upload
55     *
56     * @param Doku_Event $event
57     * @throws Exception
58     */
59    public function handle_media_upload(Doku_Event $event)
60    {
61        $this->index_file($event->data[2]);
62    }
63
64    /**
65     * Remove pages from index
66     *
67     * @param Doku_Event $event event object by reference
68     * @return void
69     */
70    public function handle_delete(Doku_Event $event) {
71        if ($event->data[3]) return; // is old revision stuff
72        if (!empty($event->data[0][1])) return; // page still exists
73        // still here? delete from index
74        $this->delete_entry($event->data[2], self::DOCTYPE_PAGE);
75    }
76
77    /**
78     * Remove deleted media from index
79     *
80     * @param Doku_Event $event
81     * @param $param
82     */
83    public function handle_media_delete(Doku_Event $event, $param)
84    {
85        if ($event->data['unl']) $this->delete_entry($event->data['id'], self::DOCTYPE_MEDIA);
86    }
87
88    /**
89     * Check if the page $id has changed since the last indexing.
90     *
91     * @param string $id
92     * @return boolean
93     */
94    protected function needs_indexing($id) {
95        $indexStateFile = metaFN($id, '.elasticsearch_indexed');
96        $refreshStateFile = metaFN($id, '.elasticsearch_refresh');
97        $dataFile = wikiFN($id);
98
99        // no data file or page is hidden ('hidepages' configuration option) -> no indexing
100        if (!file_exists($dataFile) || isHiddenPage($id)) {
101            // page should not be indexed but has a state file, try to remove from index
102            if (file_exists($indexStateFile)) {
103                $this->delete_entry($id, self::DOCTYPE_PAGE);
104            }
105            return false;
106        }
107
108        // force indexing if we're called via cli (e.g. cron)
109        if (php_sapi_name() == 'cli') {
110            return true;
111        }
112        // check if latest indexing attempt is done after page update
113        // and after other updates related to the page made by plugins
114        if (file_exists($indexStateFile)) {
115            if (
116                (filemtime($indexStateFile) > filemtime($dataFile)) &&
117                (!file_exists($refreshStateFile) || filemtime($indexStateFile) > filemtime($refreshStateFile))
118            ) {
119                return false;
120            }
121        }
122        return true;
123    }
124
125    /**
126     * @param array $data
127     */
128    protected function write_index($data)
129    {
130        /** @var helper_plugin_elasticsearch_client $hlp */
131        $hlp = plugin_load('helper', 'elasticsearch_client');
132
133        $indexName    = $this->getConf('indexname');
134        $documentType = $this->getConf('documenttype');
135        $client       = $hlp->connect();
136        $index        = $client->getIndex($indexName);
137        $type         = $index->getType($documentType);
138        $documentId   = $data['doctype'] . '_' . $data['uri'];
139
140        // check if the document still exists to update it or add it as a new one
141        try {
142            $client->updateDocument($documentId, ['doc' => $data], $index->getName(), $type->getName());
143        } catch (\Elastica\Exception\NotFoundException $e) {
144            $document = new \Elastica\Document($documentId, $data);
145            $type->addDocument($document);
146        } catch (\Elastica\Exception\ResponseException $e) {
147            if ($e->getResponse()->getStatus() == 404) {
148                $document = new \Elastica\Document($documentId, $data);
149                $type->addDocument($document);
150            } else {
151                throw $e;
152            }
153        } catch (Exception $e) {
154            msg(
155                'Something went wrong on indexing please try again later or ask an admin for help.<br /><pre>' .
156                hsc(get_class($e) . ' ' . $e->getMessage()) . '</pre>',
157                -1
158            );
159            return;
160        }
161        $index->refresh();
162        $this->update_indexstate($data['uri']);
163    }
164
165    /**
166     * Save indexed state for a page or a media file
167     *
168     * @param string $id
169     * @param string $doctype
170     * @return bool
171     */
172    protected function update_indexstate($id, $doctype = self::DOCTYPE_PAGE) {
173        $indexStateFile = ($doctype === self::DOCTYPE_MEDIA) ?
174            mediaMetaFN($id, '.elasticsearch_indexed') :
175            metaFN($id, '.elasticsearch_indexed');
176        return io_saveFile($indexStateFile, '');
177    }
178
179    /**
180     * Remove the given document from the index
181     *
182     * @param $id
183     * @param $doctype
184     */
185    public function delete_entry($id, $doctype) {
186        /** @var helper_plugin_elasticsearch_client $hlp */
187        $hlp          = plugin_load('helper', 'elasticsearch_client');
188        $indexName    = $this->getConf('indexname');
189        $documentType = $this->getConf('documenttype');
190        $client       = $hlp->connect();
191        $index        = $client->getIndex($indexName);
192        $type         = $index->getType($documentType);
193        $documentId   = $doctype . '_' . $id;
194
195        try {
196            $type->deleteById($documentId);
197            $index->refresh();
198            $this->log($documentId.' deleted ');
199        } catch(Exception $e) {
200            // we ignore this
201            $this->log($documentId.' not deleted '.$e->getMessage());
202        }
203
204        // delete state file
205        $stateFile = ($doctype === self::DOCTYPE_MEDIA) ?
206            mediaMetaFN($id, '.elasticsearch_indexed') :
207            metaFN($id, '.elasticsearch_indexed');
208        @unlink($stateFile);
209    }
210
211    /**
212     * Index a page
213     *
214     * @param $id
215     * @return void
216     */
217    public function index_page($id) {
218        global $conf;
219
220        $this->log('Indexing page ' . $id);
221
222        // collect the date which should be indexed
223        $meta = p_get_metadata($id, '', METADATA_RENDER_UNLIMITED);
224
225        $data             = array();
226        $data['uri']      = $id;
227        $data['created']  = date('Y-m-d\TH:i:s\Z', $meta['date']['created']);
228        $data['modified'] = date('Y-m-d\TH:i:s\Z', $meta['date']['modified']);
229        $data['user']     = $meta['user'];
230        $data['title']    = $meta['title'];
231        $data['abstract'] = $meta['description']['abstract'];
232        $data['syntax']   = rawWiki($id);
233        $data['mime']     = self::MIME_DOKUWIKI;
234        $data['doctype']  = self::DOCTYPE_PAGE;
235
236        // prefer rendered plaintext over raw syntax output
237        /** @var \renderer_plugin_text $textRenderer */
238        $textRenderer = plugin_load('renderer', 'text');
239        if ($textRenderer) {
240            $data['content'] = p_cached_output(wikiFN($id),'text');
241        } else {
242            $data['content']  = $data['syntax'];
243        }
244
245        /** @var helper_plugin_translation $trans */
246        $trans = plugin_load('helper', 'translation');
247        if($trans) {
248            // translation plugin available
249            $lc               = $trans->getLangPart($id);
250            $data['language'] = $trans->realLC($lc);
251        } else {
252            // no translation plugin
253            $data['language'] = $conf['lang'];
254        }
255
256        $data['namespace'] = getNS($id);
257        if(trim($data['namespace']) == '') {
258            unset($data['namespace']);
259        }
260
261        /** @var helper_plugin_elasticsearch_acl $hlpAcl */
262        $hlpAcl = plugin_load('helper', 'elasticsearch_acl');
263
264        $fullACL = $hlpAcl->getPageACL($id);
265        $queryACL = $hlpAcl->splitRules($fullACL);
266        $data = array_merge($data, $queryACL);
267
268        // let plugins add their own data to index
269        $pluginData = $this->getPluginData($data['uri']);
270        $data = array_merge($data, $pluginData);
271
272        $this->write_index($data);
273    }
274
275    /**
276     * Index a file
277     *
278     * @param string $fileId
279     * @return void
280     * @throws Exception
281     */
282    public function index_file($fileId) {
283        $this->log('Indexing file ' . $fileId);
284
285        $docparser = new \helper_plugin_elasticsearch_docparser();
286
287        $file = mediaFN($fileId);
288
289        try {
290            $data = $docparser->parse($file);
291            $data['uri'] = $fileId;
292            $data['doctype'] = self::DOCTYPE_MEDIA;
293            $data['modified'] = date('Y-m-d\TH:i:s\Z', filemtime($file));
294            $data['namespace'] = getNS($fileId);
295            if (trim($data['namespace']) == '') {
296                unset($data['namespace']);
297            }
298
299            /** @var helper_plugin_elasticsearch_acl $hlpAcl */
300            $hlpAcl = plugin_load('helper', 'elasticsearch_acl');
301
302            $fullACL = $hlpAcl->getPageACL($fileId);
303            $queryACL = $hlpAcl->splitRules($fullACL);
304            $data = array_merge($data, $queryACL);
305
306            $this->write_index($data);
307        } catch (RuntimeException $e) {
308            $this->log('Skipping ' . $fileId . ': ' . $e->getMessage());
309        }
310    }
311
312
313    /**
314     * Get plugin data to feed into the index.
315     * If data does not match previously defined mappings, it will be ignored.
316     *
317     * @param $id
318     * @return array
319     */
320    protected function getPluginData($id): array
321    {
322        $pluginData = ['uri' => $id];
323        Event::createAndTrigger('PLUGIN_ELASTICSEARCH_INDEXPAGE', $pluginData);
324        return $pluginData;
325    }
326
327    /**
328     * Log something to the debug log
329     *
330     * @param string|string[] $txt
331     */
332    protected function log($txt) {
333        if (!$this->getConf('debug')) {
334            return;
335        }
336        $logs = (array)$txt;
337
338        foreach ($logs as $entry) {
339            dbglog($entry);
340        }
341    }
342}
343