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