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