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