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