Hotfix Release 2017-02-19c "Frusterick Manners"
[dokuwiki.git] / inc / actions.php
blobadba2aa3233108d2ea4b3044067e1d24f5a77ca9
1 <?php
2 /**
3 * DokuWiki Actions
5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author Andreas Gohr <andi@splitbrain.org>
7 */
9 if(!defined('DOKU_INC')) die('meh.');
11 /**
12 * Call the needed action handlers
14 * @author Andreas Gohr <andi@splitbrain.org>
15 * @triggers ACTION_ACT_PREPROCESS
16 * @triggers ACTION_HEADERS_SEND
18 function 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;
28 $preact = $ACT;
30 // give plugins an opportunity to process the action
31 $evt = new Doku_Event('ACTION_ACT_PREPROCESS',$ACT);
33 $headers = array();
34 if ($evt->advise_before()) {
36 //sanitize $ACT
37 $ACT = act_validate($ACT);
39 //check if searchword was given - else just show
40 $s = cleanID($QUERY);
41 if($ACT == 'search' && empty($s)){
42 $ACT = 'show';
45 //login stuff
46 if(in_array($ACT,array('login','logout'))){
47 $ACT = act_auth($ACT);
50 //check if user is asking to (un)subscribe a page
51 if($ACT == 'subscribe') {
52 try {
53 $ACT = act_subscription($ACT);
54 } catch (Exception $e) {
55 msg($e->getMessage(), -1);
59 //display some info
60 if($ACT == 'check'){
61 check();
62 $ACT = 'show';
65 //check permissions
66 $ACT = act_permcheck($ACT);
68 //sitemap
69 if ($ACT == 'sitemap'){
70 act_sitemap($ACT);
73 //recent changes
74 if ($ACT == 'recent'){
75 $show_changes = $INPUT->str('show_changes');
76 if (!empty($show_changes)) {
77 set_doku_pref('show_changes', $show_changes);
81 //diff
82 if ($ACT == 'diff'){
83 $difftype = $INPUT->str('difftype');
84 if (!empty($difftype)) {
85 set_doku_pref('difftype', $difftype);
89 //register
90 if($ACT == 'register' && $INPUT->post->bool('save') && register()){
91 $ACT = 'login';
94 if ($ACT == 'resendpwd' && act_resendpwd()) {
95 $ACT = 'login';
98 // user profile changes
99 if (in_array($ACT, array('profile','profile_delete'))) {
100 if(!$INPUT->server->str('REMOTE_USER')) {
101 $ACT = 'login';
102 } else {
103 switch ($ACT) {
104 case 'profile' :
105 if(updateprofile()) {
106 msg($lang['profchanged'],1);
107 $ACT = 'show';
109 break;
110 case 'profile_delete' :
111 if(auth_deleteprofile()){
112 msg($lang['profdeleted'],1);
113 $ACT = 'show';
114 } else {
115 $ACT = 'profile';
117 break;
122 //revert
123 if($ACT == 'revert'){
124 if(checkSecurityToken()){
125 $ACT = act_revert($ACT);
126 }else{
127 $ACT = 'show';
131 //save
132 if($ACT == 'save'){
133 if(checkSecurityToken()){
134 $ACT = act_save($ACT);
135 }else{
136 $ACT = 'preview';
140 //cancel conflicting edit
141 if($ACT == 'cancel')
142 $ACT = 'show';
144 //draft deletion
145 if($ACT == 'draftdel')
146 $ACT = act_draftdel($ACT);
148 //draft saving on preview
149 if($ACT == 'preview') {
150 $headers[] = "X-XSS-Protection: 0";
151 $ACT = act_draftsave($ACT);
154 //edit
155 if(in_array($ACT, array('edit', 'preview', 'recover'))) {
156 $ACT = act_edit($ACT);
157 }else{
158 unlock($ID); //try to unlock
161 //handle export
162 if(substr($ACT,0,7) == 'export_')
163 $ACT = act_export($ACT);
165 //handle admin tasks
166 if($ACT == 'admin'){
167 // retrieve admin plugin name from $_REQUEST['page']
168 if (($page = $INPUT->str('page', '', true)) != '') {
169 /** @var $plugin DokuWiki_Admin_Plugin */
170 if ($plugin = plugin_getRequestAdminPlugin()){
171 $plugin->handle();
176 // check permissions again - the action may have changed
177 $ACT = act_permcheck($ACT);
178 } // end event ACTION_ACT_PREPROCESS default action
179 $evt->advise_after();
180 // Make sure plugs can handle 'denied'
181 if($conf['send404'] && $ACT == 'denied') {
182 http_status(403);
184 unset($evt);
186 // when action 'show', the intial not 'show' and POST, do a redirect
187 if($ACT == 'show' && $preact != 'show' && strtolower($INPUT->server->str('REQUEST_METHOD')) == 'post'){
188 act_redirect($ID,$preact);
191 global $INFO;
192 global $conf;
193 global $license;
195 //call template FIXME: all needed vars available?
196 $headers[] = 'Content-Type: text/html; charset=utf-8';
197 trigger_event('ACTION_HEADERS_SEND',$headers,'act_sendheaders');
199 include(template('main.php'));
200 // output for the commands is now handled in inc/templates.php
201 // in function tpl_content()
205 * Send the given headers using header()
207 * @param array $headers The headers that shall be sent
209 function act_sendheaders($headers) {
210 foreach ($headers as $hdr) header($hdr);
214 * Sanitize the action command
216 * @author Andreas Gohr <andi@splitbrain.org>
218 * @param array|string $act
219 * @return string
221 function act_clean($act){
222 // check if the action was given as array key
223 if(is_array($act)){
224 list($act) = array_keys($act);
227 //remove all bad chars
228 $act = strtolower($act);
229 $act = preg_replace('/[^1-9a-z_]+/','',$act);
231 if($act == 'export_html') $act = 'export_xhtml';
232 if($act == 'export_htmlbody') $act = 'export_xhtmlbody';
234 if($act === '') $act = 'show';
235 return $act;
239 * Sanitize and validate action commands.
241 * Add all allowed commands here.
243 * @author Andreas Gohr <andi@splitbrain.org>
245 * @param array|string $act
246 * @return string
248 function act_validate($act) {
249 global $conf;
250 global $INFO;
252 $act = act_clean($act);
254 // check if action is disabled
255 if(!actionOK($act)){
256 msg('Command disabled: '.htmlspecialchars($act),-1);
257 return 'show';
260 //disable all acl related commands if ACL is disabled
261 if(!$conf['useacl'] && in_array($act,array('login','logout','register','admin',
262 'subscribe','unsubscribe','profile','revert',
263 'resendpwd','profile_delete'))){
264 msg('Command unavailable: '.htmlspecialchars($act),-1);
265 return 'show';
268 //is there really a draft?
269 if($act == 'draft' && !file_exists($INFO['draft'])) return 'edit';
271 if(!in_array($act,array('login','logout','register','save','cancel','edit','draft',
272 'preview','search','show','check','index','revisions',
273 'diff','recent','backlink','admin','subscribe','revert',
274 'unsubscribe','profile','profile_delete','resendpwd','recover',
275 'draftdel','sitemap','media')) && substr($act,0,7) != 'export_' ) {
276 msg('Command unknown: '.htmlspecialchars($act),-1);
277 return 'show';
279 return $act;
283 * Run permissionchecks
285 * @author Andreas Gohr <andi@splitbrain.org>
287 * @param string $act action command
288 * @return string action command
290 function act_permcheck($act){
291 global $INFO;
293 if(in_array($act,array('save','preview','edit','recover'))){
294 if($INFO['exists']){
295 if($act == 'edit'){
296 //the edit function will check again and do a source show
297 //when no AUTH_EDIT available
298 $permneed = AUTH_READ;
299 }else{
300 $permneed = AUTH_EDIT;
302 }else{
303 $permneed = AUTH_CREATE;
305 }elseif(in_array($act,array('login','search','recent','profile','profile_delete','index', 'sitemap'))){
306 $permneed = AUTH_NONE;
307 }elseif($act == 'revert'){
308 $permneed = AUTH_ADMIN;
309 if($INFO['ismanager']) $permneed = AUTH_EDIT;
310 }elseif($act == 'register'){
311 $permneed = AUTH_NONE;
312 }elseif($act == 'resendpwd'){
313 $permneed = AUTH_NONE;
314 }elseif($act == 'admin'){
315 if($INFO['ismanager']){
316 // if the manager has the needed permissions for a certain admin
317 // action is checked later
318 $permneed = AUTH_READ;
319 }else{
320 $permneed = AUTH_ADMIN;
322 }else{
323 $permneed = AUTH_READ;
325 if($INFO['perm'] >= $permneed) return $act;
327 return 'denied';
331 * Handle 'draftdel'
333 * Deletes the draft for the current page and user
335 * @param string $act action command
336 * @return string action command
338 function act_draftdel($act){
339 global $INFO;
340 @unlink($INFO['draft']);
341 $INFO['draft'] = null;
342 return 'show';
346 * Saves a draft on preview
348 * @todo this currently duplicates code from ajax.php :-/
350 * @param string $act action command
351 * @return string action command
353 function act_draftsave($act){
354 global $INFO;
355 global $ID;
356 global $INPUT;
357 global $conf;
358 if($conf['usedraft'] && $INPUT->post->has('wikitext')) {
359 $draft = array('id' => $ID,
360 'prefix' => substr($INPUT->post->str('prefix'), 0, -1),
361 'text' => $INPUT->post->str('wikitext'),
362 'suffix' => $INPUT->post->str('suffix'),
363 'date' => $INPUT->post->int('date'),
364 'client' => $INFO['client'],
366 $cname = getCacheName($draft['client'].$ID,'.draft');
367 if(io_saveFile($cname,serialize($draft))){
368 $INFO['draft'] = $cname;
371 return $act;
375 * Handle 'save'
377 * Checks for spam and conflicts and saves the page.
378 * Does a redirect to show the page afterwards or
379 * returns a new action.
381 * @author Andreas Gohr <andi@splitbrain.org>
383 * @param string $act action command
384 * @return string action command
386 function act_save($act){
387 global $ID;
388 global $DATE;
389 global $PRE;
390 global $TEXT;
391 global $SUF;
392 global $SUM;
393 global $lang;
394 global $INFO;
395 global $INPUT;
397 //spam check
398 if(checkwordblock()) {
399 msg($lang['wordblock'], -1);
400 return 'edit';
402 //conflict check
403 if($DATE != 0 && $INFO['meta']['date']['modified'] > $DATE )
404 return 'conflict';
406 //save it
407 saveWikiText($ID,con($PRE,$TEXT,$SUF,true),$SUM,$INPUT->bool('minor')); //use pretty mode for con
408 //unlock it
409 unlock($ID);
411 //delete draft
412 act_draftdel($act);
413 session_write_close();
415 // when done, show page
416 return 'show';
420 * Revert to a certain revision
422 * @author Andreas Gohr <andi@splitbrain.org>
424 * @param string $act action command
425 * @return string action command
427 function act_revert($act){
428 global $ID;
429 global $REV;
430 global $lang;
431 /* @var Input $INPUT */
432 global $INPUT;
433 // FIXME $INFO['writable'] currently refers to the attic version
434 // global $INFO;
435 // if (!$INFO['writable']) {
436 // return 'show';
437 // }
439 // when no revision is given, delete current one
440 // FIXME this feature is not exposed in the GUI currently
441 $text = '';
442 $sum = $lang['deleted'];
443 if($REV){
444 $text = rawWiki($ID,$REV);
445 if(!$text) return 'show'; //something went wrong
446 $sum = sprintf($lang['restored'], dformat($REV));
449 // spam check
451 if (checkwordblock($text)) {
452 msg($lang['wordblock'], -1);
453 return 'edit';
456 saveWikiText($ID,$text,$sum,false);
457 msg($sum,1);
459 //delete any draft
460 act_draftdel($act);
461 session_write_close();
463 // when done, show current page
464 $INPUT->server->set('REQUEST_METHOD','post'); //should force a redirect
465 $REV = '';
466 return 'show';
470 * Do a redirect after receiving post data
472 * Tries to add the section id as hash mark after section editing
474 * @param string $id page id
475 * @param string $preact action command before redirect
477 function act_redirect($id,$preact){
478 global $PRE;
479 global $TEXT;
481 $opts = array(
482 'id' => $id,
483 'preact' => $preact
485 //get section name when coming from section edit
486 if($PRE && preg_match('/^\s*==+([^=\n]+)/',$TEXT,$match)){
487 $check = false; //Byref
488 $opts['fragment'] = sectionID($match[0], $check);
491 trigger_event('ACTION_SHOW_REDIRECT',$opts,'act_redirect_execute');
495 * Execute the redirect
497 * @param array $opts id and fragment for the redirect and the preact
499 function act_redirect_execute($opts){
500 $go = wl($opts['id'],'',true);
501 if(isset($opts['fragment'])) $go .= '#'.$opts['fragment'];
503 //show it
504 send_redirect($go);
508 * Handle 'login', 'logout'
510 * @author Andreas Gohr <andi@splitbrain.org>
512 * @param string $act action command
513 * @return string action command
515 function act_auth($act){
516 global $ID;
517 global $INFO;
518 /* @var Input $INPUT */
519 global $INPUT;
521 //already logged in?
522 if($INPUT->server->has('REMOTE_USER') && $act=='login'){
523 return 'show';
526 //handle logout
527 if($act=='logout'){
528 $lockedby = checklock($ID); //page still locked?
529 if($lockedby == $INPUT->server->str('REMOTE_USER')){
530 unlock($ID); //try to unlock
533 // do the logout stuff
534 auth_logoff();
536 // rebuild info array
537 $INFO = pageinfo();
539 act_redirect($ID,'login');
542 return $act;
546 * Handle 'edit', 'preview', 'recover'
548 * @author Andreas Gohr <andi@splitbrain.org>
550 * @param string $act action command
551 * @return string action command
553 function act_edit($act){
554 global $ID;
555 global $INFO;
557 global $TEXT;
558 global $RANGE;
559 global $PRE;
560 global $SUF;
561 global $REV;
562 global $SUM;
563 global $lang;
564 global $DATE;
566 if (!isset($TEXT)) {
567 if ($INFO['exists']) {
568 if ($RANGE) {
569 list($PRE,$TEXT,$SUF) = rawWikiSlices($RANGE,$ID,$REV);
570 } else {
571 $TEXT = rawWiki($ID,$REV);
573 } else {
574 $TEXT = pageTemplate($ID);
578 //set summary default
579 if(!$SUM){
580 if($REV){
581 $SUM = sprintf($lang['restored'], dformat($REV));
582 }elseif(!$INFO['exists']){
583 $SUM = $lang['created'];
587 // Use the date of the newest revision, not of the revision we edit
588 // This is used for conflict detection
589 if(!$DATE) $DATE = @filemtime(wikiFN($ID));
591 //check if locked by anyone - if not lock for my self
592 //do not lock when the user can't edit anyway
593 if ($INFO['writable']) {
594 $lockedby = checklock($ID);
595 if($lockedby) return 'locked';
597 lock($ID);
600 return $act;
604 * Export a wiki page for various formats
606 * Triggers ACTION_EXPORT_POSTPROCESS
608 * Event data:
609 * data['id'] -- page id
610 * data['mode'] -- requested export mode
611 * data['headers'] -- export headers
612 * data['output'] -- export output
614 * @author Andreas Gohr <andi@splitbrain.org>
615 * @author Michael Klier <chi@chimeric.de>
617 * @param string $act action command
618 * @return string action command
620 function act_export($act){
621 global $ID;
622 global $REV;
623 global $conf;
624 global $lang;
626 $pre = '';
627 $post = '';
628 $headers = array();
630 // search engines: never cache exported docs! (Google only currently)
631 $headers['X-Robots-Tag'] = 'noindex';
633 $mode = substr($act,7);
634 switch($mode) {
635 case 'raw':
636 $headers['Content-Type'] = 'text/plain; charset=utf-8';
637 $headers['Content-Disposition'] = 'attachment; filename='.noNS($ID).'.txt';
638 $output = rawWiki($ID,$REV);
639 break;
640 case 'xhtml':
641 $pre .= '<!DOCTYPE html>' . DOKU_LF;
642 $pre .= '<html lang="'.$conf['lang'].'" dir="'.$lang['direction'].'">' . DOKU_LF;
643 $pre .= '<head>' . DOKU_LF;
644 $pre .= ' <meta charset="utf-8" />' . DOKU_LF;
645 $pre .= ' <title>'.$ID.'</title>' . DOKU_LF;
647 // get metaheaders
648 ob_start();
649 tpl_metaheaders();
650 $pre .= ob_get_clean();
652 $pre .= '</head>' . DOKU_LF;
653 $pre .= '<body>' . DOKU_LF;
654 $pre .= '<div class="dokuwiki export">' . DOKU_LF;
656 // get toc
657 $pre .= tpl_toc(true);
659 $headers['Content-Type'] = 'text/html; charset=utf-8';
660 $output = p_wiki_xhtml($ID,$REV,false);
662 $post .= '</div>' . DOKU_LF;
663 $post .= '</body>' . DOKU_LF;
664 $post .= '</html>' . DOKU_LF;
665 break;
666 case 'xhtmlbody':
667 $headers['Content-Type'] = 'text/html; charset=utf-8';
668 $output = p_wiki_xhtml($ID,$REV,false);
669 break;
670 default:
671 $output = p_cached_output(wikiFN($ID,$REV), $mode, $ID);
672 $headers = p_get_metadata($ID,"format $mode");
673 break;
676 // prepare event data
677 $data = array();
678 $data['id'] = $ID;
679 $data['mode'] = $mode;
680 $data['headers'] = $headers;
681 $data['output'] =& $output;
683 trigger_event('ACTION_EXPORT_POSTPROCESS', $data);
685 if(!empty($data['output'])){
686 if(is_array($data['headers'])) foreach($data['headers'] as $key => $val){
687 header("$key: $val");
689 print $pre.$data['output'].$post;
690 exit;
692 return 'show';
696 * Handle sitemap delivery
698 * @author Michael Hamann <michael@content-space.de>
700 * @param string $act action command
702 function act_sitemap($act) {
703 global $conf;
705 if ($conf['sitemap'] < 1 || !is_numeric($conf['sitemap'])) {
706 http_status(404);
707 print "Sitemap generation is disabled.";
708 exit;
711 $sitemap = Sitemapper::getFilePath();
712 if (Sitemapper::sitemapIsCompressed()) {
713 $mime = 'application/x-gzip';
714 }else{
715 $mime = 'application/xml; charset=utf-8';
718 // Check if sitemap file exists, otherwise create it
719 if (!is_readable($sitemap)) {
720 Sitemapper::generate();
723 if (is_readable($sitemap)) {
724 // Send headers
725 header('Content-Type: '.$mime);
726 header('Content-Disposition: attachment; filename='.utf8_basename($sitemap));
728 http_conditionalRequest(filemtime($sitemap));
730 // Send file
731 //use x-sendfile header to pass the delivery to compatible webservers
732 http_sendfile($sitemap);
734 readfile($sitemap);
735 exit;
738 http_status(500);
739 print "Could not read the sitemap file - bad permissions?";
740 exit;
744 * Handle page 'subscribe'
746 * Throws exception on error.
748 * @author Adrian Lang <lang@cosmocode.de>
750 * @param string $act action command
751 * @return string action command
752 * @throws Exception if (un)subscribing fails
754 function act_subscription($act){
755 global $lang;
756 global $INFO;
757 global $ID;
758 /* @var Input $INPUT */
759 global $INPUT;
761 // subcriptions work for logged in users only
762 if(!$INPUT->server->str('REMOTE_USER')) return 'show';
764 // get and preprocess data.
765 $params = array();
766 foreach(array('target', 'style', 'action') as $param) {
767 if ($INPUT->has("sub_$param")) {
768 $params[$param] = $INPUT->str("sub_$param");
772 // any action given? if not just return and show the subscription page
773 if(empty($params['action']) || !checkSecurityToken()) return $act;
775 // Handle POST data, may throw exception.
776 trigger_event('ACTION_HANDLE_SUBSCRIBE', $params, 'subscription_handle_post');
778 $target = $params['target'];
779 $style = $params['style'];
780 $action = $params['action'];
782 // Perform action.
783 $sub = new Subscription();
784 if($action == 'unsubscribe'){
785 $ok = $sub->remove($target, $INPUT->server->str('REMOTE_USER'), $style);
786 }else{
787 $ok = $sub->add($target, $INPUT->server->str('REMOTE_USER'), $style);
790 if($ok) {
791 msg(sprintf($lang["subscr_{$action}_success"], hsc($INFO['userinfo']['name']),
792 prettyprint_id($target)), 1);
793 act_redirect($ID, $act);
794 } else {
795 throw new Exception(sprintf($lang["subscr_{$action}_error"],
796 hsc($INFO['userinfo']['name']),
797 prettyprint_id($target)));
800 // Assure that we have valid data if act_redirect somehow fails.
801 $INFO['subscribed'] = $sub->user_subscription();
802 return 'show';
806 * Validate POST data
808 * Validates POST data for a subscribe or unsubscribe request. This is the
809 * default action for the event ACTION_HANDLE_SUBSCRIBE.
811 * @author Adrian Lang <lang@cosmocode.de>
813 * @param array &$params the parameters: target, style and action
814 * @throws Exception
816 function subscription_handle_post(&$params) {
817 global $INFO;
818 global $lang;
819 /* @var Input $INPUT */
820 global $INPUT;
822 // Get and validate parameters.
823 if (!isset($params['target'])) {
824 throw new Exception('no subscription target given');
826 $target = $params['target'];
827 $valid_styles = array('every', 'digest');
828 if (substr($target, -1, 1) === ':') {
829 // Allow “list” subscribe style since the target is a namespace.
830 $valid_styles[] = 'list';
832 $style = valid_input_set('style', $valid_styles, $params,
833 'invalid subscription style given');
834 $action = valid_input_set('action', array('subscribe', 'unsubscribe'),
835 $params, 'invalid subscription action given');
837 // Check other conditions.
838 if ($action === 'subscribe') {
839 if ($INFO['userinfo']['mail'] === '') {
840 throw new Exception($lang['subscr_subscribe_noaddress']);
842 } elseif ($action === 'unsubscribe') {
843 $is = false;
844 foreach($INFO['subscribed'] as $subscr) {
845 if ($subscr['target'] === $target) {
846 $is = true;
849 if ($is === false) {
850 throw new Exception(sprintf($lang['subscr_not_subscribed'],
851 $INPUT->server->str('REMOTE_USER'),
852 prettyprint_id($target)));
854 // subscription_set deletes a subscription if style = null.
855 $style = null;
858 $params = compact('target', 'style', 'action');
861 //Setup VIM: ex: et ts=2 :