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