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