1<?php
2
3/**
4 * Plugin RefNotes: Event handler
5 *
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author     Mykola Ostrovskyy <dwpforge@gmail.com>
8 */
9
10require_once(DOKU_PLUGIN . 'refnotes/core.php');
11require_once(DOKU_PLUGIN . 'refnotes/instructions.php');
12
13////////////////////////////////////////////////////////////////////////////////////////////////////
14class action_plugin_refnotes extends DokuWiki_Action_Plugin {
15    use refnotes_localization_plugin;
16
17    private $afterParserHandlerDone;
18    private $beforeAjaxCallUnknown;
19    private $beforeParserCacheUse;
20    private $beforeParserWikitextPreprocess;
21    private $beforeTplMetaheaderOutput;
22
23    /**
24     * Constructor
25     */
26    public function __construct() {
27        refnotes_localization::initialize($this);
28
29        $this->afterParserHandlerDone = new refnotes_after_parser_handler_done();
30        $this->beforeAjaxCallUnknown = new refnotes_before_ajax_call_unknown();
31        $this->beforeParserCacheUse = new refnotes_before_parser_cache_use();
32        $this->beforeParserWikitextPreprocess = new refnotes_before_parser_wikitext_preprocess();
33        $this->beforeTplMetaheaderOutput = new refnotes_before_tpl_metaheader_output();
34    }
35
36    /**
37     * Register callbacks
38     */
39    public function register(Doku_Event_Handler $controller) {
40        $this->afterParserHandlerDone->register($controller);
41        $this->beforeAjaxCallUnknown->register($controller);
42        $this->beforeParserCacheUse->register($controller);
43        $this->beforeParserWikitextPreprocess->register($controller);
44        $this->beforeTplMetaheaderOutput->register($controller);
45    }
46}
47
48////////////////////////////////////////////////////////////////////////////////////////////////////
49class refnotes_after_parser_handler_done {
50
51    /**
52     * Register callback
53     */
54    public function register($controller) {
55        $controller->register_hook('PARSER_HANDLER_DONE', 'AFTER', $this, 'handle');
56    }
57
58    /**
59     *
60     */
61    public function handle($event, $param) {
62        refnotes_parser_core::getInstance()->exitParsingContext($event->data);
63
64        /* We need a new instance of mangler for each event because we can trigger it recursively
65         * by loading reference database or by parsing structured notes.
66         */
67        $mangler = new refnotes_instruction_mangler($event);
68
69        $mangler->process();
70    }
71}
72
73////////////////////////////////////////////////////////////////////////////////////////////////////
74class refnotes_instruction_mangler {
75
76    private $core;
77    private $calls;
78    private $paragraphReferences;
79    private $referenceGroup;
80    private $hidden;
81    private $inReference;
82    private $includedPages;
83
84    /**
85     * Constructor
86     */
87    public function __construct($event) {
88        $this->core = new refnotes_action_core();
89        $this->calls = new refnotes_instruction_list($event);
90        $this->paragraphReferences = array();
91        $this->referenceGroup = array();
92        $this->hidden = true;
93        $this->inReference = false;
94        $this->includedPages = array();
95    }
96
97    /**
98     *
99     */
100    public function process() {
101        $this->scanInstructions();
102
103        /* If there are some includes on the current page, the implicit rendering of leftover notes
104         * has to be disabled inside the included pages. Instead the notes referred by the included
105         * pages have to be rendered on the current page. So even in case when the current page has
106         * no references, there has to be leftovers rendering at the end, just to ensure that any
107         * possible references on the included pages are taken care of.
108         */
109        if ($this->core->getNamespaceCount() > 0 || count($this->includedPages) > 0) {
110            $this->renderLeftovers();
111
112            $this->calls->applyChanges();
113        }
114
115        if ($this->core->getNamespaceCount() > 0) {
116            $this->insertNotesInstructions($this->core->getStyles(), 'refnotes_notes_style_instruction');
117            $this->insertNotesInstructions($this->core->getMappings(), 'refnotes_notes_map_instruction');
118
119            $this->calls->applyChanges();
120
121            $this->renderStructuredNotes();
122
123            $this->calls->applyChanges();
124        }
125    }
126
127    /**
128     *
129     */
130    private function scanInstructions() {
131        foreach ($this->calls as $call) {
132            $this->markHiddenReferences($call);
133            $this->markReferenceGroups($call);
134            $this->markScopeLimits($call);
135            $this->extractStyles($call);
136            $this->extractMappings($call);
137            $this->collectIncludedPages($call);
138        }
139    }
140
141    /**
142     *
143     */
144    private function markHiddenReferences($call) {
145        switch ($call->getName()) {
146            case 'p_open':
147                $this->paragraphReferences = array();
148                $this->hidden = true;
149                break;
150
151            case 'p_close':
152                if ($this->hidden) {
153                    foreach ($this->paragraphReferences as $call) {
154                        $call->setRefnotesAttribute('hidden', true);
155                    }
156                }
157                break;
158
159            case 'cdata':
160                if (!$this->inReference && !empty(trim($call->getData(0)))) {
161                    $this->hidden = false;
162                }
163                break;
164
165            case 'plugin_refnotes_references':
166                switch ($call->getPluginData(0)) {
167                    case 'start':
168                        $this->inReference = true;
169                        break;
170
171                    case 'render':
172                        $this->inReference = false;
173                        $this->paragraphReferences[] = $call;
174                        break;
175                }
176                break;
177
178            default:
179                if (!$this->inReference) {
180                    $this->hidden = false;
181                }
182                break;
183        }
184    }
185
186    /**
187     *
188     */
189    private function markReferenceGroups($call) {
190        if (($call->getName() == 'plugin_refnotes_references') && ($call->getPluginData(0) == 'render')) {
191            if (!empty($this->referenceGroup)) {
192                $groupNamespace = $this->referenceGroup[0]->getRefnotesAttribute('ns');
193
194                if ($call->getRefnotesAttribute('ns') != $groupNamespace) {
195                    $this->closeReferenceGroup();
196                }
197            }
198
199            $this->referenceGroup[] = $call;
200        }
201        elseif (!$this->inReference && !empty($this->referenceGroup)) {
202            // Allow whitespace "cdata" istructions between references in a group
203            if ($call->getName() == 'cdata' && empty(trim($call->getData(0)))) {
204                return;
205            }
206
207            $this->closeReferenceGroup();
208        }
209    }
210
211    /**
212     *
213     */
214    private function closeReferenceGroup() {
215        $count = count($this->referenceGroup);
216
217        if ($count > 1) {
218            $this->referenceGroup[0]->setRefnotesAttribute('group', 'open');
219
220            for ($i = 1; $i < $count - 1; $i++) {
221                $this->referenceGroup[$i]->setRefnotesAttribute('group', 'hold');
222            }
223
224            $this->referenceGroup[$count - 1]->setRefnotesAttribute('group', 'close');
225        }
226
227        $this->referenceGroup = array();
228    }
229
230    /**
231     *
232     */
233    private function markScopeLimits($call) {
234        switch ($call->getName()) {
235            case 'plugin_refnotes_references':
236                if ($call->getPluginData(0) == 'render') {
237                    $this->core->markScopeStart($call->getRefnotesAttribute('ns'), $call->getIndex());
238                }
239                break;
240
241            case 'plugin_refnotes_notes':
242                $this->core->markScopeEnd($call->getRefnotesAttribute('ns'), $call->getIndex());
243                break;
244        }
245    }
246
247    /**
248     * Extract style data and replace "split" instructions with "render"
249     */
250    private function extractStyles($call) {
251        if (($call->getName() == 'plugin_refnotes_notes') && ($call->getPluginData(0) == 'split')) {
252            $this->core->addStyle($call->getRefnotesAttribute('ns'), $call->getPluginData(2));
253
254            $call->setPluginData(0, 'render');
255            $call->unsetPluginData(2);
256        }
257    }
258
259    /**
260     * Extract namespace mapping info
261     */
262    private function extractMappings($call) {
263        if ($call->getName() == 'plugin_refnotes_notes') {
264            $map = $call->getRefnotesAttribute('map');
265
266            if (!empty($map)) {
267                $this->core->addMapping($call->getRefnotesAttribute('ns'), $map);
268                $call->unsetRefnotesAttribute('map');
269            }
270        }
271    }
272
273    /**
274     *
275     */
276    private function collectIncludedPages($call) {
277        if ($call->getName() == 'plugin_include_include') {
278            $this->includedPages[] = $call;
279        }
280    }
281
282    /**
283     *
284     */
285    private function insertNotesInstructions($stash, $instruction) {
286        if ($stash->getCount() == 0) {
287            return;
288        }
289
290        $stash->sort();
291
292        foreach ($stash->getIndex() as $index) {
293            foreach ($stash->getAt($index) as $data) {
294                $this->calls->insert($index, new $instruction($data->getNamespace(), $data->getData()));
295            }
296        }
297    }
298
299    /**
300     * Insert render call at the very bottom of the page
301     */
302    private function renderLeftovers() {
303        /* Block leftovers rendering on the included pages */
304        foreach ($this->includedPages as $call) {
305            $call->insertBefore(new refnotes_notes_render_block_instruction('enter'));
306            $call->insertAfter(new refnotes_notes_render_block_instruction('exit'));
307        }
308
309        $this->calls->append(new refnotes_notes_render_instruction('*'));
310    }
311
312    /**
313     *
314     */
315    private function renderStructuredNotes() {
316        $this->core->reset();
317
318        foreach ($this->calls as $call) {
319            $this->styleNamespaces($call);
320            $this->setNamespaceMappings($call);
321            $this->addReferences($call);
322            $this->rewriteReferences($call);
323        }
324    }
325
326    /**
327     *
328     */
329    private function styleNamespaces($call) {
330        if (($call->getName() == 'plugin_refnotes_notes') && ($call->getPluginData(0) == 'style')) {
331            $this->core->styleNamespace($call->getRefnotesAttribute('ns'), $call->getPluginData(2));
332        }
333    }
334
335    /**
336     *
337     */
338    private function setNamespaceMappings($call) {
339        if (($call->getName() == 'plugin_refnotes_notes') && ($call->getPluginData(0) == 'map')) {
340            $this->core->setNamespaceMapping($call->getRefnotesAttribute('ns'), $call->getPluginData(2));
341        }
342    }
343
344    /**
345     *
346     */
347    private function addReferences($call) {
348        if (($call->getName() == 'plugin_refnotes_references') && ($call->getPluginData(0) == 'render')) {
349            $attributes = $call->getPluginData(1);
350            $data = (count($call->getData(1)) > 2) ? $call->getPluginData(2) : array();
351            $reference = $this->core->addReference($attributes, $data, $call);
352
353            if ($call->getPrevious()->getName() != 'plugin_refnotes_references') {
354                $reference->getNote()->setText('defined');
355            }
356        }
357    }
358
359    /**
360     *
361     */
362    private function rewriteReferences($call) {
363        if (($call->getName() == 'plugin_refnotes_notes') && ($call->getPluginData(0) == 'render')) {
364            $this->core->rewriteReferences($call->getRefnotesAttribute('ns'), $call->getRefnotesAttribute('limit'));
365        }
366    }
367}
368
369////////////////////////////////////////////////////////////////////////////////////////////////////
370class refnotes_before_ajax_call_unknown {
371
372    /**
373     * Register callback
374     */
375    public function register($controller) {
376        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle');
377    }
378
379    /**
380     *
381     */
382    public function handle($event, $param) {
383        global $conf;
384
385        if ($event->data == 'refnotes-admin') {
386            $event->preventDefault();
387            $event->stopPropagation();
388
389            /* Check admin rights */
390            if (auth_quickaclcheck($conf['start']) < AUTH_ADMIN) {
391                die('access denied');
392            }
393
394            switch ($_POST['action']) {
395                case 'load-settings':
396                    $this->sendConfig();
397                    break;
398
399                case 'save-settings':
400                    $this->saveConfig($_POST['settings']);
401                    break;
402            }
403        }
404    }
405
406    /**
407     *
408     */
409    private function sendResponse($contentType, $data) {
410        static $cookie = '{B27067E9-3DDA-4E31-9768-E66F23D18F4A}';
411
412        header('Content-Type: ' . $contentType);
413        print($cookie . $data . $cookie);
414    }
415
416    /**
417     *
418     */
419    private function sendConfig() {
420        $namespace = refnotes_configuration::load('namespaces');
421        $namespace = $this->translateStyles($namespace, 'dw', 'js');
422
423        $config['general'] = refnotes_configuration::load('general');
424        $config['namespaces'] = $namespace;
425        $config['notes'] = refnotes_configuration::load('notes');
426
427        $this->sendResponse('application/x-suggestions+json', json_encode($config));
428    }
429
430    /**
431     *
432     */
433    private function saveConfig($config) {
434        global $config_cascade;
435
436        $config = json_decode($config, true);
437
438        $namespace = $config['namespaces'];
439        $namespace = $this->translateStyles($namespace, 'js', 'dw');
440
441        $saved = refnotes_configuration::save('general', $config['general']);
442        $saved = $saved && refnotes_configuration::save('namespaces', $namespace);
443        $saved = $saved && refnotes_configuration::save('notes', $config['notes']);
444
445        if ($config['general']['reference-db-enable']) {
446            $saved = $saved && $this->setupReferenceDatabase($config['general']['reference-db-namespace']);
447        }
448
449        /* Touch local config file to expire the cache */
450        $saved = $saved && touch(reset($config_cascade['main']['local']));
451
452        $this->sendResponse('text/plain', $saved ? 'saved' : 'failed');
453    }
454
455    /**
456     *
457     */
458    private function translateStyles($namespace, $from, $to) {
459        foreach ($namespace as &$ns) {
460            foreach ($ns as $styleName => &$style) {
461                $style = $this->translateStyle($styleName, $style, $from, $to);
462            }
463        }
464
465        return $namespace;
466    }
467
468    /**
469     *
470     */
471    private function translateStyle($styleName, $style, $from, $to) {
472        static $dictionary = array(
473            'refnote-id' => array(
474                'dw' => array('1'      , 'a'          , 'A'          , 'i'          , 'I'          , '*'    , 'name'     ),
475                'js' => array('numeric', 'latin-lower', 'latin-upper', 'roman-lower', 'roman-upper', 'stars', 'note-name')
476            ),
477            'reference-base' => array(
478                'dw' => array('sup'  , 'text'       ),
479                'js' => array('super', 'normal-text')
480            ),
481            'reference-format' => array(
482                'dw' => array(')'           , '()'     , ']'            , '[]'      ),
483                'js' => array('right-parent', 'parents', 'right-bracket', 'brackets')
484            ),
485            'reference-group' => array(
486                'dw' => array('none'      , ','          , 's'              ),
487                'js' => array('group-none', 'group-comma', 'group-semicolon')
488            ),
489            'multi-ref-id' => array(
490                'dw' => array('ref'        , 'note'   ),
491                'js' => array('ref-counter', 'note-counter')
492            ),
493            'note-id-base' => array(
494                'dw' => array('sup'  , 'text'       ),
495                'js' => array('super', 'normal-text')
496            ),
497            'note-id-format' => array(
498                'dw' => array(')'           , '()'     , ']'            , '[]'      , '.'  ),
499                'js' => array('right-parent', 'parents', 'right-bracket', 'brackets', 'dot')
500            ),
501            'back-ref-base' => array(
502                'dw' => array('sup'  , 'text'       ),
503                'js' => array('super', 'normal-text')
504            ),
505            'back-ref-format' => array(
506                'dw' => array('1'      , 'a'    , 'note'   ),
507                'js' => array('numeric', 'latin', 'note-id')
508            ),
509            'back-ref-separator' => array(
510                'dw' => array(','    ),
511                'js' => array('comma')
512            ),
513            'struct-refs' => array(
514                'dw' => array('off'    , 'on'    ),
515                'js' => array('disable', 'enable')
516            )
517        );
518
519        if (array_key_exists($styleName, $dictionary)) {
520            $key = array_search($style, $dictionary[$styleName][$from]);
521
522            if ($key !== false) {
523                $style = $dictionary[$styleName][$to][$key];
524            }
525        }
526
527        return $style;
528    }
529
530    /**
531     *
532     */
533    private function setupReferenceDatabase($namespace) {
534        $success = true;
535        $source = refnotes_localization::getInstance()->getFileName('__template');
536        $destination = wikiFN(cleanID($namespace . ':template'));
537        $destination = preg_replace('/template.txt$/', '__template.txt', $destination);
538
539        if (@filemtime($destination) < @filemtime($source)) {
540            if (!file_exists(dirname($destination))) {
541                @mkdir(dirname($destination), 0755, true);
542            }
543
544            $success = copy($source, $destination);
545
546            touch($destination, filemtime($source));
547        }
548
549        return $success;
550    }
551}
552
553////////////////////////////////////////////////////////////////////////////////////////////////////
554class refnotes_before_parser_cache_use {
555
556    /**
557     * Register callback
558     */
559    public function register($controller) {
560        $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'handle');
561    }
562
563    /**
564     *
565     */
566    public function handle($event, $param) {
567        global $ID;
568
569        $cache = $event->data;
570
571        if (isset($cache->page) && ($cache->page == $ID)) {
572            if (isset($cache->mode) && (($cache->mode == 'xhtml') || ($cache->mode == 'i'))) {
573                $meta = p_get_metadata($ID, 'plugin refnotes');
574
575                if (!empty($meta) && isset($meta['dbref'])) {
576                    $this->addDependencies($cache, array_keys($meta['dbref']));
577                }
578            }
579        }
580    }
581
582    /**
583     * Add extra dependencies to the cache
584     */
585    private function addDependencies($cache, $depends) {
586        foreach ($depends as $file) {
587            if (!in_array($file, $cache->depends['files']) && file_exists($file)) {
588                $cache->depends['files'][] = $file;
589            }
590        }
591    }
592}
593
594////////////////////////////////////////////////////////////////////////////////////////////////////
595class refnotes_before_parser_wikitext_preprocess {
596
597    /**
598     * Register callback
599     */
600    public function register($controller) {
601        $controller->register_hook('PARSER_WIKITEXT_PREPROCESS', 'BEFORE', $this, 'handle');
602    }
603
604    /**
605     *
606     */
607    public function handle($event, $param) {
608        refnotes_parser_core::getInstance()->enterParsingContext();
609    }
610}
611
612////////////////////////////////////////////////////////////////////////////////////////////////////
613class refnotes_before_tpl_metaheader_output {
614
615    /**
616     * Register callback
617     */
618    public function register($controller) {
619        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'handle');
620    }
621
622    /**
623     *
624     */
625    public function handle($event, $param) {
626        if (!empty($_REQUEST['do']) && $_REQUEST['do'] == 'admin' &&
627                !empty($_REQUEST['page']) && $_REQUEST['page'] == 'refnotes') {
628            $this->addAdminIncludes($event);
629        }
630    }
631
632    /**
633     *
634     */
635    private function addAdminIncludes($event) {
636        $this->addTemplateHeaderInclude($event, 'admin.js');
637        $this->addTemplateHeaderInclude($event, 'admin.css');
638    }
639
640    /**
641     *
642     */
643    private function addTemplateHeaderInclude($event, $fileName) {
644        $type = '';
645        $fileName = DOKU_BASE . 'lib/plugins/refnotes/' . $fileName;
646
647        switch (pathinfo($fileName, PATHINFO_EXTENSION)) {
648            case 'js':
649                $type = 'script';
650                $data = array('type' => 'text/javascript', 'charset' => 'utf-8', 'src' => $fileName, '_data' => '', 'defer' => 'defer');
651                break;
652
653            case 'css':
654                $type = 'link';
655                $data = array('type' => 'text/css', 'rel' => 'stylesheet', 'href' => $fileName);
656                break;
657        }
658
659        if ($type != '') {
660            $event->data[$type][] = $data;
661        }
662    }
663}
664