xref: /dokuwiki/inc/actions.php (revision 940dbfe468f779c7c88783a4cecb82c4dc97143b)
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