1<?php 2/** 3 * DokuWiki Actions 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <andi@splitbrain.org> 7 */ 8 9if(!defined('DOKU_INC')) die('meh.'); 10 11/** 12 * Call the needed action handlers 13 * 14 * @author Andreas Gohr <andi@splitbrain.org> 15 * @triggers ACTION_ACT_PREPROCESS 16 * @triggers ACTION_HEADERS_SEND 17 */ 18function act_dispatch(){ 19 global $ACT; 20 global $ID; 21 global $INFO; 22 global $QUERY; 23 global $INPUT; 24 global $lang; 25 global $conf; 26 27 $preact = $ACT; 28 29 // give plugins an opportunity to process the action 30 $evt = new Doku_Event('ACTION_ACT_PREPROCESS',$ACT); 31 if ($evt->advise_before()) { 32 33 //sanitize $ACT 34 $ACT = act_validate($ACT); 35 36 //check if searchword was given - else just show 37 $s = cleanID($QUERY); 38 if($ACT == 'search' && empty($s)){ 39 $ACT = 'show'; 40 } 41 42 //login stuff 43 if(in_array($ACT,array('login','logout'))){ 44 $ACT = act_auth($ACT); 45 } 46 47 //check if user is asking to (un)subscribe a page 48 if($ACT == 'subscribe') { 49 try { 50 $ACT = act_subscription($ACT); 51 } catch (Exception $e) { 52 msg($e->getMessage(), -1); 53 } 54 } 55 56 //display some infos 57 if($ACT == 'check'){ 58 check(); 59 $ACT = 'show'; 60 } 61 62 //check permissions 63 $ACT = act_permcheck($ACT); 64 65 //sitemap 66 if ($ACT == 'sitemap'){ 67 act_sitemap($ACT); 68 } 69 70 //recent changes 71 if ($ACT == 'recent'){ 72 $show_changes = $INPUT->str('show_changes'); 73 if (!empty($show_changes)) { 74 set_doku_pref('show_changes', $show_changes); 75 } 76 } 77 78 //diff 79 if ($ACT == 'diff'){ 80 $difftype = $INPUT->str('difftype'); 81 if (!empty($difftype)) { 82 set_doku_pref('difftype', $difftype); 83 } 84 } 85 86 //register 87 if($ACT == 'register' && $INPUT->post->bool('save') && register()){ 88 $ACT = 'login'; 89 } 90 91 if ($ACT == 'resendpwd' && act_resendpwd()) { 92 $ACT = 'login'; 93 } 94 95 //update user profile 96 if ($ACT == 'profile') { 97 if(!$_SERVER['REMOTE_USER']) { 98 $ACT = 'login'; 99 } else { 100 if(updateprofile()) { 101 msg($lang['profchanged'],1); 102 $ACT = 'show'; 103 } 104 } 105 } 106 107 //revert 108 if($ACT == 'revert'){ 109 if(checkSecurityToken()){ 110 $ACT = act_revert($ACT); 111 }else{ 112 $ACT = 'show'; 113 } 114 } 115 116 //save 117 if($ACT == 'save'){ 118 if(checkSecurityToken()){ 119 $ACT = act_save($ACT); 120 }else{ 121 $ACT = 'preview'; 122 } 123 } 124 125 //cancel conflicting edit 126 if($ACT == 'cancel') 127 $ACT = 'show'; 128 129 //draft deletion 130 if($ACT == 'draftdel') 131 $ACT = act_draftdel($ACT); 132 133 //draft saving on preview 134 if($ACT == 'preview') 135 $ACT = act_draftsave($ACT); 136 137 //edit 138 if(in_array($ACT, array('edit', 'preview', 'recover'))) { 139 $ACT = act_edit($ACT); 140 }else{ 141 unlock($ID); //try to unlock 142 } 143 144 //handle export 145 if(substr($ACT,0,7) == 'export_') 146 $ACT = act_export($ACT); 147 148 //handle admin tasks 149 if($ACT == 'admin'){ 150 // retrieve admin plugin name from $_REQUEST['page'] 151 if (($page = $INPUT->str('page', '', true)) != '') { 152 $pluginlist = plugin_list('admin'); 153 if (in_array($page, $pluginlist)) { 154 // attempt to load the plugin 155 if ($plugin =& plugin_load('admin',$page) !== null){ 156 /** @var DokuWiki_Admin_Plugin $plugin */ 157 if($plugin->forAdminOnly() && !$INFO['isadmin']){ 158 // a manager tried to load a plugin that's for admins only 159 $INPUT->remove('page'); 160 msg('For admins only',-1); 161 }else{ 162 $plugin->handle(); 163 } 164 } 165 } 166 } 167 } 168 169 // check permissions again - the action may have changed 170 $ACT = act_permcheck($ACT); 171 } // end event ACTION_ACT_PREPROCESS default action 172 $evt->advise_after(); 173 // Make sure plugs can handle 'denied' 174 if($conf['send404'] && $ACT == 'denied') { 175 header('HTTP/1.0 403 Forbidden'); 176 } 177 unset($evt); 178 179 // when action 'show', the intial not 'show' and POST, do a redirect 180 if($ACT == 'show' && $preact != 'show' && strtolower($_SERVER['REQUEST_METHOD']) == 'post'){ 181 act_redirect($ID,$preact); 182 } 183 184 global $INFO; 185 global $conf; 186 global $license; 187 188 //call template FIXME: all needed vars available? 189 $headers[] = 'Content-Type: text/html; charset=utf-8'; 190 trigger_event('ACTION_HEADERS_SEND',$headers,'act_sendheaders'); 191 192 include(template('main.php')); 193 // output for the commands is now handled in inc/templates.php 194 // in function tpl_content() 195} 196 197/** 198 * Send the given headers using header() 199 * 200 * @param array $headers The headers that shall be sent 201 */ 202function act_sendheaders($headers) { 203 foreach ($headers as $hdr) header($hdr); 204} 205 206/** 207 * Sanitize the action command 208 * 209 * @author Andreas Gohr <andi@splitbrain.org> 210 */ 211function act_clean($act){ 212 // check if the action was given as array key 213 if(is_array($act)){ 214 list($act) = array_keys($act); 215 } 216 217 //remove all bad chars 218 $act = strtolower($act); 219 $act = preg_replace('/[^1-9a-z_]+/','',$act); 220 221 if($act == 'export_html') $act = 'export_xhtml'; 222 if($act == 'export_htmlbody') $act = 'export_xhtmlbody'; 223 224 if($act === '') $act = 'show'; 225 return $act; 226} 227 228/** 229 * Sanitize and validate action commands. 230 * 231 * Add all allowed commands here. 232 * 233 * @author Andreas Gohr <andi@splitbrain.org> 234 */ 235function act_validate($act) { 236 global $conf; 237 global $INFO; 238 239 $act = act_clean($act); 240 241 // check if action is disabled 242 if(!actionOK($act)){ 243 msg('Command disabled: '.htmlspecialchars($act),-1); 244 return 'show'; 245 } 246 247 //disable all acl related commands if ACL is disabled 248 if(!$conf['useacl'] && in_array($act,array('login','logout','register','admin', 249 'subscribe','unsubscribe','profile','revert', 250 'resendpwd'))){ 251 msg('Command unavailable: '.htmlspecialchars($act),-1); 252 return 'show'; 253 } 254 255 //is there really a draft? 256 if($act == 'draft' && !file_exists($INFO['draft'])) return 'edit'; 257 258 if(!in_array($act,array('login','logout','register','save','cancel','edit','draft', 259 'preview','search','show','check','index','revisions', 260 'diff','recent','backlink','admin','subscribe','revert', 261 'unsubscribe','profile','resendpwd','recover', 262 'draftdel','sitemap','media')) && substr($act,0,7) != 'export_' ) { 263 msg('Command unknown: '.htmlspecialchars($act),-1); 264 return 'show'; 265 } 266 return $act; 267} 268 269/** 270 * Run permissionchecks 271 * 272 * @author Andreas Gohr <andi@splitbrain.org> 273 */ 274function act_permcheck($act){ 275 global $INFO; 276 global $conf; 277 278 if(in_array($act,array('save','preview','edit','recover'))){ 279 if($INFO['exists']){ 280 if($act == 'edit'){ 281 //the edit function will check again and do a source show 282 //when no AUTH_EDIT available 283 $permneed = AUTH_READ; 284 }else{ 285 $permneed = AUTH_EDIT; 286 } 287 }else{ 288 $permneed = AUTH_CREATE; 289 } 290 }elseif(in_array($act,array('login','search','recent','profile','index', 'sitemap'))){ 291 $permneed = AUTH_NONE; 292 }elseif($act == 'revert'){ 293 $permneed = AUTH_ADMIN; 294 if($INFO['ismanager']) $permneed = AUTH_EDIT; 295 }elseif($act == 'register'){ 296 $permneed = AUTH_NONE; 297 }elseif($act == 'resendpwd'){ 298 $permneed = AUTH_NONE; 299 }elseif($act == 'admin'){ 300 if($INFO['ismanager']){ 301 // if the manager has the needed permissions for a certain admin 302 // action is checked later 303 $permneed = AUTH_READ; 304 }else{ 305 $permneed = AUTH_ADMIN; 306 } 307 }else{ 308 $permneed = AUTH_READ; 309 } 310 if($INFO['perm'] >= $permneed) return $act; 311 312 return 'denied'; 313} 314 315/** 316 * Handle 'draftdel' 317 * 318 * Deletes the draft for the current page and user 319 */ 320function act_draftdel($act){ 321 global $INFO; 322 @unlink($INFO['draft']); 323 $INFO['draft'] = null; 324 return 'show'; 325} 326 327/** 328 * Saves a draft on preview 329 * 330 * @todo this currently duplicates code from ajax.php :-/ 331 */ 332function act_draftsave($act){ 333 global $INFO; 334 global $ID; 335 global $INPUT; 336 global $conf; 337 if($conf['usedraft'] && $INPUT->post->has('wikitext')) { 338 $draft = array('id' => $ID, 339 'prefix' => substr($INPUT->post->str('prefix'), 0, -1), 340 'text' => $INPUT->post->str('wikitext'), 341 'suffix' => $INPUT->post->str('suffix'), 342 'date' => $INPUT->post->int('date'), 343 'client' => $INFO['client'], 344 ); 345 $cname = getCacheName($draft['client'].$ID,'.draft'); 346 if(io_saveFile($cname,serialize($draft))){ 347 $INFO['draft'] = $cname; 348 } 349 } 350 return $act; 351} 352 353/** 354 * Handle 'save' 355 * 356 * Checks for spam and conflicts and saves the page. 357 * Does a redirect to show the page afterwards or 358 * returns a new action. 359 * 360 * @author Andreas Gohr <andi@splitbrain.org> 361 */ 362function act_save($act){ 363 global $ID; 364 global $DATE; 365 global $PRE; 366 global $TEXT; 367 global $SUF; 368 global $SUM; 369 global $lang; 370 global $INFO; 371 global $INPUT; 372 373 //spam check 374 if(checkwordblock()) { 375 msg($lang['wordblock'], -1); 376 return 'edit'; 377 } 378 //conflict check 379 if($DATE != 0 && $INFO['meta']['date']['modified'] > $DATE ) 380 return 'conflict'; 381 382 //save it 383 saveWikiText($ID,con($PRE,$TEXT,$SUF,1),$SUM,$INPUT->bool('minor')); //use pretty mode for con 384 //unlock it 385 unlock($ID); 386 387 //delete draft 388 act_draftdel($act); 389 session_write_close(); 390 391 // when done, show page 392 return 'show'; 393} 394 395/** 396 * Revert to a certain revision 397 * 398 * @author Andreas Gohr <andi@splitbrain.org> 399 */ 400function act_revert($act){ 401 global $ID; 402 global $REV; 403 global $lang; 404 // FIXME $INFO['writable'] currently refers to the attic version 405 // global $INFO; 406 // if (!$INFO['writable']) { 407 // return 'show'; 408 // } 409 410 // when no revision is given, delete current one 411 // FIXME this feature is not exposed in the GUI currently 412 $text = ''; 413 $sum = $lang['deleted']; 414 if($REV){ 415 $text = rawWiki($ID,$REV); 416 if(!$text) return 'show'; //something went wrong 417 $sum = sprintf($lang['restored'], dformat($REV)); 418 } 419 420 // spam check 421 422 if (checkwordblock($text)) { 423 msg($lang['wordblock'], -1); 424 return 'edit'; 425 } 426 427 saveWikiText($ID,$text,$sum,false); 428 msg($sum,1); 429 430 //delete any draft 431 act_draftdel($act); 432 session_write_close(); 433 434 // when done, show current page 435 $_SERVER['REQUEST_METHOD'] = 'post'; //should force a redirect 436 $REV = ''; 437 return 'show'; 438} 439 440/** 441 * Do a redirect after receiving post data 442 * 443 * Tries to add the section id as hash mark after section editing 444 */ 445function act_redirect($id,$preact){ 446 global $PRE; 447 global $TEXT; 448 449 $opts = array( 450 'id' => $id, 451 'preact' => $preact 452 ); 453 //get section name when coming from section edit 454 if($PRE && preg_match('/^\s*==+([^=\n]+)/',$TEXT,$match)){ 455 $check = false; //Byref 456 $opts['fragment'] = sectionID($match[0], $check); 457 } 458 459 trigger_event('ACTION_SHOW_REDIRECT',$opts,'act_redirect_execute'); 460} 461 462/** 463 * Execute the redirect 464 * 465 * @param array $opts id and fragment for the redirect 466 */ 467function act_redirect_execute($opts){ 468 $go = wl($opts['id'],'',true); 469 if(isset($opts['fragment'])) $go .= '#'.$opts['fragment']; 470 471 //show it 472 send_redirect($go); 473} 474 475/** 476 * Handle 'login', 'logout' 477 * 478 * @author Andreas Gohr <andi@splitbrain.org> 479 */ 480function act_auth($act){ 481 global $ID; 482 global $INFO; 483 484 //already logged in? 485 if(isset($_SERVER['REMOTE_USER']) && $act=='login'){ 486 return 'show'; 487 } 488 489 //handle logout 490 if($act=='logout'){ 491 $lockedby = checklock($ID); //page still locked? 492 if($lockedby == $_SERVER['REMOTE_USER']) 493 unlock($ID); //try to unlock 494 495 // do the logout stuff 496 auth_logoff(); 497 498 // rebuild info array 499 $INFO = pageinfo(); 500 501 act_redirect($ID,'login'); 502 } 503 504 return $act; 505} 506 507/** 508 * Handle 'edit', 'preview', 'recover' 509 * 510 * @author Andreas Gohr <andi@splitbrain.org> 511 */ 512function act_edit($act){ 513 global $ID; 514 global $INFO; 515 516 global $TEXT; 517 global $RANGE; 518 global $PRE; 519 global $SUF; 520 global $REV; 521 global $SUM; 522 global $lang; 523 global $DATE; 524 525 if (!isset($TEXT)) { 526 if ($INFO['exists']) { 527 if ($RANGE) { 528 list($PRE,$TEXT,$SUF) = rawWikiSlices($RANGE,$ID,$REV); 529 } else { 530 $TEXT = rawWiki($ID,$REV); 531 } 532 } else { 533 $TEXT = pageTemplate($ID); 534 } 535 } 536 537 //set summary default 538 if(!$SUM){ 539 if($REV){ 540 $SUM = sprintf($lang['restored'], dformat($REV)); 541 }elseif(!$INFO['exists']){ 542 $SUM = $lang['created']; 543 } 544 } 545 546 // Use the date of the newest revision, not of the revision we edit 547 // This is used for conflict detection 548 if(!$DATE) $DATE = @filemtime(wikiFN($ID)); 549 550 //check if locked by anyone - if not lock for my self 551 //do not lock when the user can't edit anyway 552 if ($INFO['writable']) { 553 $lockedby = checklock($ID); 554 if($lockedby) return 'locked'; 555 556 lock($ID); 557 } 558 559 return $act; 560} 561 562/** 563 * Export a wiki page for various formats 564 * 565 * Triggers ACTION_EXPORT_POSTPROCESS 566 * 567 * Event data: 568 * data['id'] -- page id 569 * data['mode'] -- requested export mode 570 * data['headers'] -- export headers 571 * data['output'] -- export output 572 * 573 * @author Andreas Gohr <andi@splitbrain.org> 574 * @author Michael Klier <chi@chimeric.de> 575 */ 576function act_export($act){ 577 global $ID; 578 global $REV; 579 global $conf; 580 global $lang; 581 582 $pre = ''; 583 $post = ''; 584 $output = ''; 585 $headers = array(); 586 587 // search engines: never cache exported docs! (Google only currently) 588 $headers['X-Robots-Tag'] = 'noindex'; 589 590 $mode = substr($act,7); 591 switch($mode) { 592 case 'raw': 593 $headers['Content-Type'] = 'text/plain; charset=utf-8'; 594 $headers['Content-Disposition'] = 'attachment; filename='.noNS($ID).'.txt'; 595 $output = rawWiki($ID,$REV); 596 break; 597 case 'xhtml': 598 $pre .= '<!DOCTYPE html>' . DOKU_LF; 599 $pre .= '<html lang="'.$conf['lang'].'" dir="'.$lang['direction'].'">' . DOKU_LF; 600 $pre .= '<head>' . DOKU_LF; 601 $pre .= ' <meta charset="utf-8" />' . DOKU_LF; 602 $pre .= ' <title>'.$ID.'</title>' . DOKU_LF; 603 604 // get metaheaders 605 ob_start(); 606 tpl_metaheaders(); 607 $pre .= ob_get_clean(); 608 609 $pre .= '</head>' . DOKU_LF; 610 $pre .= '<body>' . DOKU_LF; 611 $pre .= '<div class="dokuwiki export">' . DOKU_LF; 612 613 // get toc 614 $pre .= tpl_toc(true); 615 616 $headers['Content-Type'] = 'text/html; charset=utf-8'; 617 $output = p_wiki_xhtml($ID,$REV,false); 618 619 $post .= '</div>' . DOKU_LF; 620 $post .= '</body>' . DOKU_LF; 621 $post .= '</html>' . DOKU_LF; 622 break; 623 case 'xhtmlbody': 624 $headers['Content-Type'] = 'text/html; charset=utf-8'; 625 $output = p_wiki_xhtml($ID,$REV,false); 626 break; 627 default: 628 $output = p_cached_output(wikiFN($ID,$REV), $mode); 629 $headers = p_get_metadata($ID,"format $mode"); 630 break; 631 } 632 633 // prepare event data 634 $data = array(); 635 $data['id'] = $ID; 636 $data['mode'] = $mode; 637 $data['headers'] = $headers; 638 $data['output'] =& $output; 639 640 trigger_event('ACTION_EXPORT_POSTPROCESS', $data); 641 642 if(!empty($data['output'])){ 643 if(is_array($data['headers'])) foreach($data['headers'] as $key => $val){ 644 header("$key: $val"); 645 } 646 print $pre.$data['output'].$post; 647 exit; 648 } 649 return 'show'; 650} 651 652/** 653 * Handle sitemap delivery 654 * 655 * @author Michael Hamann <michael@content-space.de> 656 */ 657function act_sitemap($act) { 658 global $conf; 659 660 if ($conf['sitemap'] < 1 || !is_numeric($conf['sitemap'])) { 661 header("HTTP/1.0 404 Not Found"); 662 print "Sitemap generation is disabled."; 663 exit; 664 } 665 666 $sitemap = Sitemapper::getFilePath(); 667 if (Sitemapper::sitemapIsCompressed()) { 668 $mime = 'application/x-gzip'; 669 }else{ 670 $mime = 'application/xml; charset=utf-8'; 671 } 672 673 // Check if sitemap file exists, otherwise create it 674 if (!is_readable($sitemap)) { 675 Sitemapper::generate(); 676 } 677 678 if (is_readable($sitemap)) { 679 // Send headers 680 header('Content-Type: '.$mime); 681 header('Content-Disposition: attachment; filename='.utf8_basename($sitemap)); 682 683 http_conditionalRequest(filemtime($sitemap)); 684 685 // Send file 686 //use x-sendfile header to pass the delivery to compatible webservers 687 if (http_sendfile($sitemap)) exit; 688 689 readfile($sitemap); 690 exit; 691 } 692 693 header("HTTP/1.0 500 Internal Server Error"); 694 print "Could not read the sitemap file - bad permissions?"; 695 exit; 696} 697 698/** 699 * Handle page 'subscribe' 700 * 701 * Throws exception on error. 702 * 703 * @author Adrian Lang <lang@cosmocode.de> 704 */ 705function act_subscription($act){ 706 global $lang; 707 global $INFO; 708 global $ID; 709 global $INPUT; 710 711 // subcriptions work for logged in users only 712 if(!$_SERVER['REMOTE_USER']) return 'show'; 713 714 // get and preprocess data. 715 $params = array(); 716 foreach(array('target', 'style', 'action') as $param) { 717 if ($INPUT->has("sub_$param")) { 718 $params[$param] = $INPUT->str("sub_$param"); 719 } 720 } 721 722 // any action given? if not just return and show the subscription page 723 if(!$params['action'] || !checkSecurityToken()) return $act; 724 725 // Handle POST data, may throw exception. 726 trigger_event('ACTION_HANDLE_SUBSCRIBE', $params, 'subscription_handle_post'); 727 728 $target = $params['target']; 729 $style = $params['style']; 730 $action = $params['action']; 731 732 // Perform action. 733 $sub = new Subscription(); 734 if($action == 'unsubscribe'){ 735 $ok = $sub->remove($target, $_SERVER['REMOTE_USER'], $style); 736 }else{ 737 $ok = $sub->add($target, $_SERVER['REMOTE_USER'], $style); 738 } 739 740 if($ok) { 741 msg(sprintf($lang["subscr_{$action}_success"], hsc($INFO['userinfo']['name']), 742 prettyprint_id($target)), 1); 743 act_redirect($ID, $act); 744 } else { 745 throw new Exception(sprintf($lang["subscr_{$action}_error"], 746 hsc($INFO['userinfo']['name']), 747 prettyprint_id($target))); 748 } 749 750 // Assure that we have valid data if act_redirect somehow fails. 751 $INFO['subscribed'] = $sub->user_subscription(); 752 return 'show'; 753} 754 755/** 756 * Validate POST data 757 * 758 * Validates POST data for a subscribe or unsubscribe request. This is the 759 * default action for the event ACTION_HANDLE_SUBSCRIBE. 760 * 761 * @author Adrian Lang <lang@cosmocode.de> 762 */ 763function subscription_handle_post(&$params) { 764 global $INFO; 765 global $lang; 766 767 // Get and validate parameters. 768 if (!isset($params['target'])) { 769 throw new Exception('no subscription target given'); 770 } 771 $target = $params['target']; 772 $valid_styles = array('every', 'digest'); 773 if (substr($target, -1, 1) === ':') { 774 // Allow “list” subscribe style since the target is a namespace. 775 $valid_styles[] = 'list'; 776 } 777 $style = valid_input_set('style', $valid_styles, $params, 778 'invalid subscription style given'); 779 $action = valid_input_set('action', array('subscribe', 'unsubscribe'), 780 $params, 'invalid subscription action given'); 781 782 // Check other conditions. 783 if ($action === 'subscribe') { 784 if ($INFO['userinfo']['mail'] === '') { 785 throw new Exception($lang['subscr_subscribe_noaddress']); 786 } 787 } elseif ($action === 'unsubscribe') { 788 $is = false; 789 foreach($INFO['subscribed'] as $subscr) { 790 if ($subscr['target'] === $target) { 791 $is = true; 792 } 793 } 794 if ($is === false) { 795 throw new Exception(sprintf($lang['subscr_not_subscribed'], 796 $_SERVER['REMOTE_USER'], 797 prettyprint_id($target))); 798 } 799 // subscription_set deletes a subscription if style = null. 800 $style = null; 801 } 802 803 $params = compact('target', 'style', 'action'); 804} 805 806//Setup VIM: ex: et ts=2 : 807