298 lines
9.2 KiB
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|