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