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'] = ' ('.$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"> </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').': '; 526 $TR .= $fullname.' ('.$_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