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