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