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