add documentation to functions and:
- use single quotes where possible - explicitly initialize mysql bind variables - change the sync_command() to one file per call - sanitize inputs for export_result endpoint - add JSON decode error handling
This commit is contained in:
parent
8743fd12a5
commit
5f7db5f8ea
|
@ -23,6 +23,5 @@ rawpath = /path/to/json/dir
|
|||
; path in which signed files will be stored
|
||||
path = /path/to/dir
|
||||
; synccmd can be empty, if no synchronization command is needed. Placeholders:
|
||||
; %s - signed file
|
||||
; %u - unsigned file
|
||||
;synccmd = rclone %s Remote:dir
|
||||
; %f - filename
|
||||
;synccmd = rclone %f Remote:dir
|
||||
|
|
229
functions.php
229
functions.php
|
@ -14,74 +14,82 @@ class AbstimmIDd {
|
|||
$this->cfg['database']['password'] = '';
|
||||
}
|
||||
|
||||
function get_voting_ids($body) {
|
||||
$event_id = $this->get_event_id($body->event_token);
|
||||
$vote_round = $body->round;
|
||||
$result = [];
|
||||
if (!$event_id) {
|
||||
return ["error" => "event not found"];
|
||||
/**
|
||||
* 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];
|
||||
$result[] = ['round' => $vote_round, 'user_name' => $name, 'hash' => $hash];
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
function get_event_id($token) {
|
||||
$query = "SELECT id FROM events WHERE token=? LIMIT 1";
|
||||
$stmt = $this->mysqli->prepare($query);
|
||||
$stmt->bind_param('s', $token);
|
||||
$stmt->bind_result($event_id);
|
||||
$stmt->execute();
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
return $event_id;
|
||||
}
|
||||
|
||||
function register_event($body) {
|
||||
if ($this->get_event_id($body->event_token)) {
|
||||
return ["success" => false];
|
||||
}
|
||||
$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];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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";
|
||||
$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->bind_result($hash);
|
||||
$stmt->execute();
|
||||
|
||||
$hash = '';
|
||||
$stmt->bind_result($hash);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
|
||||
|
@ -91,52 +99,107 @@ class AbstimmIDd {
|
|||
return $hash;
|
||||
}
|
||||
|
||||
function export_result($body) {
|
||||
if (!$this->get_event_id($body->event_token)) {
|
||||
return ["success" => false];
|
||||
}
|
||||
$sha256 = $this->create_text_file($body);
|
||||
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));
|
||||
}
|
||||
return ["success" => true, "sha256" => $sha256, "signing_key" => $this->cfg["export"]["pgpkey"]];
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
function create_header($body) {
|
||||
$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);
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
|
||||
return <<<EOT
|
||||
$sha256 = $this->create_text_file($body);
|
||||
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));
|
||||
}
|
||||
return ['success' => true, 'sha256' => $sha256, '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
|
||||
* @return string The SHA256 hash of the file created
|
||||
*/
|
||||
function create_text_file($body) : string {
|
||||
// sanitize the inputs which will go to the shell
|
||||
$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.txt";
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
function create_text_file($body) {
|
||||
$file_path = $this->cfg["export"]["path"] . "/" . date('Y-m-d') . "__" .
|
||||
str_replace(" ", "_", $body->event_title) . "__Abstimmung-" . $body->vote_round . ".txt";
|
||||
|
||||
$file_data = $this->create_header($body);
|
||||
// append the votes
|
||||
foreach ($body->votes as $vote) {
|
||||
$file_data .= $vote->hash . " | " . implode(", ", $vote->vote) . "\n";
|
||||
$file_data .= $vote->hash . ' | ' . implode(', ', $vote->vote) . "\n";
|
||||
}
|
||||
$file_data .= "\n";
|
||||
file_put_contents($file_path, $file_data);
|
||||
|
||||
// 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 }'"));
|
||||
}
|
||||
|
||||
function sync_command($file_path) {
|
||||
if (isset($this->cfg["export"]["synccmd"])) {
|
||||
$cmd = $this->cfg["export"]["synccmd"];
|
||||
/**
|
||||
* 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("%s", $file_path . ".gpg", $cmd);
|
||||
$cmd = str_replace("%u", $file_path, $cmd);
|
||||
$cmd = str_replace('%f', $file_path, $cmd);
|
||||
shell_exec($cmd);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,9 @@ header('Content-Type: application/json');
|
|||
try {
|
||||
|
||||
$aidd = new AbstimmIDd();
|
||||
$body = json_decode(file_get_contents('php://input'));
|
||||
|
||||
if (($body = json_decode(file_get_contents('php://input'))) === null)
|
||||
throw new Exception("invalid JSON");
|
||||
|
||||
if(ends_with($_SERVER["REQUEST_URI"], '/get_ids')) {
|
||||
$data = $aidd->get_voting_ids($body);
|
||||
|
@ -15,7 +17,7 @@ try {
|
|||
} else if(ends_with($_SERVER["REQUEST_URI"], '/export_result')) {
|
||||
$data = $aidd->export_result($body);
|
||||
} else {
|
||||
$data = ["error" => "no route"];
|
||||
throw new Exception("no route");
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
|
|
Loading…
Reference in New Issue