1<?php
2
3/**
4 * DokuWiki Plugin doxycode (Action Component)
5 *
6 * @license     GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
7 * @author      Lukas Probsthain <lukas.probsthain@gmail.com>
8 */
9
10use dokuwiki\Extension\ActionPlugin;
11use dokuwiki\HTTP\DokuHTTPClient;
12use dokuwiki\Extension\Event;
13use dokuwiki\Cache\Cache;
14use dokuwiki\Extension\EventHandler;
15
16/**
17 * Class action_plugin_doxycode
18 *
19 * This action component of the doxygen plugin handles the download of remote tag files,
20 * build job/task execution, cache invalidation, and ajax calls for checking the job/task
21 * execution and dynamic loading of finished code snippets.
22 *
23 * @author      Lukas Probsthain <lukas.probsthain@gmail.com>
24 */
25class action_plugin_doxycode extends ActionPlugin
26{
27    public function register(EventHandler $controller)
28    {
29        $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'loadRemoteTagFiles');
30        $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'renderDoxyCodeSnippets');
31        $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'beforeParserCacheUse');
32        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'ajaxCall');
33        $controller->register_hook('TOOLBAR_DEFINE', 'AFTER', $this, 'insertTagButton');
34        $controller->register_hook('RPC_CALL_ADD', 'AFTER', $this, 'addRpcMethod');
35    }
36
37    /**
38     * Download remote doxygen tag files and place the in the tag file directory.
39     *
40     * Remote tag files are tag files that are publicly hosted on another website.
41     * This task runner hook gets the tag file configuration for remote tag files and checks
42     * if it is time to check the remote tag file again.
43     *
44     * If the time threshold is reached for checking again it tries to download the tag file again,
45     * updates the 'last_update' timestamp in the configuration, and then checks if we have an updated
46     * tag file by comparing the md5 of the cached tag file.
47     *
48     * The configuration with the updated 'last_update' timestamp is saved without modifying the mtime
49     * of the configuration because the configuration file is listed as a file dependency among the used tag files
50     * for the cached snippets. We only want to invalidate the cached snippets if the configuration really changes
51     * or a new tag file is available.
52     */
53    public function loadRemoteTagFiles(Event $event, $param)
54    {
55        // get the tag files from the helper
56
57        /** @var helper_plugin_doxycode_tagmanager $tagmanager */
58        $tagmanager = plugin_load('helper', 'doxycode_tagmanager');
59
60        // load complete tag file configuration
61        // we need this since we'll save the configuration later
62        // if we use the filtered configuration for saving we would remove all other elements
63        $tag_config = $tagmanager->loadTagFileConfig();
64
65        // only try to download a tag file if it is a remote file and enabled!
66        // TODO: we could also use a filter function for filtering remote configurations with overdue updates
67        $filtered_config = $tagmanager->filterConfig($tag_config, ['isConfigEnabled','isValidRemoteConfig']);
68
69        // loop over all tag file configurations
70        foreach ($filtered_config as $key => &$conf) {
71            $now = time();
72            $timestamp = $conf['last_update'] ? $conf['last_update'] : 0;
73            $update_period = $conf['update_period'] ? $conf['update_period'] :  $this->getConf('update_period');
74            $filename = $tagmanager->getTagFileDir() . $key . '.xml';
75
76            // only try to update every $update_period amount of seconds
77            if ($now - $update_period >= $timestamp) {
78                // only process one thing per task runner run
79                $event->stopPropagation();
80                $event->preventDefault();
81
82                $tag_config[$key]['last_update'] = $now;
83                // save the new timestamp - regardless of the success of the rest
84                // on connection failures, we don't want to run the task runner constantly on failures!
85                // true: do not update the mtime of the configuration!
86                // if a new tag file is available we invalidate the cache if this tag file was used in a page!
87                $tagmanager->saveTagFileConfig($tag_config, true);
88
89                $exists = false;    // if the file does not exist - just write now!
90                $cachedHash = '';   // this is used to check if we really have a new file
91
92                // first read the old file and free memory so we do not exeed the limit!
93                if ($cachedContent = @file_get_contents($filename)) {
94                    $exists = true;
95
96                    $cachedHash = md5($cachedContent);
97
98                    unset($cachedContent);
99                }
100
101                // it's time to reload the tag file
102                $http = new DokuHTTPClient();
103                $url = $conf['remote_url'];
104                if (!$http->get($url)) {
105                    $error = 'could not get ' . hsc($url) . ', HTTP status ' . $http->status . '. ';
106                    throw new Exception($error);
107                }
108
109                // here we have the new content
110                $newContent = $http->resp_body;
111                $newHash = md5($newContent);
112
113                if (!$exists || $cachedHash !== $newHash) {
114                    // save the new tag file
115                    file_put_contents($filename, $newContent);
116                }
117
118
119                return; // we only ever want to run one file!
120            }
121        }
122    }
123
124    public function renderDoxyCodeSnippets(Event $event)
125    {
126        global $ID;
127
128        /** @var helper_plugin_doxycode_buildmanager $buildmanager */
129        $buildmanager = plugin_load('helper', 'doxycode_buildmanager');
130
131        // TODO: instead of counting the executions in $iterations we could directly obtain
132        // a specific amount of tasks from getBuildTasks
133        $tasks = $buildmanager->getBuildTasks();
134
135        if (sizeof($tasks) > 0) {
136            $event->stopPropagation();
137            $event->preventDefault();
138
139            $iterations = 0;
140            // TODO: we should implement a maximum amount of tasks to run in one task runner instance!
141            foreach ($tasks as $task) {
142                $iterations++;
143
144                if ($iterations > $this->getConf('runner_max_tasks')) {
145                    return;
146                }
147
148                if (!$buildmanager->runTask($task['TaskID'])) {
149                    // if we couldn't build abort the task runner
150                    // this might happen if another instance is already running
151                    return;
152                }
153            }
154        }
155    }
156
157    public function beforeParserCacheUse(Event $event)
158    {
159        global $ID;
160        $cache = $event->data;
161        if (isset($cache->mode) && ($cache->mode == 'xhtml')) {
162            // load doxycode meta that includes the used tag files and the cache files for the snippets
163            $doxycode_meta = p_get_metadata($ID, 'doxycode');
164
165            if ($doxycode_meta == null) {
166                // doxycode was not used in this page!
167                return;
168            }
169
170            $depends = [];
171
172            // if the tagfiles were updated in the background, we need to rebuild the snippets
173            // in this case we first need to invalidate the page cache
174            $tagfiles = $doxycode_meta['tagfiles'];
175
176            if ($tagfiles != null) {
177                // transform the list of tag files into an array that can be used by the helper
178                // ['tag_name' => []]: empty array as value since we do not have a loaded tag configuration here!
179                $tag_config = array_fill_keys($tagfiles, []);
180
181                /** @var helper_plugin_doxycode $helper */
182                $helper = plugin_load('helper', 'doxycode');
183
184                // transform the tag names to full file paths
185                $helper->getTagFiles($depends, $tag_config);
186            };
187
188            // load the PHP file dependencies
189            // if any file was changed, we want to do a reload
190            // the other dependencies (cache files) might not be enough,
191            // if e.g. the way we generate the hash names change
192            // this might happen if the old cache files still exist and the meta data was not updated
193            $helper->getPHPFileDependencies($depends);
194
195            // add these to the dependency list
196            if (!empty($depends) && isset($depends['files'])) {
197                $this->addDependencies($cache, $depends['files']);
198            }
199
200            // if the XML cache is not existent we should check if the build is scheduled in the main syntax component
201            $cache_files = $doxycode_meta['xml_cachefiles'];
202
203            foreach ($cache_files as $cacheID) {
204                $cache_name = getCacheName($cacheID, ".xml");
205
206                if (!@file_exists($cache_name)) {
207                    $event->preventDefault();
208                    $event->stopPropagation();
209                    $event->result = false;
210                }
211
212                $this->addDependencies($cache, [$cache_name]);
213            }
214
215            // if the HTML cache is not existent we should parse
216            // the XML in the main syntax component before the page loads
217            $cache_files = $doxycode_meta['html_cachefiles'];
218
219            foreach ($cache_files as $cacheID) {
220                $cache_name = getCacheName($cacheID, ".html");
221
222                if (!@file_exists($cache_name)) {
223                    $event->preventDefault();
224                    $event->stopPropagation();
225                    $event->result = false;
226                }
227
228                $this->addDependencies($cache, [$cache_name]);
229            }
230        }
231    }
232
233
234    /**
235     * Add extra dependencies to the cache
236     *
237     * copied from changes plugin
238     */
239    protected function addDependencies($cache, $depends)
240    {
241        // Prevent "Warning: in_array() expects parameter 2 to be array, null given"
242        if (!is_array($cache->depends)) {
243            $cache->depends = [];
244        }
245        if (!array_key_exists('files', $cache->depends)) {
246            $cache->depends['files'] = [];
247        }
248
249        foreach ($depends as $file) {
250            if (!in_array($file, $cache->depends['files']) && @file_exists($file)) {
251                $cache->depends['files'][] = $file;
252            }
253        }
254    }
255
256
257    /**
258     * handle ajax requests
259     */
260    public function ajaxCall(Event $event)
261    {
262        switch ($event->data) {
263            case 'plugin_doxycode_check_status':
264            case 'plugin_doxycode_get_snippet_html':
265            case 'plugin_doxycode_get_tag_files':
266                break;
267            default:
268                return;
269        }
270
271        // no other ajax call handlers needed
272        $event->stopPropagation();
273        $event->preventDefault();
274
275        if ($event->data === 'plugin_doxycode_check_status') {
276            // the main syntax component has put placeholders into the page while rendering
277            // the client tries to get the newest state for the doxygen builds executed through the buildmanager
278
279            /** @var helper_plugin_doxycode_buildmanager $buildmanager */
280            $buildmanager = plugin_load('helper', 'doxycode_buildmanager');
281
282            // the client sends us an array with the cache names (md5 hashes)
283            global $INPUT;
284            $hashes = $INPUT->arr('hashes');
285
286            // get the job state for each XML file
287            foreach ($hashes as &$hash) {
288                if (strlen($hash['xmlHash']) > 0) {
289                    $hash['state'] = $buildmanager->getJobState($hash['xmlHash']);
290                }
291            }
292
293            //set content type
294            header('Content-Type: application/json');
295            echo json_encode($hashes);
296
297            return;
298        } // plugin_doxycode_check_status
299
300        if ($event->data === 'plugin_doxycode_get_snippet_html') {
301            header('Content-Type: application/json');
302
303            // a client tries to dynamically load the rendered code snippet
304            global $INPUT;
305            $hashes = $INPUT->arr('hashes');
306
307            $xml_cacheID = $hashes['xmlHash'];
308            $html_cacheID = $hashes['htmlHash'];
309
310            /** @var helper_plugin_doxycode $helper */
311            $helper = plugin_load('helper', 'doxycode');
312            /** @var helper_plugin_doxycode_buildmanager $buildmanager */
313            $buildmanager = plugin_load('helper', 'doxycode_buildmanager');
314            /** @var helper_plugin_doxycode_parser $parser */
315            $parser = plugin_load('helper', 'doxycode_parser');
316            /** @var helper_plugin_doxycode_tagmanager $tagmanager */
317            $tagmanager = plugin_load('helper', 'doxycode_tagmanager');
318
319            // maybe the snippet was already rendered in the meantime (by another ajax call or through page reload)
320            $html_cache = new Cache($html_cacheID, '.html');
321            $xml_cache = new Cache($xml_cacheID, '.xml');
322
323            $task_config = $buildmanager->getJobTaskConf($xml_cacheID);
324            $tag_conf = $tagmanager->getFilteredTagConfig($task_config['tagfiles']);
325
326            $depends = [];
327            $helper->getHTMLFileDependencies($depends, $xml_cacheID, $tag_conf);
328
329            $data = [
330                'success' => false,
331                'hashes' => $hashes,
332                'html' => ''
333            ];
334
335            // how will we generate the dependencies?
336            if ($html_cache->useCache($depends)) {
337                // we have a valid HTML rendered!
338
339                if ($cachedContent = @file_get_contents($html_cache->cache)) {
340                    // add HTML cache to response
341
342                    $data['html'] = $cachedContent;
343                    $data['success'] = true;
344
345                    echo json_encode($data);
346                    return;
347                }
348            }
349
350            $job_config = $buildmanager->getJobConf($xml_cacheID);
351
352            $depends = [];
353            $helper->getXMLFileDependencies($depends, $tag_conf);
354            //set content type
355
356            if ($xml_cache->useCache($depends)) {
357                // we have a valid XML!
358
359                $xml_content = @file_get_contents($xml_cache->cache);
360
361                $rendered_text = $parser->renderXMLToDokuWikiCode($xml_content, $job_config['linenumbers'], $tag_conf);
362
363                // save content to cache
364                @file_put_contents($html_cache->cache, $rendered_text);
365
366                // add HTML to response
367                $data['html'] = $rendered_text;
368                $data['success'] = true;
369
370                echo json_encode($data);
371
372                return;
373            }
374
375            echo json_encode($data);
376            return;
377        } // plugin_doxycode_get_snippet_html
378
379        if ($event->data === 'plugin_doxycode_get_tag_files') {
380            // the client has requested a list of available tag file configurations
381
382            /** @var helper_plugin_doxycode_tagmanager $tagmanager */
383            $tagmanager = plugin_load('helper', 'doxycode_tagmanager');
384
385            // load the tag file configuration
386            $tag_config = $tagmanager->getFilteredTagConfig();
387
388            header('Content-Type: application/json');
389
390            // send data
391            echo json_encode($tag_config);
392
393            return;
394        }
395    }
396
397    public function insertTagButton(Event $event)
398    {
399        $event->data[] = array (
400            'type' => 'doxycodeTagSelector',
401            'title' => 'doxycode',
402            'icon' => DOKU_REL . 'lib/plugins/doxycode/images/toolbar/doxycode.png',
403            'open'   => '<doxycode>',
404            'close'  => '</doxycode>',
405            'block'  => false
406        );
407    }
408
409    public function addRpcMethod(&$event, $param)
410    {
411        $my_rpc_call = array(
412            'doxycode.listTagFiles' => array('doxycode', 'listTagFiles'),
413            'doxycode.uploadTagFile' => array('doxycode', 'uploadTagFile')
414        );
415        $event->data = array_merge($event->data, $my_rpc_call);
416    }
417}
418