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