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