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