1<?php
2/**
3 * Doodle Plugin 3: helps to schedule meetings
4 *
5 * @license	GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @url     http://www.dokuwiki.org/plugin:doodle3
7 * @author	Matthias Jung <matzekuh@web.de>
8 * @author  Robert Rackl <wiki@doogie.de>
9 * @author	Jonathan Tsai <tryweb@ichiayi.com>
10 * @author  Esther Brunner <wikidesign@gmail.com>
11 * @author  Romain Coltel <aorimn@gmail.com>
12 */
13
14if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
15if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
16require_once(DOKU_PLUGIN.'syntax.php');
17
18/**
19 * Displays a table where users can vote for some predefined choices
20 * Syntax:
21 *
22 * <pre>
23 * <doodle
24 *   title="What do you like best?"
25 *   auth="none|ip|user"
26 *   adminUsers="user1|user2"
27 *   adminGroups="group1|group2"
28 *   voteType="default|multi"
29 *   fieldwidth="auto|123px"
30 *   closed="true|false" >
31 *     * Option 1
32 *     * Option 2 **some wikimarkup** \\ is __allowed__!
33 *     * Option 3
34 * </doodle>
35 * </pre>
36 *
37 * Only required parameteres are a title and at least one option.
38 *
39 * <h3>Parameters</h3>
40 * auth="none" - everyone can vote with any username, (IPs will be recorded but not checked)
41 * auth="ip"   - everyone can vote with any username, votes will be tracked by IP to prevent duplicate voting
42 * auth="user" - users must login with a valid dokuwiki user. This has the advantage, that users can
43 *               edit their vote ("change their mind") later on.
44 *
45 * <h3>adminUsers and adminGroups</h3>
46 * "|"-separated list of adminUsers or adminGroups, whose members can always edit and delete <b>any</b> entry.
47 *
48 * <h3>Vote Type</h3>
49 * default    - user can vote for exactly one option (round checkboxes will be shown)
50 * multi      - can choose any number of options, including none (square checkboxes will be shown).
51 *
52 * <h3>fieldwidth</h3>
53 * auto	      - width of option columns is determined by content (css: width:auto)
54 * 123px      - width of all option columns is set to provided value (e.g. css: width:123px)
55 		valid values must match regexp: /^[0-9]+px$/ (see https://regex101.com/r/yKOhAo/1 for details)
56 *
57 * If closed=="true", then no one can vote anymore. The result will still be shown on the page.
58 *
59 * The doodle's data is saved in '<dokuwiki>/data/meta/title_of_vote.doodle'. The filename is the (masked) title.
60 * This has the advantage that you can move your doodle to another page, without loosing the data.
61 */
62class syntax_plugin_doodle3 extends DokuWiki_Syntax_Plugin
63{
64    const AUTH_NONE = 0;
65    const AUTH_IP   = 1;
66    const AUTH_USER = 2;
67
68    /**
69     * return info about this plugin
70     */
71    function getInfo() {
72        return array(
73            'author' => 'Matthias Jung',
74            'email'  => 'matzekuh@web.de',
75            'date'   => '2017/03/30',
76            'name'   => 'Doodle Plugin 3',
77            'desc'   => 'helps to schedule meetings',
78            'url'    => 'https://www.dokuwiki.org/plugin:doodle3',
79        );
80    }
81    function getType()  { return 'substition';}
82    function getPType() { return 'block';}
83    function getSort()  { return 168; }
84
85    /**
86     * Connect pattern to lexer
87     */
88    function connectTo($mode) {
89        $this->Lexer->addSpecialPattern('<doodle\b.*?>.+?</doodle>', $mode, 'plugin_doodle3');
90    }
91
92    /**
93     * Handle the match, parse parameters & choices
94     * and prepare everything for the render() method.
95     */
96    function handle($match, $state, $pos, Doku_Handler $handler) {
97        $match = substr($match, 8, -9);              // strip markup (including space after "<doodle ")
98        list($parameterStr, $choiceStr) = preg_split('/>/u', $match, 2);
99
100        //----- default parameter settings
101        $params = array(
102            'title'          => 'Default title',
103            'auth'           => self::AUTH_NONE,
104            'adminUsers'     => '',
105            'adminGroups'    => '',
106            'adminMail'      => null,
107            'voteType'       => 'default',
108            'closed'         => FALSE,
109	    'fieldwidth'     => 'auto'
110        );
111
112        //----- parse parameteres into name="value" pairs
113        preg_match_all("/(\w+?)=\"(.*?)\"/", $parameterStr, $regexMatches, PREG_SET_ORDER);
114        //debout($parameterStr);
115        //debout($regexMatches);
116        for ($i = 0; $i < count($regexMatches); $i++) {
117            $name  = strtoupper($regexMatches[$i][1]);  // first subpattern: name of attribute in UPPERCASE
118            $value = $regexMatches[$i][2];              // second subpattern is value
119            if (strcmp($name, "TITLE") == 0) {
120                $params['title'] = hsc(trim($value));
121            } else
122            if (strcmp($name, "AUTH") == 0) {
123               if (strcasecmp($value, 'IP') == 0) {
124                   $params['auth'] = self::AUTH_IP;
125               } else
126               if (strcasecmp($value, 'USER') == 0) {
127                   $params['auth'] = self::AUTH_USER;
128               }
129            } else
130            if (strcmp($name, "ADMINUSERS") == 0) {
131                $params['adminUsers'] = $value;
132            } else
133            if (strcmp($name, "ADMINGROUPS") == 0) {
134                $params['adminGroups'] = $value;
135            } else
136            if (strcmp($name, "ADMINMAIL") == 0) {
137                // check for valid email adress
138                if (preg_match('/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,5})$/', $value)) {
139                    $params['adminMail'] = $value;
140                }
141            } else
142            if (strcmp($name, "VOTETYPE") == 0) {
143                if (preg_match('/default|multi/', $value)) {
144                    $params['voteType'] = $value;
145                }
146            } else
147            if ((strcmp($name, "CLOSEON") == 0) &&
148                (($timestamp = strtotime($value)) !== false) &&
149                (time() > $timestamp)   )
150            {
151                $params['closed'] = 1;
152            } else
153            if (strcmp($name, "CLOSED") == 0) {
154		if (strcasecmp($value, "TRUE") == 0) {
155			$params['closed'] = 1;
156		} else if ($time = strtotime($value)) {
157			if ($time < time()) $params['closed'] = 1;
158		} else {
159                	$params['closed'] = 0;
160		}
161            } else
162            if (strcmp($name, "SORT") == 0) {
163                $params['sort'] = $value;  // make it possible to sort by time
164            } else
165	    if (strcmp($name, "FIELDWIDTH") == 0) {
166		if (preg_match("/^[0-9]+px$/",$value,$hit) == 1)
167		    $params['fieldwidth'] = $hit[0];
168	    }
169        }
170
171        // (If there are no choices inside the <doodle> tag, then doodle's data will be reset.)
172        $choices = $this->parseChoices($choiceStr);
173
174        $result = array('params' => $params, 'choices' => $choices);
175        //debout('handle returns', $result);
176        return $result;
177    }
178
179    /**
180     * parse list of choices
181     * explode, trim and encode html entities,
182     * empty choices will be skipped.
183     */
184    function parseChoices($choiceStr) {
185        $choices = array();
186        preg_match_all('/^\s{0,3}\* (.*?)$/m', $choiceStr, $matches, PREG_PATTERN_ORDER);
187        foreach ($matches[1] as $choice) {
188            $choice = hsc(trim($choice));
189            if (!empty($choice)) {
190                $choice = preg_replace('#\\\\\\\\#', '<br />', $choice);       # two(!) backslashes for a newline
191                $choice = preg_replace('#\*\*(.*?)\*\*#', '<b>\1</b>', $choice);   # bold
192                $choice = preg_replace('#__(.*?)__#', '<u>\1</u>', $choice);   # underscore
193                $choice = preg_replace('#//(.*?)//#', '<i>\1</i>', $choice);   # italic
194                $choices []= $choice;
195            }
196        }
197        //debout($choices);
198        return $choices;
199    }
200
201    // ----- these fields will always be initialized at the beginning of the render function
202    //       and can then be used in helper functions below.
203    public $params    = array();
204    public $choices   = array();
205    public $doodle    = array();
206    public $template  = array();   // output values for doodle_template.php
207
208    /**
209     * Read doodle data from file,
210     * add new vote if user just submitted one and
211     * create output xHTML from template
212     */
213    function render($mode, Doku_Renderer $renderer, $data) {
214        if ($mode != 'xhtml') return false;
215
216        //debout("render: $mode");
217        global $lang;
218        global $auth;
219        global $conf;
220        global $INFO; // needed for users real name
221        global $ACT;  // action from $_REQUEST['do']
222        global $REV;  // to not allow any action if it's an old page
223        global $ID;   // name of current page
224
225        //debout('data in render', $data);
226
227        $this->params    = $data['params'];
228        $this->choices   = $data['choices'];
229        $this->doodle    = array();
230        $this->template  = array();
231
232        // prevent caching to ensure the poll results are fresh
233        $renderer->info['cache'] = false;
234
235        // ----- read doodle data from file (if there are choices given and there is a file)
236        if (count($this->choices) > 0) {
237            $this->doodle = $this->readDoodleDataFromFile();
238        }
239
240        //FIXME: count($choices) may be different from number of choices in $doodle data!
241
242        // ----- FORM ACTIONS (only allowed when showing the most recent version of the page, not when editing) -----
243        $formId =  'doodle__form__'.cleanID($this->params['title']);
244        if ($ACT == 'show' && $_REQUEST['formId'] == $formId && $REV == false) {
245            // ---- cast new vote
246            if (!empty($_REQUEST['cast__vote'])) {
247                $this->castVote();
248            } else
249            // ---- start editing an entry
250            if (!empty($_REQUEST['edit__entry']) ) {
251                $this->startEditEntry();
252            } else
253            // ---- save changed entry
254            if (!empty($_REQUEST['change__vote']) ) {
255                $this->castVote();
256            } else
257            // ---- delete an entry completely
258            if (!empty($_REQUEST['delete__entry']) ) {
259                $this->deleteEntry();
260            }
261        }
262
263        /******** Format of the $doodle array ***********
264         * The $doodle array maps fullnames (with html special characters masked) to an array of userData for this vote.
265         * Each sub array contains:
266         *   'username' loggin name if use was logged in
267         *   'choices'  is an (variable length!) array of column indexes where user has voted
268         *   'ip'       ip of voting machine
269         *   'time'     unix timestamp when vote was casted
270
271
272        $doodle = array(
273          'Robert' => array(
274            'username'  => 'doogie'
275            'choices'   => array(0, 3),
276            'ip'        => '123.123.123.123',
277            'time'      => 1284970602
278          ),
279          'Peter' => array(
280            'choices'   => array(),
281            'ip'        => '222.122.111.1',
282            'time'      > 12849702333
283          ),
284          'Sabine' => array(
285            'choices'   => array(0, 1, 2, 3, 4),
286            'ip'        => '333.333.333.333',
287            'time'      => 1284970222
288          ),
289        );
290        */
291
292        // ---- fill $this->template variable for doodle_template.php (column by column)
293        $this->template['title']      = hsc($this->params['title']);
294        $this->template['choices']    = $this->choices;
295        $this->template['result']     = $this->params['closed'] ? $this->getLang('final_result') : $this->getLang('count');
296        $this->template['doodleData'] = array();  // this will be filled with some HTML snippets
297        $this->template['formId']     = $formId;
298	$this->template['fieldwidth'] = $this->params['fieldwidth'];
299        if ($this->params['closed']) {
300            $this->template['msg'] = $this->getLang('poll_closed');
301        }
302
303        for($col = 0; $col < count($this->choices); $col++) {
304            $this->template['count'][$col] = 0;
305            foreach ($this->doodle as $fullname => $userData) {
306                if (!empty($userData['username'])) {
307                  $this->template['doodleData']["$fullname"]['username'] = '&nbsp;('.$userData['username'].')';
308                }
309                if (in_array($col, $userData['choices'])) {
310                    $timeLoc = strftime($conf['dformat'], $userData['time']);  // localized time of vote
311                    $this->template['doodleData']["$fullname"]['choice'][$col] =
312                        '<td  class="centeralign" style="background-color:#AFA"><img src="'.DOKU_BASE.'lib/images/success.png" title="'.$timeLoc.'"></td>';
313                    $this->template['count']["$col"]++;
314                } else {
315                    $this->template['doodleData']["$fullname"]['choice'][$col] =
316                        '<td  class="centeralign" style="background-color:#FCC">&nbsp;</td>';
317                }
318            }
319        }
320
321        // ---- add edit link to editable entries
322        foreach($this->doodle as $fullname => $userData) {
323            if ($ACT == 'show' && $REV == false &&
324                $this->isAllowedToEditEntry($fullname))
325            {
326                // the javascript source of these functions is in script.js
327                $this->template['doodleData']["$fullname"]['editLinks'] =
328                   '<a href="javascript:editEntry(\''.$formId.'\',\''.$fullname.'\')">'.
329                   '  <img src="'.DOKU_BASE.'lib/plugins/doodle3/pencil.png" alt="edit entry" style="float:left">'.
330                   '</a>'.
331                   '<a href="javascript:deleteEntry(\''.$formId.'\',\''.$fullname.'\')">'.
332                   '  <img src="'.DOKU_BASE.'lib/plugins/doodle3/delete.png" alt="delete entry" style="float:left">'.
333                   '</a>';
334            }
335        }
336
337        // ---- calculates if user is allowed to vote
338        $this->template['inputTR'] = $this->getInputTR();
339
340        // ----- I am using PHP as a templating engine here.
341        //debout("Template", $this->template);
342        ob_start();
343        include 'doodle_template.php';  // the array $template can be used inside doodle_template.php!
344        $doodle_table = ob_get_contents();
345        ob_end_clean();
346        $renderer->doc .= $doodle_table;
347    }
348
349    // --------------- FORM ACTIONS -----------
350    /**
351     * ACTION: cast a new vote
352     * or save a changed vote
353     * (If user is allowed to.)
354     */
355    function castVote() {
356        $fullname          = hsc(trim($_REQUEST['fullname']));
357        $selected_indexes  = $_REQUEST['selected_indexes'];  // may not be set when all checkboxes are deseleted.
358
359        if (empty($fullname)) {
360            $this->template['msg'] = $this->getLang('dont_have_name');
361            return;
362        }
363        if (empty($selected_indexes)) {
364            if ($this->params['voteType'] == 'multi') {
365                $selected_indexes = array();   //allow empty vote only if voteType is "multi"
366            } else {
367                $this->template['msg'] = $this->getLang('select_one_option');
368                return;
369            }
370        }
371
372        //---- check if user is allowed to vote, according to 'auth' parameter
373
374        //if AUTH_USER, then user must be logged in
375        if ($this->params['auth'] == self::AUTH_USER  && !$this->isLoggedIn()) {
376            $this->template['msg'] = $this->getLang('must_be_logged_in');
377            return;
378        }
379
380        //if AUTH_IP, then prevent duplicate votes by IP.
381        //Exception: If user is logged in he is always allowed to change the vote with his fullname, even if he is on another IP.
382        if ($this->params['auth'] == self::AUTH_IP && !$this->isLoggedIn() && !isset($_REQUEST['change__vote']) ) {
383            foreach($this->doodle as $existintFullname => $userData) {
384              if (strcmp($userData['ip'], $_SERVER['REMOTE_ADDR']) == 0) {
385                $this->template['msg'] = sprintf($this->getLang('ip_has_already_voted'), $_SERVER['REMOTE_ADDR']);
386                return;
387              }
388            }
389        }
390
391        //do not vote twice, unless change__vote is set
392        if (isset($this->doodle["$fullname"]) && !isset($_REQUEST['change__vote']) ) {
393            $this->template['msg'] = $this->getLang('you_voted_already');
394            return;
395        }
396
397        //check if change__vote is allowed
398        if (!empty($_REQUEST['change__vote']) &&
399            !$this->isAllowedToEditEntry($fullname))
400        {
401            $this->template['msg'] = $this->getLang('not_allowed_to_change');
402            return;
403        }
404
405        if (!empty($_SERVER['REMOTE_USER'])) {
406          $this->doodle["$fullname"]['username'] = $_SERVER['REMOTE_USER'];
407        }
408        $this->doodle["$fullname"]['choices'] = $selected_indexes;
409        $this->doodle["$fullname"]['time']    = time();
410        $this->doodle["$fullname"]['ip']      = $_SERVER['REMOTE_ADDR'];
411        $this->writeDoodleDataToFile();
412        $this->template['msg'] = $this->getLang('vote_saved');
413
414        //send mail if  $params['adminMail'] is filled
415        if (!empty($this->params['adminMail'])) {
416            $subj = "[DoodlePlugin] Vote casted by $fullname (".$this->doodle["$fullname"]['username'].')';
417            $body = 'User has casted a vote'."\n\n".print_r($this->doodle["$fullname"], true);
418            mail_send($this->params['adminMail'], $subj, $body, $conf['mailfrom']);
419        }
420    }
421
422    /**
423     * ACTION: start editing an entry
424     * expects fullname of voter in request param edit__entry
425     */
426    function startEditEntry() {
427        $fullname = hsc(trim($_REQUEST['edit__entry']));
428        if (!$this->isAllowedToEditEntry($fullname)) return;
429
430        $this->template['editEntry']['fullname']         = $fullname;
431        $this->template['editEntry']['selected_indexes'] = $this->doodle["$fullname"]['choices'];
432        // $fullname will be shown in the input row
433    }
434
435    /** ACTION: delete an entry completely */
436    function deleteEntry() {
437        $fullname = hsc(trim($_REQUEST['delete__entry']));
438        if (!$this->isAllowedToEditEntry($fullname))   return;
439
440        unset($this->doodle["$fullname"]);
441        $this->writeDoodleDataToFile();
442        $this->template['msg'] = $this->getLang('vote_deleted');
443    }
444
445    // ---------- HELPER METHODS -----------
446
447    /**
448     * check if the currently logged in user is allowed to edit a given entry.
449     * @return true if entryFullname is the entry of the current user, or
450     *         the currently logged in user is in the list of admins
451     */
452    function isAllowedToEditEntry($entryFullname) {
453        global $INFO;
454        global $auth;
455
456        if (empty($entryFullname)) return false;
457        if (!isset($this->doodle["$entryFullname"])) return false;
458        if ($this->params['closed']) return false;
459        if (!$this->isLoggedIn()) return false;
460
461		$allowFlag = false;
462        //check adminGroups
463        if (!empty($this->params['adminGroups'])) {
464            $adminGroups = explode('|', $this->params['adminGroups']); // array of adminGroups
465            $usersGroups = $INFO['userinfo']['grps'];  // array of groups that the user is in
466            if(count(array_intersect($adminGroups, $usersGroups)) > 0) $allowFlag = true;
467        }
468
469        //check adminUsers
470        if (!empty($this->params['adminUsers'])) {
471            $adminUsers = explode('|', $this->params['adminUsers']);
472            if(in_array($_SERVER['REMOTE_USER'], $adminUsers)) $allowFlag = true;
473        }
474
475        //check own entry
476        if(strcasecmp(hsc($INFO['userinfo']['name']), $entryFullname) == 0) $allowFlag = true;  // compare real name
477		return $allowFlag;
478    }
479
480    /**
481     * return true if the user is currently logged in
482     */
483    function isLoggedIn() {
484        // see http://www.dokuwiki.org/devel:environment
485        global $INFO;
486        return isset($INFO['userinfo']);
487    }
488
489    /**
490     * calculate the input table row:
491     * @return   complete <TR> tags for input row and information message
492     * May return empty string, if user is not allowed to vote
493     *
494     * If user is logged in he is always allowed edit his own entry. ("change his mind")
495     * If user is logged in and has already voted, empty string will be returned.
496     * If user is not logged in but login is required (auth="user"), then also return '';
497     */
498    function getInputTR() {
499        global $ACT;
500        global $INFO;
501        if ($ACT != 'show') return '';
502        if ($this->params['closed']) return '';
503
504        $fullname = '';
505        $editMode = false;
506        if ($this->isLoggedIn()) {
507            $fullname = $INFO['userinfo']['name'];
508            if (isset($this->template['editEntry'])) {
509                $fullname = $this->template['editEntry']['fullname'];
510                $editMode = true;
511            } else {
512                if (isset($this->doodle["$fullname"]) ) return '';
513            }
514        } else {
515            if ($this->params['auth'] == self::AUTH_USER) return '';
516        }
517
518        // build html for tr
519        $c = count($this->choices);
520        $TR  = '';
521        //$TR .= '<tr style="height:3px"><th colspan="'.($c+1).'"></th></tr>';
522        $TR .= '<tr>';
523        $TR .= '<td class="rightalign">';
524        if ($fullname) {
525            if ($editMode) $TR .= $this->getLang('edit').':&nbsp;';
526            $TR .= $fullname.'&nbsp;('.$_SERVER['REMOTE_USER'].')';
527            $TR .= '<input type="hidden" name="fullname" value="'.$fullname.'">';
528        } else {
529            $TR .= '<input type="text" name="fullname" value="">';
530        }
531        $TR .='</td>';
532
533        for($col = 0; $col < $c; $col++) {
534            $selected = '';
535            if ($editMode && in_array($col, $this->template['editEntry']['selected_indexes']) ) {
536                $selected = 'checked="checked"';
537            }
538            $TR .= '<td class="centeralign">';
539
540            if ($this->params['voteType'] == 'multi') {
541                $inputType = "checkbox";
542            } else {
543                $inputType = "radio";
544            }
545            $TR .= '<input type="'.$inputType.'" name="selected_indexes[]" value="'.$col.'"';
546            $TR .= $selected.">";
547            $TR .= '</TD>';
548        }
549
550        $TR .= '</tr>';
551        $TR .= '<tr>';
552        $TR .= '  <td colspan="'.($c+1).'" class="centeralign">';
553
554        if ($editMode) {
555            $TR .= '    <input type="submit" id="voteButton" value=" '.$this->getLang('btn_change').' " name="change__vote" class="button">';
556        } else {
557            $TR .= '    <input type="submit" id="voteButton" value=" '.$this->getLang('btn_vote').' " name="cast__vote" class="button">';
558        }
559        $TR .= '  </td>';
560        $TR .= '</tr>';
561
562        return $TR;
563    }
564
565
566    /**
567     * Loads the serialized doodle data from the file in the metadata directory.
568     * If the file does not exist yet, an empty array is returned.
569     * @return the $doodle array
570     * @see writeDoodleDataToFile()
571     */
572    function readDoodleDataFromFile() {
573        $dfile     = $this->getDoodleFileName();
574        $doodle    = array();
575        if (file_exists($dfile)) {
576            $doodle = unserialize(file_get_contents($dfile));
577        }
578        //sanitize: $doodle[$fullnmae]['choices'] must be at least an array
579        //          This may happen if user deselected all choices
580        foreach($doodle as $fullname => $userData) {
581            if (!is_array($doodle["$fullname"]['choices'])) {
582                $doodle["$fullname"]['choices'] = array();
583            }
584        }
585
586        if (strcmp($this->params['sort'], 'time') == 0) {
587            debout("sorting by time");
588            uasort($doodle, 'cmpEntryByTime');
589        } else {
590            uksort($doodle, "strnatcasecmp"); // case insensitive "natural" sort
591        }
592        //debout("read from $dfile", $doodle);
593        return $doodle;
594    }
595
596    /**
597     * serialize the doodles data to a file
598     */
599    function writeDoodleDataToFile() {
600        if (!is_array($this->doodle)) return;
601        $dfile = $this->getDoodleFileName();
602        uksort($this->doodle, "strnatcasecmp"); // case insensitive "natural" sort
603        io_saveFile($dfile, serialize($this->doodle));
604        //debout("written to $dfile", $doodle);
605        return $dfile;
606    }
607
608    /**
609     * create unique filename for this doodle from its title.
610     * (replaces space with underscore etc.)
611     */
612    function getDoodleFileName() {
613        if (empty($this->params['title'])) {
614          debout('Doodle must have title.');
615          return 'doodle.doodle';
616        }
617        $dID       = hsc(trim($this->params['title']));
618        $dfile     = metaFN($dID, '.doodle');       // serialized doodle data file in meta directory
619        return $dfile;
620    }
621
622
623} // end of class
624
625// ----- static functions
626
627/** compare two doodle entries by the time of vote */
628function cmpEntryByTime($a, $b) {
629    return strcmp($a['time'], $b['time']);
630}
631
632
633function debout() {
634    if (func_num_args() == 1) {
635        msg('<pre>'.hsc(print_r(func_get_arg(0), true)).'</pre>');
636    } else if (func_num_args() == 2) {
637        msg('<h2>'.func_get_arg(0).'</h2><pre>'.hsc(print_r(func_get_arg(1), true)).'</pre>');
638    }
639
640}
641
642?>
643