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