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