1, 'date_fmt' => 'j M H:i', //'d/m/Y H:i:s' 'ip_log' => 1, 'ip_block' => 0, 'ip_max_repeat' => 3, //max vote repeats before a block can be activated on the ip )); $FoxFilterFunctions['foxpoll'] = 'FoxPoll'; function FoxPoll ($pagename, $fx) { global $FoxPollConfig, $FoxMsgFmt, $PCache; //DEBUG show($fx,'fx first'); // add ip address to vote entry for logging to check for voting abuse if ($FoxPollConfig['ip_log'] == true) $fx['ip'] = get_IP_Address(); // add date and time for vote log if ($FoxPollConfig['date_log'] == true) $fx['date'] = date($FoxPollConfig['date_fmt'], time()); // poll-inputs parameter is needed! if (empty ($fx['poll-inputs'])) FoxAbort($pagename,"Error in form. poll-inputs missing"); $sep = "_"; //character used in PTVs to separate input name and value // make template for adding vote choices into csv as one line string // of replacement variables, use ';' as separator if (empty($fx['template']) && empty($fx['foxtemplate'])) { $tmpl = "{\$\$vote}; "; //adds hex-encoded vote if ($FoxPollConfig['date_log'] == true) $tmpl .= "{\$\$date}; "; //adds date and time if ($FoxPollConfig['ip_log'] == true) $tmpl .= "{\$\$ip}; "; //adds ip address if (isset($fx['voter-name'])) $tmpl .= "{\$\$voter-name}; "; //adds voter name if supplied from input if (isset($fx['voter-email'])) $tmpl .= "{\$\$voter-email}; "; //adds voter email if supplied from input $fx['foxtemplate'] = $tmpl; } // set page name of data (store) page from target supplied $target = $fx['target']; if ($pos = strpos($target,'#')) { $tpage = substr($target,0,$pos); //strip anchor $tsection = substr($target, $pos); } else { $tpage = $target; $tsection = "#data"; //set missing anchor } $tpage = MakePagename($pagename, $tpage); $fx['target'] = $tpage.$tsection; // set ptvtarget if not supplied on same data page in #datasums section if (empty($fx['ptvtarget'])) $fx['ptvtarget'] = $tpage.$tsection."sums"; // make $alloptions array of polling options either from form parameter // or else from PTV or if that fails construct from poll input fields $alloptions = array(); if (isset ($fx['poll-options'])) $pop = preg_split('/,\s*/', trim($fx['poll-options'],',')); else { $pollopt = PageTextVar($tpage, 'Poll_Options'); if ($pollopt!="") $pop = preg_split('/,\s*/', trim($pollopt,',')); } if (isset($pop)) { foreach($pop as $i => $v) $alloptions[$v] = 0; //set option key to id, and set default value of 0 // make $pollinputs array from 'poll-inputs' string // keys are the input control names, values are arrays of each control's values $pollinputs = array(); $pi = preg_split('/,\s*/', trim($fx['poll-inputs'],',')); foreach($pi as $i => $id) { foreach ($alloptions as $k => $o) { if (strpos($k,$id)=="") continue; $pollinputs[$id][] = preg_replace('/^'.$id.$sep.'/','',$k); } } } // no poll-options set, so form gets parsed to get the options from input markup. // Still needs parameter 'poll-inputs' from form. else { $pollinputs = foxpoll_Get_Poll_Inputs ($pagename, $fx); foreach ($pollinputs as $id => $ops) foreach ($ops as $i => $v) $alloptions[$id.$sep.$v] = 0; //set key to id+value and value to 0 } //DEBUG show($alloptions,'alloptions'); show($pollinputs,'pollinputs'); // initial one-time store setup if data page does not exists // make template for setting up new sums PTVs with 0 value if (!PageExists($tpage)) { $storetmpl = "[[#displaysums]]"; $storetmpl .= "\nvotes: {\$:VOTES} "; foreach ($pollinputs as $key => $ops) { $storetmpl .= "\n".$key.": {\$:TOTL_".$key."} <=> "; foreach ($ops as $k => $val) { $storetmpl .= $val.": {\$:SUM_".$key.$sep.$val."} | "; } } $storetmpl .= "\n(:if false:)\n[[".$tsection."sums]]"; $storetmpl .= "\n(:VOTES: 0:)"; $tpptvs = "\n(:Poll_Options: "; foreach ($pollinputs as $key => $ops) { $storetmpl .= "\n(:TOTL_".$key.": 0:) "; foreach ($ops as $k => $val) { $storetmpl .= "(:SUM_".$key.$sep.$val.": 0:) "; } foreach ($ops as $k => $val) { $tpptvs .= $key.$sep.$val.","; } } $storetmpl .= $tpptvs.":)"; $storetmpl .= "\n[[".$tsection."]]\n"; if (isset($fx['foxtemplate'])) { $ft = $fx['foxtemplate']; $ft = str_replace('{$$','', $ft); $ft = str_replace('}','', $ft); $storetmpl .= $ft; } $fx['foxtemplate'] = $storetmpl; $fx['target'] = $tpage; unset($fx['ptvtarget']); $FoxMsgFmt[] = "Creating new data store on page [[".$tpage."]]."; $FoxMsgFmt[] = "%red%No voting data has been stored yet!"; return $fx; } // treat empty comment inputs same as empty vote choices // when comment gets processed for separate target than the csv table if (isset($fx[':target'])) foreach($fx as $key => $val) { if (substr($key, 0,7)=='comment') { if ($val == "") { unset($fx[$key]); //remove key if empty val foreach ($fx[':foxtemplate'] as $i => $tmpl) { if (strstr($tmpl, $key)) $fx[':foxaction'][$i] = "skip"; //will cause particular target process to be skipped } } } } //create choices array with values of 1 according to vote input $choices = array(); foreach ($pollinputs as $key => $ops) { if (array_key_exists($key, $fx)) { if (is_array($fx[$key])) { foreach ($fx[$key] as $k => $val) $choices[$key.$sep.$val] = 1; } else $choices[$key.$sep.$fx[$key]] = 1; } } // get admin parameters for store and sums integrity checks and corrections $datacheck = (isset( $fx['polldatacheck'])) ? 1 : 0; $updatesums = (isset($fx['pollupdatesums'])) ? 1 : 0; if (empty($choices) && $datacheck==0 && $updatesums==0) FoxAbort($pagename, "%red%'''You need to select from one or more of the options!'''"); //get data from store if (PageExists($tpage)) $data = foxpoll_Get_Data( $pagename, $fx['target'], ';'); // get column sums from data table, and total votes as number of rows $sums = array(); if (empty($data)) $FoxMsgFmt[] = "'''Warning: cannot update vote sums!'''"; else $storesums = foxpoll_Get_Sums( $data, $alloptions, $datacheck ); // check IP address against IP list of previous votes and block vote if used before if (isset($fx['ip']) && $FoxPollConfig['ip_block'] == true) { $check = foxpoll_IP_Check ($data, $fx['ip']); if ($check == true ) FoxAbort($pagename, "'''Error: Vote submission failed'''"); } // get sums and total votes from PTVs on poll-results page if (empty($sums)) { PageTextVar($tpage, ''); foreach($PCache[$tpage] as $key => $val) { if (substr($key,0,3)!="=p_") continue; if ($key == "=p_VOTES") { $new = substr($key,7); $sums['VOTES'] = $val; } if (substr($key,0,7)=="=p_SUM_") { $new = substr($key,7); $sums[$new] = $val; } } } //compare PTV sums with store sums if ($sums!=$storesums) { $FoxMsgFmt[] = "%red%'''Sums in PTVs do not match sums from data store!'''%%"; if ($datacheck==1) show($sums,'sums'); show($storesums,'storesums'); } // update PTV sums with data store sums (overwrite PTVs) if ($updatesums==1) $sums = $storesums; //DEBUG show($alloptions,'alloptions'); show($sums,'sums'); show($choices,'choices'); //make PTVs with sums of all options and totals of poll inputs if (!empty($sums)) { $fx['ptv_VOTES'] = $sums['VOTES'] = $sums['VOTES'] + 1; foreach ($sums as $key => $val) { if ($key=='VOTES') continue; if (array_key_exists($key, $choices)) $fx['ptv_SUM_'.$key] = $sums[$key] = $val + 1; else $fx['ptv_SUM_'.$key] = $val; } foreach ($pollinputs as $key => $ops) { $total = 0; foreach ($ops as $i => $val) $total = $total + $sums[$key.$sep.$val]; $fx['ptv_TOTL_'.$key] = $total; } } // branch to update of PTVs with store sum values, no vote submission! if ($updatesums==1) { unset($fx['target'], $fx['foxtemplate'], $fx['template'], $fx['vote']); $fx['ptv_VOTES'] = $sums['VOTES'] - 1; $FoxMsgFmt[] = "'''PTVs are updated, no vote submitted.'''"; return $fx; } //convert vote choices into hex number for template var {$$vote} $bin = array_merge($alloptions, $choices); $cnt = count($alloptions) + 1; $b = "1"; //initial 1 so not to loose any leading 0's foreach ($bin as $x) $b .= $x; $fx['vote'] = $hx = base_convert($b,2,16); if ($datacheck==1) { $bs = foxpoll_space_string($b); echo "
$bs :$hx :New vote
"; } if (empty($choices) && $datacheck==1) FoxAbort($pagename, "'''Datacheck, no choices made, nothing posted!'''"); if (strlen($b) !== $cnt) //binary string length not matching poll option number FoxAbort($pagename, "%red%'''Error in data input! Check poll-options are set correctly!'''"); //DEBUG show($fx,'fx last'); exit; return $fx; } // get poll data array from source page location function foxpoll_Get_Data($pagename, $source, $sep) { global $FoxMsgFmt; // get text rows from page section $text = RetrieveAuthSection($pagename, $source); if ($text=='') { $FoxMsgFmt[] = "Error: cannot get csv data from '''$source'''"; return;} $text = trim($text,"\r\n"); $data = explode("\n", $text); foreach ($data as $i => $row) $data[$i] = preg_split('/;\s*/', trim($row,';')); if (empty($data)) { $FoxMsgFmt[] = "Error: Parsing of csv data failed!"; return; } return $data; } // return array with sums of vote choices from first column of csv data // $sums array keys taken from $alloptions (poll-options) function foxpoll_Get_Sums ( $data, $alloptions, $datacheck ) { global $FoxMsgFmt; $bin = array(); $cnt = count($alloptions) + 1; $header = array_shift($data); if ($datacheck==1) echo "data dump: binary conversion from :hex :row"; // make a binary table from first data column foreach ($data as $i => $row) { if (preg_match("/^[a-f0-9]{1,}$/is",$row[0])) { $b = base_convert($row[0],16,2); //convert hex to binary of first column items //intergrity check $n = $i + 1; $bs = array(); if (strlen($b) !== $cnt) $FoxMsgFmt[] = "Warning! Error in vote data on line ".$n; $bin[$i] = $bs = str_split($b); //datacheck: show entire table as binary :hexadec :row if ($datacheck==1) { $b = foxpoll_space_string($b); echo "
$b :$row[0] :$n 
"; } } } $sums['VOTES'] = count($bin); $i = 0; foreach ($alloptions as $key => $o) { $i++; $sums[$key] = array_sum(array_column($bin, $i)); } return $sums; } // helper function used for echo display of data table via polldatacheck // str x, groups of n, leading fill z, default will be trimmed function foxpoll_space_string ($x, $n=4, $z="-") { $k = strlen($x); $m = $k % $n; //modulo (remainder of int division) $y = ($m==0) ? $k : $k + $n - $m; $x = str_pad($x, $y, $z, STR_PAD_LEFT); //add $y of $z left to $x, for groups of $n return trim( wordwrap($x, $n, " ", true), '-'); //now add space after every $n chars } // return an array of input names and their values used for the poll // from the fox poll form markup function foxpoll_Get_Poll_Inputs ($pagename, $fx) { $source = $fx['foxpage']; $n = $fx['foxname']; $pollinputs = preg_split('/,\s*/', $fx['poll-inputs']); $text = RetrieveAuthSection($pagename, $source); $pat = '/\(:fox\\s+'.$n.'(.*?)\(:foxend\\s+'.$n.'\\s?:\)/is'; preg_match($pat, $text, $m); $text = $m[0]; preg_match_all('/\(:input\\s+(.*?)\\s+(.*?):\)/', $text, $m); $inputs = array(); foreach ($m[2] as $k => $str) { $arg = preg_split("/[\s,]+/", $str); foreach ($pollinputs as $i => $id) { if (str_contains($arg[0], $id)) { $inputs[$id][] = $arg[1]; continue 2; } } } return $inputs; } // check ip against logged ip addresses, // return false if not found, true if found and exceeding max repeats allowed function foxpoll_IP_Check ( $data, $ip ) { global $FoxPollConfig; $ipcol = array_search('ip', $data[0]); //identify ip column from header if (isset($ipcol)) { $ips = array_filter(array_column($data, $ipcol)); $ip = trim($ip); $n = 0; foreach ($ips as $i => $v) if ($ip==trim($v)) $n++; if ($n > $FoxPollConfig['ip_max_repeat']) return true; //activate to block vote } return false; } // get ip address from $_SERVER var function get_IP_Address() { $ip_keys = array( // Providers 'HTTP_CF_CONNECTING_IP', // Cloudflare 'HTTP_INCAP_CLIENT_IP', // Incapsula 'HTTP_X_CLUSTER_CLIENT_IP', // RackSpace 'HTTP_TRUE_CLIENT_IP', // Akamai // Proxies 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_CLIENT_IP', 'HTTP_X_REAL_IP', 'HTTP_FORWARDED', 'HTTP_FORWARDED_FOR', // Standard fallback 'REMOTE_ADDR' ); foreach ($ip_keys as $key) { if (empty($_SERVER[$key])) continue; $val = $_SERVER[$key]; if (!empty($val) && is_string($val)) { foreach (explode(',', $val) as $ip) { $ip = trim($ip); // trim for safety measures // attempt to validate IP if ( //filter_var($ip, FILTER_VALIDATE_IP) // any valid IP address filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE | // filter reserved IP addresses FILTER_FLAG_IPV4 // allow only IPv4 addresses ) ) return $ip; } } } return $_SERVER['REMOTE_ADDR']; }