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