abstimm-idD/functions.php

298 lines
9.2 KiB
PHP

<?php
class AbstimmIDd {
private $cfg;
private $mysqli;
function __construct() {
$this->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 = <<<EOT
$header\n
Abstimm-ID | Stimme(n)
#################################|#############\n
EOT;
// append the votes
foreach ($body->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);
}
}
}
}