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