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