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">&nbsp;</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').':&nbsp;';
600
601		if ($this->params['printName'] == 'both'){
602			$TR .= $fullname.'&nbsp;('.$_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