xref: /dokuwiki/inc/actions.php (revision b83823e5b0c95ae2365cfc20eb33094c6ab108f2)
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 $lang;
24    global $conf;
25
26    $preact = $ACT;
27
28    // give plugins an opportunity to process the action
29    $evt = new Doku_Event('ACTION_ACT_PREPROCESS',$ACT);
30    if ($evt->advise_before()) {
31
32        //sanitize $ACT
33        $ACT = act_validate($ACT);
34
35        //check if searchword was given - else just show
36        $s = cleanID($QUERY);
37        if($ACT == 'search' && empty($s)){
38            $ACT = 'show';
39        }
40
41        //login stuff
42        if(in_array($ACT,array('login','logout'))){
43            $ACT = act_auth($ACT);
44        }
45
46        //check if user is asking to (un)subscribe a page
47        if($ACT == 'subscribe') {
48            try {
49                $ACT = act_subscription($ACT);
50            } catch (Exception $e) {
51                msg($e->getMessage(), -1);
52            }
53        }
54
55        //display some infos
56        if($ACT == 'check'){
57            check();
58            $ACT = 'show';
59        }
60
61        //check permissions
62        $ACT = act_permcheck($ACT);
63
64        //sitemap
65        if ($ACT == 'sitemap'){
66            $ACT = act_sitemap($ACT);
67        }
68
69        //register
70        if($ACT == 'register' && $_POST['save'] && register()){
71            $ACT = 'login';
72        }
73
74        if ($ACT == 'resendpwd' && act_resendpwd()) {
75            $ACT = 'login';
76        }
77
78        //update user profile
79        if ($ACT == 'profile') {
80            if(!$_SERVER['REMOTE_USER']) {
81                $ACT = 'login';
82            } else {
83                if(updateprofile()) {
84                    msg($lang['profchanged'],1);
85                    $ACT = 'show';
86                }
87            }
88        }
89
90        //revert
91        if($ACT == 'revert'){
92            if(checkSecurityToken()){
93                $ACT = act_revert($ACT);
94            }else{
95                $ACT = 'show';
96            }
97        }
98
99        //save
100        if($ACT == 'save'){
101            if(checkSecurityToken()){
102                $ACT = act_save($ACT);
103            }else{
104                $ACT = 'preview';
105            }
106        }
107
108        //cancel conflicting edit
109        if($ACT == 'cancel')
110            $ACT = 'show';
111
112        //draft deletion
113        if($ACT == 'draftdel')
114            $ACT = act_draftdel($ACT);
115
116        //draft saving on preview
117        if($ACT == 'preview')
118            $ACT = act_draftsave($ACT);
119
120        //edit
121        if(in_array($ACT, array('edit', 'preview', 'recover'))) {
122            $ACT = act_edit($ACT);
123        }else{
124            unlock($ID); //try to unlock
125        }
126
127        //handle export
128        if(substr($ACT,0,7) == 'export_')
129            $ACT = act_export($ACT);
130
131        //handle admin tasks
132        if($ACT == 'admin'){
133            // retrieve admin plugin name from $_REQUEST['page']
134            if (!empty($_REQUEST['page'])) {
135                $pluginlist = plugin_list('admin');
136                if (in_array($_REQUEST['page'], $pluginlist)) {
137                    // attempt to load the plugin
138                    if ($plugin =& plugin_load('admin',$_REQUEST['page']) !== null){
139                        if($plugin->forAdminOnly() && !$INFO['isadmin']){
140                            // a manager tried to load a plugin that's for admins only
141                            unset($_REQUEST['page']);
142                            msg('For admins only',-1);
143                        }else{
144                            $plugin->handle();
145                        }
146                    }
147                }
148            }
149        }
150
151        // check permissions again - the action may have changed
152        $ACT = act_permcheck($ACT);
153    }  // end event ACTION_ACT_PREPROCESS default action
154    $evt->advise_after();
155    // Make sure plugs can handle 'denied'
156    if($conf['send404'] && $ACT == 'denied') {
157        header('HTTP/1.0 403 Forbidden');
158    }
159    unset($evt);
160
161    // when action 'show', the intial not 'show' and POST, do a redirect
162    if($ACT == 'show' && $preact != 'show' && strtolower($_SERVER['REQUEST_METHOD']) == 'post'){
163        act_redirect($ID,$preact);
164    }
165
166    global $INFO;
167    global $conf;
168    global $license;
169
170    //call template FIXME: all needed vars available?
171    $headers[] = 'Content-Type: text/html; charset=utf-8';
172    trigger_event('ACTION_HEADERS_SEND',$headers,'act_sendheaders');
173
174    include(template('main.php'));
175    // output for the commands is now handled in inc/templates.php
176    // in function tpl_content()
177}
178
179function act_sendheaders($headers) {
180    foreach ($headers as $hdr) header($hdr);
181}
182
183/**
184 * Sanitize the action command
185 *
186 * @author Andreas Gohr <andi@splitbrain.org>
187 */
188function act_clean($act){
189    global $lang;
190    global $conf;
191    global $INFO;
192
193    // check if the action was given as array key
194    if(is_array($act)){
195        list($act) = array_keys($act);
196    }
197
198    //remove all bad chars
199    $act = strtolower($act);
200    $act = preg_replace('/[^1-9a-z_]+/','',$act);
201
202    if($act == 'export_html') $act = 'export_xhtml';
203    if($act == 'export_htmlbody') $act = 'export_xhtmlbody';
204
205    if($act === '') $act = 'show';
206    return $act;
207}
208
209/**
210 * Sanitize and validate action commands.
211 *
212 * Add all allowed commands here.
213 *
214 * @author Andreas Gohr <andi@splitbrain.org>
215 */
216function act_validate($act) {
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 $conf;
314    if($conf['usedraft'] && $_POST['wikitext']){
315        $draft = array('id'     => $ID,
316                'prefix' => substr($_POST['prefix'], 0, -1),
317                'text'   => $_POST['wikitext'],
318                'suffix' => $_POST['suffix'],
319                'date'   => (int) $_POST['date'],
320                'client' => $INFO['client'],
321                );
322        $cname = getCacheName($draft['client'].$ID,'.draft');
323        if(io_saveFile($cname,serialize($draft))){
324            $INFO['draft'] = $cname;
325        }
326    }
327    return $act;
328}
329
330/**
331 * Handle 'save'
332 *
333 * Checks for spam and conflicts and saves the page.
334 * Does a redirect to show the page afterwards or
335 * returns a new action.
336 *
337 * @author Andreas Gohr <andi@splitbrain.org>
338 */
339function act_save($act){
340    global $ID;
341    global $DATE;
342    global $PRE;
343    global $TEXT;
344    global $SUF;
345    global $SUM;
346    global $lang;
347    global $INFO;
348
349    //spam check
350    if(checkwordblock()) {
351        msg($lang['wordblock'], -1);
352        return 'edit';
353    }
354    //conflict check
355    if($DATE != 0 && $INFO['meta']['date']['modified'] > $DATE )
356        return 'conflict';
357
358    //save it
359    saveWikiText($ID,con($PRE,$TEXT,$SUF,1),$SUM,$_REQUEST['minor']); //use pretty mode for con
360    //unlock it
361    unlock($ID);
362
363    //delete draft
364    act_draftdel($act);
365    session_write_close();
366
367    // when done, show page
368    return 'show';
369}
370
371/**
372 * Revert to a certain revision
373 *
374 * @author Andreas Gohr <andi@splitbrain.org>
375 */
376function act_revert($act){
377    global $ID;
378    global $REV;
379    global $lang;
380    // FIXME $INFO['writable'] currently refers to the attic version
381    // global $INFO;
382    // if (!$INFO['writable']) {
383    //     return 'show';
384    // }
385
386    // when no revision is given, delete current one
387    // FIXME this feature is not exposed in the GUI currently
388    $text = '';
389    $sum  = $lang['deleted'];
390    if($REV){
391        $text = rawWiki($ID,$REV);
392        if(!$text) return 'show'; //something went wrong
393        $sum = sprintf($lang['restored'], dformat($REV));
394    }
395
396    // spam check
397
398    if (checkwordblock($text)) {
399        msg($lang['wordblock'], -1);
400        return 'edit';
401    }
402
403    saveWikiText($ID,$text,$sum,false);
404    msg($sum,1);
405
406    //delete any draft
407    act_draftdel($act);
408    session_write_close();
409
410    // when done, show current page
411    $_SERVER['REQUEST_METHOD'] = 'post'; //should force a redirect
412    $REV = '';
413    return 'show';
414}
415
416/**
417 * Do a redirect after receiving post data
418 *
419 * Tries to add the section id as hash mark after section editing
420 */
421function act_redirect($id,$preact){
422    global $PRE;
423    global $TEXT;
424
425    $opts = array(
426            'id'       => $id,
427            'preact'   => $preact
428            );
429    //get section name when coming from section edit
430    if($PRE && preg_match('/^\s*==+([^=\n]+)/',$TEXT,$match)){
431        $check = false; //Byref
432        $opts['fragment'] = sectionID($match[0], $check);
433    }
434
435    trigger_event('ACTION_SHOW_REDIRECT',$opts,'act_redirect_execute');
436}
437
438function act_redirect_execute($opts){
439    $go = wl($opts['id'],'',true);
440    if(isset($opts['fragment'])) $go .= '#'.$opts['fragment'];
441
442    //show it
443    send_redirect($go);
444}
445
446/**
447 * Handle 'login', 'logout'
448 *
449 * @author Andreas Gohr <andi@splitbrain.org>
450 */
451function act_auth($act){
452    global $ID;
453    global $INFO;
454
455    //already logged in?
456    if(isset($_SERVER['REMOTE_USER']) && $act=='login'){
457        return 'show';
458    }
459
460    //handle logout
461    if($act=='logout'){
462        $lockedby = checklock($ID); //page still locked?
463        if($lockedby == $_SERVER['REMOTE_USER'])
464            unlock($ID); //try to unlock
465
466        // do the logout stuff
467        auth_logoff();
468
469        // rebuild info array
470        $INFO = pageinfo();
471
472        act_redirect($ID,'login');
473    }
474
475    return $act;
476}
477
478/**
479 * Handle 'edit', 'preview', 'recover'
480 *
481 * @author Andreas Gohr <andi@splitbrain.org>
482 */
483function act_edit($act){
484    global $ID;
485    global $INFO;
486
487    global $TEXT;
488    global $RANGE;
489    global $PRE;
490    global $SUF;
491    global $REV;
492    global $SUM;
493    global $lang;
494    global $DATE;
495
496    if (!isset($TEXT)) {
497        if ($INFO['exists']) {
498            if ($RANGE) {
499                list($PRE,$TEXT,$SUF) = rawWikiSlices($RANGE,$ID,$REV);
500            } else {
501                $TEXT = rawWiki($ID,$REV);
502            }
503        } else {
504            $TEXT = pageTemplate($ID);
505        }
506    }
507
508    //set summary default
509    if(!$SUM){
510        if($REV){
511            $SUM = sprintf($lang['restored'], dformat($REV));
512        }elseif(!$INFO['exists']){
513            $SUM = $lang['created'];
514        }
515    }
516
517    // Use the date of the newest revision, not of the revision we edit
518    // This is used for conflict detection
519    if(!$DATE) $DATE = @filemtime(wikiFN($ID));
520
521    //check if locked by anyone - if not lock for my self
522    //do not lock when the user can't edit anyway
523    if ($INFO['writable']) {
524        $lockedby = checklock($ID);
525        if($lockedby) return 'locked';
526
527        lock($ID);
528    }
529
530    return $act;
531}
532
533/**
534 * Export a wiki page for various formats
535 *
536 * Triggers ACTION_EXPORT_POSTPROCESS
537 *
538 *  Event data:
539 *    data['id']      -- page id
540 *    data['mode']    -- requested export mode
541 *    data['headers'] -- export headers
542 *    data['output']  -- export output
543 *
544 * @author Andreas Gohr <andi@splitbrain.org>
545 * @author Michael Klier <chi@chimeric.de>
546 */
547function act_export($act){
548    global $ID;
549    global $REV;
550    global $conf;
551    global $lang;
552
553    $pre = '';
554    $post = '';
555    $output = '';
556    $headers = array();
557
558    // search engines: never cache exported docs! (Google only currently)
559    $headers['X-Robots-Tag'] = 'noindex';
560
561    $mode = substr($act,7);
562    switch($mode) {
563        case 'raw':
564            $headers['Content-Type'] = 'text/plain; charset=utf-8';
565            $headers['Content-Disposition'] = 'attachment; filename='.noNS($ID).'.txt';
566            $output = rawWiki($ID,$REV);
567            break;
568        case 'xhtml':
569            $pre .= '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"' . DOKU_LF;
570            $pre .= ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' . DOKU_LF;
571            $pre .= '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="'.$conf['lang'].'"' . DOKU_LF;
572            $pre .= ' lang="'.$conf['lang'].'" dir="'.$lang['direction'].'">' . DOKU_LF;
573            $pre .= '<head>' . DOKU_LF;
574            $pre .= '  <meta http-equiv="Content-Type" content="text/html; 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='.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
683    // subcriptions work for logged in users only
684    if(!$_SERVER['REMOTE_USER']) return 'show';
685
686    // get and preprocess data.
687    $params = array();
688    foreach(array('target', 'style', 'action') as $param) {
689        if (isset($_REQUEST["sub_$param"])) {
690            $params[$param] = $_REQUEST["sub_$param"];
691        }
692    }
693
694    // any action given? if not just return and show the subscription page
695    if(!$params['action'] || !checkSecurityToken()) return $act;
696
697    // Handle POST data, may throw exception.
698    trigger_event('ACTION_HANDLE_SUBSCRIBE', $params, 'subscription_handle_post');
699
700    $target = $params['target'];
701    $style  = $params['style'];
702    $data   = $params['data'];
703    $action = $params['action'];
704
705    // Perform action.
706    if (!subscription_set($_SERVER['REMOTE_USER'], $target, $style, $data)) {
707        throw new Exception(sprintf($lang["subscr_{$action}_error"],
708                                    hsc($INFO['userinfo']['name']),
709                                    prettyprint_id($target)));
710    }
711    msg(sprintf($lang["subscr_{$action}_success"], hsc($INFO['userinfo']['name']),
712                prettyprint_id($target)), 1);
713    act_redirect($ID, $act);
714
715    // Assure that we have valid data if act_redirect somehow fails.
716    $INFO['subscribed'] = get_info_subscribed();
717    return 'show';
718}
719
720/**
721 * Validate POST data
722 *
723 * Validates POST data for a subscribe or unsubscribe request. This is the
724 * default action for the event ACTION_HANDLE_SUBSCRIBE.
725 *
726 * @author Adrian Lang <lang@cosmocode.de>
727 */
728function subscription_handle_post(&$params) {
729    global $INFO;
730    global $lang;
731
732    // Get and validate parameters.
733    if (!isset($params['target'])) {
734        throw new Exception('no subscription target given');
735    }
736    $target = $params['target'];
737    $valid_styles = array('every', 'digest');
738    if (substr($target, -1, 1) === ':') {
739        // Allow “list” subscribe style since the target is a namespace.
740        $valid_styles[] = 'list';
741    }
742    $style  = valid_input_set('style', $valid_styles, $params,
743                              'invalid subscription style given');
744    $action = valid_input_set('action', array('subscribe', 'unsubscribe'),
745                              $params, 'invalid subscription action given');
746
747    // Check other conditions.
748    if ($action === 'subscribe') {
749        if ($INFO['userinfo']['mail'] === '') {
750            throw new Exception($lang['subscr_subscribe_noaddress']);
751        }
752    } elseif ($action === 'unsubscribe') {
753        $is = false;
754        foreach($INFO['subscribed'] as $subscr) {
755            if ($subscr['target'] === $target) {
756                $is = true;
757            }
758        }
759        if ($is === false) {
760            throw new Exception(sprintf($lang['subscr_not_subscribed'],
761                                        $_SERVER['REMOTE_USER'],
762                                        prettyprint_id($target)));
763        }
764        // subscription_set deletes a subscription if style = null.
765        $style = null;
766    }
767
768    $data = in_array($style, array('list', 'digest')) ? time() : null;
769    $params = compact('target', 'style', 'data', 'action');
770}
771
772//Setup VIM: ex: et ts=2 :
773