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