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