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