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