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