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