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