xref: /dokuwiki/inc/actions.php (revision ebb29737d32bc331541f78a1a47f33ba33919938)
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 $QUERY;
22    global $lang;
23
24    $preact = $ACT;
25
26    // give plugins an opportunity to process the action
27    $evt = new Doku_Event('ACTION_ACT_PREPROCESS',$ACT);
28    if ($evt->advise_before()) {
29
30        //sanitize $ACT
31        $ACT = act_clean($ACT);
32
33        //check if searchword was given - else just show
34        $s = cleanID($QUERY);
35        if($ACT == 'search' && empty($s)){
36            $ACT = 'show';
37        }
38
39        //login stuff
40        if(in_array($ACT,array('login','logout'))){
41            $ACT = act_auth($ACT);
42        }
43
44        //check if user is asking to (un)subscribe a page
45        if($ACT == 'subscribe') {
46            try {
47                $ACT = act_subscription($ACT);
48            } catch (Exception $e) {
49                msg($e->getMessage(), -1);
50            }
51        }
52
53        //check permissions
54        $ACT = act_permcheck($ACT);
55
56        //sitemap
57        if ($ACT == 'sitemap'){
58            $ACT = act_sitemap($ACT);
59        }
60
61        //register
62        if($ACT == 'register' && $_POST['save'] && register()){
63            $ACT = 'login';
64        }
65
66        if ($ACT == 'resendpwd' && act_resendpwd()) {
67            $ACT = 'login';
68        }
69
70        //update user profile
71        if ($ACT == 'profile') {
72            if(!$_SERVER['REMOTE_USER']) {
73                $ACT = 'login';
74            } else {
75                if(updateprofile()) {
76                    msg($lang['profchanged'],1);
77                    $ACT = 'show';
78                }
79            }
80        }
81
82        //revert
83        if($ACT == 'revert'){
84            if(checkSecurityToken()){
85                $ACT = act_revert($ACT);
86            }else{
87                $ACT = 'show';
88            }
89        }
90
91        //save
92        if($ACT == 'save'){
93            if(checkSecurityToken()){
94                $ACT = act_save($ACT);
95            }else{
96                $ACT = 'show';
97            }
98        }
99
100        //cancel conflicting edit
101        if($ACT == 'cancel')
102            $ACT = 'show';
103
104        //draft deletion
105        if($ACT == 'draftdel')
106            $ACT = act_draftdel($ACT);
107
108        //draft saving on preview
109        if($ACT == 'preview')
110            $ACT = act_draftsave($ACT);
111
112        //edit
113        if(in_array($ACT, array('edit', 'preview', 'recover'))) {
114            $ACT = act_edit($ACT);
115        }else{
116            unlock($ID); //try to unlock
117        }
118
119        //handle export
120        if(substr($ACT,0,7) == 'export_')
121            $ACT = act_export($ACT);
122
123        //display some infos
124        if($ACT == 'check'){
125            check();
126            $ACT = 'show';
127        }
128
129        //handle admin tasks
130        if($ACT == 'admin'){
131            // retrieve admin plugin name from $_REQUEST['page']
132            if (!empty($_REQUEST['page'])) {
133                $pluginlist = plugin_list('admin');
134                if (in_array($_REQUEST['page'], $pluginlist)) {
135                    // attempt to load the plugin
136                    if ($plugin =& plugin_load('admin',$_REQUEST['page']) !== null)
137                        $plugin->handle();
138                }
139            }
140        }
141
142        // check permissions again - the action may have changed
143        $ACT = act_permcheck($ACT);
144    }  // end event ACTION_ACT_PREPROCESS default action
145    $evt->advise_after();
146    unset($evt);
147
148    // when action 'show', the intial not 'show' and POST, do a redirect
149    if($ACT == 'show' && $preact != 'show' && strtolower($_SERVER['REQUEST_METHOD']) == 'post'){
150        act_redirect($ID,$preact);
151    }
152
153    global $INFO;
154    global $conf;
155    global $license;
156
157    //call template FIXME: all needed vars available?
158    $headers[] = 'Content-Type: text/html; charset=utf-8';
159    trigger_event('ACTION_HEADERS_SEND',$headers,'act_sendheaders');
160
161    include(template('main.php'));
162    // output for the commands is now handled in inc/templates.php
163    // in function tpl_content()
164}
165
166function act_sendheaders($headers) {
167    foreach ($headers as $hdr) header($hdr);
168}
169
170/**
171 * Sanitize the action command
172 *
173 * Add all allowed commands here.
174 *
175 * @author Andreas Gohr <andi@splitbrain.org>
176 */
177function act_clean($act){
178    global $lang;
179    global $conf;
180
181    // check if the action was given as array key
182    if(is_array($act)){
183        list($act) = array_keys($act);
184    }
185
186    //remove all bad chars
187    $act = strtolower($act);
188    $act = preg_replace('/[^1-9a-z_]+/','',$act);
189
190    if($act == 'export_html') $act = 'export_xhtml';
191    if($act == 'export_htmlbody') $act = 'export_xhtmlbody';
192
193    if($act === '') $act = 'show';
194
195    // check if action is disabled
196    if(!actionOK($act)){
197        msg('Command disabled: '.htmlspecialchars($act),-1);
198        return 'show';
199    }
200
201    //disable all acl related commands if ACL is disabled
202    if(!$conf['useacl'] && in_array($act,array('login','logout','register','admin',
203                    'subscribe','unsubscribe','profile','revert',
204                    'resendpwd','subscribens','unsubscribens',))){
205        msg('Command unavailable: '.htmlspecialchars($act),-1);
206        return 'show';
207    }
208
209    if(!in_array($act,array('login','logout','register','save','cancel','edit','draft',
210                    'preview','search','show','check','index','revisions',
211                    'diff','recent','backlink','admin','subscribe','revert',
212                    'unsubscribe','profile','resendpwd','recover',
213                    'draftdel','subscribens','unsubscribens','sitemap')) && substr($act,0,7) != 'export_' ) {
214        msg('Command unknown: '.htmlspecialchars($act),-1);
215        return 'show';
216    }
217    return $act;
218}
219
220/**
221 * Run permissionchecks
222 *
223 * @author Andreas Gohr <andi@splitbrain.org>
224 */
225function act_permcheck($act){
226    global $INFO;
227    global $conf;
228
229    if(in_array($act,array('save','preview','edit','recover'))){
230        if($INFO['exists']){
231            if($act == 'edit'){
232                //the edit function will check again and do a source show
233                //when no AUTH_EDIT available
234                $permneed = AUTH_READ;
235            }else{
236                $permneed = AUTH_EDIT;
237            }
238        }else{
239            $permneed = AUTH_CREATE;
240        }
241    }elseif(in_array($act,array('login','search','recent','profile','index', 'sitemap'))){
242    }elseif(in_array($act,array('login','search','recent','profile','sitemap'))){
243        $permneed = AUTH_NONE;
244    }elseif($act == 'revert'){
245        $permneed = AUTH_ADMIN;
246        if($INFO['ismanager']) $permneed = AUTH_EDIT;
247    }elseif($act == 'register'){
248        $permneed = AUTH_NONE;
249    }elseif($act == 'resendpwd'){
250        $permneed = AUTH_NONE;
251    }elseif($act == 'admin'){
252        if($INFO['ismanager']){
253            // if the manager has the needed permissions for a certain admin
254            // action is checked later
255            $permneed = AUTH_READ;
256        }else{
257            $permneed = AUTH_ADMIN;
258        }
259    }else{
260        $permneed = AUTH_READ;
261    }
262    if($INFO['perm'] >= $permneed) return $act;
263
264    return 'denied';
265}
266
267/**
268 * Handle 'draftdel'
269 *
270 * Deletes the draft for the current page and user
271 */
272function act_draftdel($act){
273    global $INFO;
274    @unlink($INFO['draft']);
275    $INFO['draft'] = null;
276    return 'show';
277}
278
279/**
280 * Saves a draft on preview
281 *
282 * @todo this currently duplicates code from ajax.php :-/
283 */
284function act_draftsave($act){
285    global $INFO;
286    global $ID;
287    global $conf;
288    if($conf['usedraft'] && $_POST['wikitext']){
289        $draft = array('id'     => $ID,
290                'prefix' => $_POST['prefix'],
291                'text'   => $_POST['wikitext'],
292                'suffix' => $_POST['suffix'],
293                'date'   => $_POST['date'],
294                'client' => $INFO['client'],
295                );
296        $cname = getCacheName($draft['client'].$ID,'.draft');
297        if(io_saveFile($cname,serialize($draft))){
298            $INFO['draft'] = $cname;
299        }
300    }
301    return $act;
302}
303
304/**
305 * Handle 'save'
306 *
307 * Checks for spam and conflicts and saves the page.
308 * Does a redirect to show the page afterwards or
309 * returns a new action.
310 *
311 * @author Andreas Gohr <andi@splitbrain.org>
312 */
313function act_save($act){
314    global $ID;
315    global $DATE;
316    global $PRE;
317    global $TEXT;
318    global $SUF;
319    global $SUM;
320    global $lang;
321    global $INFO;
322
323    //spam check
324    if(checkwordblock()) {
325        msg($lang['wordblock'], -1);
326        return 'edit';
327    }
328    //conflict check
329    if($DATE != 0 && $INFO['meta']['date']['modified'] > $DATE )
330        return 'conflict';
331
332    //save it
333    saveWikiText($ID,con($PRE,$TEXT,$SUF,1),$SUM,$_REQUEST['minor']); //use pretty mode for con
334    //unlock it
335    unlock($ID);
336
337    //delete draft
338    act_draftdel($act);
339    session_write_close();
340
341    // when done, show page
342    return 'show';
343}
344
345/**
346 * Revert to a certain revision
347 *
348 * @author Andreas Gohr <andi@splitbrain.org>
349 */
350function act_revert($act){
351    global $ID;
352    global $REV;
353    global $lang;
354    // FIXME $INFO['writable'] currently refers to the attic version
355    // global $INFO;
356    // if (!$INFO['writable']) {
357    //     return 'show';
358    // }
359
360    // when no revision is given, delete current one
361    // FIXME this feature is not exposed in the GUI currently
362    $text = '';
363    $sum  = $lang['deleted'];
364    if($REV){
365        $text = rawWiki($ID,$REV);
366        if(!$text) return 'show'; //something went wrong
367        $sum  = $lang['restored'];
368    }
369
370    // spam check
371
372    if (checkwordblock($text)) {
373        msg($lang['wordblock'], -1);
374        return 'edit';
375    }
376
377    saveWikiText($ID,$text,$sum,false);
378    msg($sum,1);
379
380    //delete any draft
381    act_draftdel($act);
382    session_write_close();
383
384    // when done, show current page
385    $_SERVER['REQUEST_METHOD'] = 'post'; //should force a redirect
386    $REV = '';
387    return 'show';
388}
389
390/**
391 * Do a redirect after receiving post data
392 *
393 * Tries to add the section id as hash mark after section editing
394 */
395function act_redirect($id,$preact){
396    global $PRE;
397    global $TEXT;
398
399    $opts = array(
400            'id'       => $id,
401            'preact'   => $preact
402            );
403    //get section name when coming from section edit
404    if($PRE && preg_match('/^\s*==+([^=\n]+)/',$TEXT,$match)){
405        $check = false; //Byref
406        $opts['fragment'] = sectionID($match[0], $check);
407    }
408
409    trigger_event('ACTION_SHOW_REDIRECT',$opts,'act_redirect_execute');
410}
411
412function act_redirect_execute($opts){
413    $go = wl($opts['id'],'',true);
414    if(isset($opts['fragment'])) $go .= '#'.$opts['fragment'];
415
416    //show it
417    send_redirect($go);
418}
419
420/**
421 * Handle 'login', 'logout'
422 *
423 * @author Andreas Gohr <andi@splitbrain.org>
424 */
425function act_auth($act){
426    global $ID;
427    global $INFO;
428
429    //already logged in?
430    if(isset($_SERVER['REMOTE_USER']) && $act=='login'){
431        return 'show';
432    }
433
434    //handle logout
435    if($act=='logout'){
436        $lockedby = checklock($ID); //page still locked?
437        if($lockedby == $_SERVER['REMOTE_USER'])
438            unlock($ID); //try to unlock
439
440        // do the logout stuff
441        auth_logoff();
442
443        // rebuild info array
444        $INFO = pageinfo();
445
446        act_redirect($ID,'login');
447    }
448
449    return $act;
450}
451
452/**
453 * Handle 'edit', 'preview', 'recover'
454 *
455 * @author Andreas Gohr <andi@splitbrain.org>
456 */
457function act_edit($act){
458    global $ID;
459    global $INFO;
460
461    global $TEXT;
462    global $RANGE;
463    global $PRE;
464    global $SUF;
465    global $REV;
466    global $SUM;
467    global $lang;
468    global $DATE;
469
470    if (!isset($TEXT)) {
471        if ($INFO['exists']) {
472            if ($RANGE) {
473                list($PRE,$TEXT,$SUF) = rawWikiSlices($RANGE,$ID,$REV);
474            } else {
475                $TEXT = rawWiki($ID,$REV);
476            }
477        } else {
478            $TEXT = pageTemplate($ID);
479        }
480    }
481
482    //set summary default
483    if(!$SUM){
484        if($REV){
485            $SUM = $lang['restored'];
486        }elseif(!$INFO['exists']){
487            $SUM = $lang['created'];
488        }
489    }
490
491    // Use the date of the newest revision, not of the revision we edit
492    // This is used for conflict detection
493    if(!$DATE) $DATE = $INFO['meta']['date']['modified'];
494
495    //check if locked by anyone - if not lock for my self
496    $lockedby = checklock($ID);
497    if($lockedby) return 'locked';
498
499    lock($ID);
500    return $act;
501}
502
503/**
504 * Export a wiki page for various formats
505 *
506 * Triggers ACTION_EXPORT_POSTPROCESS
507 *
508 *  Event data:
509 *    data['id']      -- page id
510 *    data['mode']    -- requested export mode
511 *    data['headers'] -- export headers
512 *    data['output']  -- export output
513 *
514 * @author Andreas Gohr <andi@splitbrain.org>
515 * @author Michael Klier <chi@chimeric.de>
516 */
517function act_export($act){
518    global $ID;
519    global $REV;
520    global $conf;
521    global $lang;
522
523    $pre = '';
524    $post = '';
525    $output = '';
526    $headers = array();
527
528    // search engines: never cache exported docs! (Google only currently)
529    $headers['X-Robots-Tag'] = 'noindex';
530
531    $mode = substr($act,7);
532    switch($mode) {
533        case 'raw':
534            $headers['Content-Type'] = 'text/plain; charset=utf-8';
535            $headers['Content-Disposition'] = 'attachment; filename='.noNS($ID).'.txt';
536            $output = rawWiki($ID,$REV);
537            break;
538        case 'xhtml':
539            $pre .= '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"' . DOKU_LF;
540            $pre .= ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' . DOKU_LF;
541            $pre .= '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="'.$conf['lang'].'"' . DOKU_LF;
542            $pre .= ' lang="'.$conf['lang'].'" dir="'.$lang['direction'].'">' . DOKU_LF;
543            $pre .= '<head>' . DOKU_LF;
544            $pre .= '  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . DOKU_LF;
545            $pre .= '  <title>'.$ID.'</title>' . DOKU_LF;
546
547            // get metaheaders
548            ob_start();
549            tpl_metaheaders();
550            $pre .= ob_get_clean();
551
552            $pre .= '</head>' . DOKU_LF;
553            $pre .= '<body>' . DOKU_LF;
554            $pre .= '<div class="dokuwiki export">' . DOKU_LF;
555
556            // get toc
557            $pre .= tpl_toc(true);
558
559            $headers['Content-Type'] = 'text/html; charset=utf-8';
560            $output = p_wiki_xhtml($ID,$REV,false);
561
562            $post .= '</div>' . DOKU_LF;
563            $post .= '</body>' . DOKU_LF;
564            $post .= '</html>' . DOKU_LF;
565            break;
566        case 'xhtmlbody':
567            $headers['Content-Type'] = 'text/html; charset=utf-8';
568            $output = p_wiki_xhtml($ID,$REV,false);
569            break;
570        default:
571            $output = p_cached_output(wikiFN($ID,$REV), $mode);
572            $headers = p_get_metadata($ID,"format $mode");
573            break;
574    }
575
576    // prepare event data
577    $data = array();
578    $data['id'] = $ID;
579    $data['mode'] = $mode;
580    $data['headers'] = $headers;
581    $data['output'] =& $output;
582
583    trigger_event('ACTION_EXPORT_POSTPROCESS', $data);
584
585    if(!empty($data['output'])){
586        if(is_array($data['headers'])) foreach($data['headers'] as $key => $val){
587            header("$key: $val");
588        }
589        print $pre.$data['output'].$post;
590        exit;
591    }
592    return 'show';
593}
594
595/**
596 * Handle sitemap delivery
597 *
598 * @author Michael Hamann <michael@content-space.de>
599 */
600function act_sitemap($act) {
601    global $conf;
602
603    if ($conf['sitemap'] < 1 || !is_numeric($conf['sitemap'])) {
604        header("HTTP/1.0 404 Not Found");
605        print "Sitemap generation is disabled.";
606        exit;
607    }
608
609    $sitemap = Sitemapper::getFilePath();
610    if(strrchr($sitemap, '.') === '.gz'){
611        $mime = 'application/x-gzip';
612    }else{
613        $mime = 'application/xml; charset=utf-8';
614    }
615
616    // Check if sitemap file exists, otherwise create it
617    if (!is_readable($sitemap)) {
618        Sitemapper::generate();
619    }
620
621    if (is_readable($sitemap)) {
622        // Send headers
623        header('Content-Type: '.$mime);
624
625        http_conditionalRequest(filemtime($sitemap));
626
627        // Send file
628        //use x-sendfile header to pass the delivery to compatible webservers
629        if (http_sendfile($sitemap)) exit;
630
631        readfile($sitemap);
632        exit;
633    }
634
635    header("HTTP/1.0 500 Internal Server Error");
636    print "Could not read the sitemap file - bad permissions?";
637    exit;
638}
639
640/**
641 * Handle page 'subscribe'
642 *
643 * Throws exception on error.
644 *
645 * @author Adrian Lang <lang@cosmocode.de>
646 */
647function act_subscription($act){
648    global $lang;
649    global $INFO;
650    global $ID;
651
652    // subcriptions work for logged in users only
653    if(!$_SERVER['REMOTE_USER']) return 'show';
654
655    // get and preprocess data.
656    $params = array();
657    foreach(array('target', 'style', 'action') as $param) {
658        if (isset($_REQUEST["sub_$param"])) {
659            $params[$param] = $_REQUEST["sub_$param"];
660        }
661    }
662
663    // any action given? if not just return and show the subscription page
664    if(!$params['action'] || !checkSecurityToken()) return $act;
665
666    // Handle POST data, may throw exception.
667    trigger_event('ACTION_HANDLE_SUBSCRIBE', $params, 'subscription_handle_post');
668
669    $target = $params['target'];
670    $style  = $params['style'];
671    $data   = $params['data'];
672    $action = $params['action'];
673
674    // Perform action.
675    if (!subscription_set($_SERVER['REMOTE_USER'], $target, $style, $data)) {
676        throw new Exception(sprintf($lang["subscr_{$action}_error"],
677                                    hsc($INFO['userinfo']['name']),
678                                    prettyprint_id($target)));
679    }
680    msg(sprintf($lang["subscr_{$action}_success"], hsc($INFO['userinfo']['name']),
681                prettyprint_id($target)), 1);
682    act_redirect($ID, $act);
683
684    // Assure that we have valid data if act_redirect somehow fails.
685    $INFO['subscribed'] = get_info_subscribed();
686    return 'show';
687}
688
689/**
690 * Validate POST data
691 *
692 * Validates POST data for a subscribe or unsubscribe request. This is the
693 * default action for the event ACTION_HANDLE_SUBSCRIBE.
694 *
695 * @author Adrian Lang <lang@cosmocode.de>
696 */
697function subscription_handle_post(&$params) {
698    global $INFO;
699    global $lang;
700
701    // Get and validate parameters.
702    if (!isset($params['target'])) {
703        throw new Exception('no subscription target given');
704    }
705    $target = $params['target'];
706    $valid_styles = array('every', 'digest');
707    if (substr($target, -1, 1) === ':') {
708        // Allow “list” subscribe style since the target is a namespace.
709        $valid_styles[] = 'list';
710    }
711    $style  = valid_input_set('style', $valid_styles, $params,
712                              'invalid subscription style given');
713    $action = valid_input_set('action', array('subscribe', 'unsubscribe'),
714                              $params, 'invalid subscription action given');
715
716    // Check other conditions.
717    if ($action === 'subscribe') {
718        if ($INFO['userinfo']['mail'] === '') {
719            throw new Exception($lang['subscr_subscribe_noaddress']);
720        }
721    } elseif ($action === 'unsubscribe') {
722        $is = false;
723        foreach($INFO['subscribed'] as $subscr) {
724            if ($subscr['target'] === $target) {
725                $is = true;
726            }
727        }
728        if ($is === false) {
729            throw new Exception(sprintf($lang['subscr_not_subscribed'],
730                                        $_SERVER['REMOTE_USER'],
731                                        prettyprint_id($target)));
732        }
733        // subscription_set deletes a subscription if style = null.
734        $style = null;
735    }
736
737    $data = in_array($style, array('list', 'digest')) ? time() : null;
738    $params = compact('target', 'style', 'data', 'action');
739}
740
741//Setup VIM: ex: et ts=2 enc=utf-8 :
742