cfg = parse_ini_file('config.ini', $process_sections = true); $this->mysqli = new mysqli( $this->cfg['database']['host'], $this->cfg['database']['user'], $this->cfg['database']['password'], $this->cfg['database']['database']); $this->cfg['database']['password'] = ''; } /** * Utility function to get the event ID for an event token * @param string $token The UUID of the event * @return int The ID if the event was found, 0 otherwise */ function get_event_id(string $token) : int { $query = 'SELECT id FROM events WHERE token=? LIMIT 1'; $stmt = $this->mysqli->prepare($query); $stmt->bind_param('s', $token); $stmt->execute(); $event_id = 0; $stmt->bind_result($event_id); $stmt->fetch(); $stmt->close(); return (int)$event_id; } /** * API endpoint to register an event token * @param mixed $body The JSON object as specified in the API documentation * @return mixed The JSON response object */ function register_event($body) { // check if the event already exists if ($this->get_event_id($body->event_token) !== 0) { return ['success' => false]; } // otherwise, create the event $query = 'INSERT INTO events (token) VALUES (?)'; $stmt = $this->mysqli->prepare($query); $stmt->bind_param('s', $body->event_token); $stmt->execute(); $stmt->close(); return ['success' => true]; } /** * API endpoint to get voting IDs (will be generated on demand if needed) * @param mixed $body The JSON object as specified in the API documentation * @return mixed The JSON response object */ function get_voting_ids($body) { if (($event_id = $this->get_event_id($body->event_token)) === 0) { return ['error' => 'event not found']; } $vote_round = (int)$body->round; $result = []; foreach($body->user_names as $name) { // skip empty names (e.g. if batch processing text files with empty lines) if (strlen($name) > 0) { $hash = $this->get_hash($event_id, $vote_round, $name); $result[] = ['round' => $vote_round, 'user_name' => $name, 'hash' => $hash]; } } return $result; } /** * Utility function to get a specific voting hash * @param int $event_id The ID of the event * @param int $vote_round The vote round * @param string $name The name of the delegatee * @return string The voting hash */ function get_hash(int $event_id, int $vote_round, string $name) : string { $query = 'SELECT hash FROM hashes WHERE event=? AND vote_round=? AND name=? LIMIT 1'; $stmt = $this->mysqli->prepare($query); $stmt->bind_param('iis', $event_id, $vote_round, $name); $stmt->execute(); $hash = ''; $stmt->bind_result($hash); $stmt->fetch(); $stmt->close(); if (strlen($hash) == 0) return $this->create_hash($event_id, $vote_round, $name); else return $hash; } /** * Utility function to generate a new hash * @param int $event_id The ID of the event * @param int $vote_round The vote round * @param string $name The name of the delegatee * @return string The voting hash */ function create_hash(int $event_id, int $vote_round, string $name) : string { // sanitize the inputs which will go to the shell $vote_round = (int)$vote_round; $name = addslashes($name); // generate the hash (the PHP password_hash function does not provide the required options) $hash = shell_exec("echo -n '$name' | argon2 'Abstimmung $vote_round' \ -p {$this->cfg['argon2']['threads']} \ -m {$this->cfg['argon2']['memory']} \ -t {$this->cfg['argon2']['iterations']} \ -l {$this->cfg['argon2']['length']} -id -r"); $hash = str_replace(array("\n", "\r"), '', $hash); // insert the new hash into the database $query = 'INSERT INTO hashes (event, vote_round, name, hash) VALUES (?, ?, ?, ?)'; $stmt = $this->mysqli->prepare($query); $stmt->bind_param('iiss', $event_id, $vote_round, $name, $hash); $stmt->execute(); $stmt->close(); return $hash; } /** * API endpoint to generate a voting result textfile and PDF * @param mixed $body The JSON object as specified in the API documentation * @return mixed The JSON response object */ function export_result($body) { if ($this->get_event_id($body->event_token) === 0) { return ['success' => false]; } // if a raw JSON export is configured, write the data if (strlen($this->cfg['export']['rawpath']) > 0) { $filepath = "{$this->cfg['export']['rawpath']}/{$body->event_token}-{$body->vote_round}.json"; file_put_contents($filepath, json_encode($body)); } // sanitize the inputs which will go to the filesystem path $vote_round = (int)$body->vote_round; $event_title = str_replace(' ', '_', addslashes($body->event_title)); $file_path = $this->cfg['export']['path'] . '/' . date('Y-m-d') . "__{$event_title}__Abstimmung-$vote_round"; $sha256_txt = $this->create_text_file($body, "$file_path.txt"); $sha256_pdf = $this->create_pdf_file($body, "$file_path.pdf"); return [ 'success' => true, 'hash_txt' => $sha256_txt, 'hash_pdf' => $sha256_pdf, 'signing_key' => $this->cfg['export']['pgpkey']]; } /** * Utility function to create a signed voting result text file * @param mixed $body The complete JSON object from the API endpoint * @param string $file_path The path and name of the output file * @return string The SHA256 hash of the file created */ function create_text_file($body, string $file_path) : string { // create the file's header $header = str_replace('%t', $body->event_title, $this->cfg['export']['header']); $header = str_replace('%d', date('d.m.Y'), $header); $header = str_replace('%r', $body->vote_round, $header); $file_data = <<votes as $vote) { $file_data .= $vote->hash . ' | ' . implode(', ', $vote->vote) . "\n"; } $file_data .= "\n"; // write the file and create a detached GPG signature file_put_contents($file_path, $file_data); shell_exec("gpg --sign '$file_path'"); // synchronize both files $this->sync_command($file_path); $this->sync_command($file_path.".gpg"); // return the hash of the text file return trim(shell_exec("/usr/bin/sha256sum '$file_path' | awk '{ print $1 }'")); } /** * Utility function to create a signed voting result PDF file * @param mixed $body The complete JSON object from the API endpoint * @param string $file_path The path and name of the output file * @return string The SHA256 hash of the file created */ function create_pdf_file($body, string $file_path) : string { // get the maximum number of votes a single person did to determine the number of columns $col_count = 0; foreach ($body->votes as $vote) if (count($vote->vote) > $col_count) $col_count = count($vote->vote); // if no columns (or too many), stop the pdf process if ($col_count < 1 || $col_count > 5) return ''; // build the table header variables $table_col_count = str_repeat('|l', $col_count); $table_col_headers = ''; for ($i = 0; $i < $col_count; $i++) $table_col_headers .= ' & \textbf{Stimme '.($i+1).'}'; // build the table body with the votes $table_body = ''; foreach ($body->votes as $vote) { // start with the vote hash $table_body .= " $vote->hash"; // continue with the existing votes $cols_done = 0; foreach ($vote->vote as $v){ $table_body .= " & $v"; $cols_done++; } // fill the empty columns, if any for ($i = $cols_done; $i < $col_count; $i++) $table_body .= ' &'; // end the line $table_body .= " \\\\\n"; } // read in the template file if (($tmpl = file_get_contents($this->cfg['export']['pdf_tmpl'])) === false) throw new Exception('PDF template not found'); // do the template variable replacements $tmpl = str_replace('==EVENT_NAME==', $body->event_title, $tmpl); $tmpl = str_replace('==VOTE_ROUND==', $body->vote_round, $tmpl); $tmpl = str_replace('==COL_COUNT==', $table_col_count, $tmpl); $tmpl = str_replace('==COL_HDRS==', $table_col_headers, $tmpl); $tmpl = str_replace('==TABLE_BODY==', $table_body, $tmpl); $fdir = pathinfo($file_path, PATHINFO_DIRNAME).'/'; // just the directory part $fname = pathinfo($file_path, PATHINFO_FILENAME); // filename without extension $ffull = $fdir.$fname; // write the temporary tex file and convert it to pdf if (file_put_contents("$ffull.tex", $tmpl) === false) throw new Exception('Could not write tex file'); shell_exec("cd $fdir; /usr/bin/pdflatex $fname.tex"); // remove the temporary files unlink("$ffull.tex"); unlink("$ffull.log"); unlink("$ffull.aux"); // TODO: sign the pdf file shell_exec("gpg --sign '$ffull'.pdf"); // synchronize the file $this->sync_command("$ffull.pdf"); // return the hash of the file return trim(shell_exec("/usr/bin/sha256sum '$file_path' | awk '{ print $1 }'")); } /** * Utility function to synchronize a specific file with a remote location, * with a command specified in the config.ini file * @param string $file_path The path to the file * @return none */ function sync_command(string $file_path) { if (isset($this->cfg['export']['synccmd'])) { $cmd = $this->cfg['export']['synccmd']; if (strlen($cmd) > 0) { $cmd = str_replace('%f', $file_path, $cmd); shell_exec($cmd); } } } }