瀏覽代碼

2023 Changes - Added setup utility, skymanager data export, and more generic voting controls

2023
Tyler Dence 2 年之前
父節點
當前提交
fbdb8f84fa
簽署人: Tyler Dence <tyzoid.d@gmail.com> GPG Key ID: 3B08EFC6BA974CFC
共有 17 個文件被更改,包括 590 次插入106 次删除
  1. 10
    0
      cli/process.sh
  2. 3
    0
      cli/sqlize.sh
  3. 40
    0
      cli/userlist.sh
  4. 27
    6
      web/admin/checkin.php
  5. 19
    20
      web/admin/paper.php
  6. 2
    2
      web/admin/results.php
  7. 202
    0
      web/admin/voting.php
  8. 162
    0
      web/configure.php
  9. 19
    8
      web/inc/db.php
  10. 9
    0
      web/inc/inc.php
  11. 10
    3
      web/inc/user.php
  12. 11
    36
      web/index.php
  13. 2
    0
      web/js/admin-search.js
  14. 1
    1
      web/login.php
  15. 58
    1
      web/styles/style.css
  16. 4
    2
      web/templates/header.php
  17. 11
    27
      web/vote.php

+ 10
- 0
cli/process.sh 查看文件

@@ -0,0 +1,10 @@
#!/bin/bash

process() {
tr -dc '\n\040-\176' \
| grep -oe '<div><b>[^<]*' -oe '<div>Email <a href="mailto:[^"]*' -oe '<span class="tag">[^<]*' -oe '<li><a href="/Roster/Member/[0-9]*' -oe '<div>Balance \$.*</div>' | tee stage-a.dat \
| sed 's/^\s*//;s%^<div><b>\([^(]*\) (\([^)]*\)).*$%{"name": "\1", "username": "\2", "tags": [%;s%^<div><b>\([^<]*\).*$%{"username": "\1", "tags": [%;s%^<span class="tag">\([^<]*\).*$%"\1",%;s%^<li><a href="/Roster/Member/\([0-9]*\).*$%], "smid": \1}%;s%<div>Email.*mailto:\([^"]*\).*$%"email": "\1",%;s%^<div>Balance \$\([0-9,.]\+\).*$%"balance": \1,%;s%^<div>Balance \$<span[^>]*>(\([0-9,.]\+\)).*$%"balance": -\1,%;s%\("balance": -\?[0-9]\+\),\([0-9,.]\+\), %\1\2, %;s%\("balance": -?[0-9]\+\),\([0-9.]\+\), %\1\2, %;s%^<div>Balance $&nbsp;-&nbsp;.*$%"balance": 0,%' | tee stage-b.dat \
| tr -d '\n' | tee stage-c.dat \
| sed 's%"tags": \[\("email": [^,]*,\)%\1 "tags": [%g;s%,\]%]%g;s%"tags": \[\("balance"[^],]*,\)%\1 "tags": [%g;s%"tags": \[\("balance"[^],]*\)\]%\1, "tags": []%g' | tee stage-d.dat \
| jq -c . | tee stage-e.dat
}

+ 3
- 0
cli/sqlize.sh 查看文件

@@ -0,0 +1,3 @@
#!/bin/bash
echo -n "INSERT IGNORE INTO members (voting_id, skymanager_id, name, username, email) VALUES "
paste -d '' <(seq 100 999 | shuf | head -n $(wc -l < voters.json) | sed 's/^/(/') <(jq -r '", " + (.smid|tostring) + ", \"" + (.name // .username) + "\", \"" + .username + "\", " + (if .email then ("\"" + .email + "\"") else "NULL" end) + ")"' voters.json) | tr '\n' ',' | sed 's/,$/;\n/'

+ 40
- 0
cli/userlist.sh 查看文件

@@ -0,0 +1,40 @@
#!/bin/bash
read -p "Skymanager Username: " username
read -sp "Skymanager Password: " password; echo

token="$(curl -vvv 'https://umflyers.skymanager.com/Home/LogIn?ReturnUrl=%2f' -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data-urlencode "Username=$username" --data-urlencode "Password=$password" --data-urlencode "RememberMe=false" 2>&1 | grep -o 'Set-Cookie: .ASPXAUTH[^;]*' | cut -f2- -d' ')";

. process.sh

help() {
cat <<- _END_
userlist help:

-h Shows this help menu
-r Reprocess existing fetched data
_END_
}

reprocess=false
while getopts "hr" opt; do
case "$opt" in
r) reprocess=true ;;
h) help
exit ;;
esac
done

if ! $reprocess; then
mkdir -p tmp
pushd tmp
for letter in {A..Z}; do
printf "%s\n" $letter >&2;
curl -s "https://umflyers.skymanager.com/Roster/Letter/$letter" -H "Cookie: $token" \
| tee "raw-$letter.html" | process
done | tee ../results.json > /dev/null

popd
fi

echo -n "Eligible voters: "
jq -c '. | select(.balance >= 0) | select(.tags | contains(["Flying"]) or contains(["Honorary Dues"]) or contains(["CFI/Mechanic"]) or contains(["CFI/MECH/DIRECTORS Dues"]))' results.json | tee voters.json | wc -l

+ 27
- 6
web/admin/checkin.php 查看文件

@@ -11,22 +11,43 @@ if ($user->getRole() !== "admin") {
die();
}

if (!empty($_POST['voter']) && ((int) $_POST['voter']) == $_POST['voter']) {
if (isset($_POST['voter'])) {
$voter = (int) $_POST['voter'];
$result = $db->query("update members set checkedin=true where voting_id=$voter");
if (!empty($_POST['voter']) && ((int) $_POST['voter']) == $_POST['voter']) {
$result = $db->query("update members set checkedin=true where voting_id=$voter");
} else {
$result = false;
$error = "The selected voter is not eligible";
}
}

$header = new Header("2022 Michigan Flyers Election : Poll Worker");
$header = new Header("Michigan Flyers Election : Poll Worker");
$header->addStyle("/styles/style.css");
$header->addStyle("/styles/admin.css");
$header->addStyle("/styles/vote.css");
$header->addScript("/js/jquery-1.11.3.min.js");
$header->addScript("/js/admin-search.js");
$header->setAttribute('title', 'Michigan Flyers');
$header->setAttribute('tagline', '2022 Election Administration');
$header->setAttribute('tagline', 'Election Poll Worker Tools');
$header->output();

$voters = $db->fetchAssoc('select ANY_VALUE(skymanager_id) as `skymanager_id`, ANY_VALUE(members.voting_id) as `voting_id`, ANY_VALUE(name) as `name`, ANY_VALUE(username) as `username`, group_concat(proxy.voting_id) as `proxies`, ANY_VALUE(upstream_proxy.delegate_id) as `delegate`, md5(coalesce(ANY_VALUE(email), "")) as `gravatar_hash` from members left join proxy on (members.voting_id=proxy.delegate_id) left join proxy as upstream_proxy on (upstream_proxy.voting_id=members.voting_id) where members.voting_id is not null group by members.voting_id UNION select skymanager_id, voting_id, name, username, NULL as `proxies`, NULL as `delegate`, md5(coalesce(email, "")) as `gravatar_hash` from members where members.voting_id is null');
$voters = $db->fetchAssoc('
select
MIN(skymanager_id) as `skymanager_id`,
MIN(members.voting_id) as `voting_id`,
MIN(name) as `name`,
MIN(username) as `username`,
group_concat(proxy.voting_id) as `proxies`,
MIN(upstream_proxy.delegate_id) as `delegate`,
md5(coalesce(MIN(email), "")) as `gravatar_hash`
from members
left join proxy on (members.voting_id=proxy.delegate_id)
left join proxy as upstream_proxy on (upstream_proxy.voting_id=members.voting_id)
where members.voting_id is not null
group by members.voting_id
UNION
select skymanager_id, voting_id, name, username, NULL as `proxies`, NULL as `delegate`, md5(coalesce(email, "")) as `gravatar_hash`
from members where members.voting_id is null');
?>
<script type="text/javascript">
var voters = <?= json_encode($voters); ?>;
@@ -60,7 +81,7 @@ var voters = <?= json_encode($voters); ?>;
<input class="submit" type="submit" name="submit" value="Check In" />
</div>
</form>
<?php if (!empty($voter)): ?>
<?php if (isset($voter)): ?>
<div id="vote-result">
<div id="status" class="<?= $result ? "success" : "failure"; ?>"></div>
<div id="message" class="<?= $result ? "success" : "failure"; ?>">

+ 19
- 20
web/admin/paper.php 查看文件

@@ -11,6 +11,12 @@ if ($user->getRole() !== "admin") {
die();
}

$_pos = $db->fetchAssoc("select position as code, description as label from positions");
$positions = [];
foreach ($_pos as $position)
$positions[$position['code']] = $position['label'];

$result = null;
if (!empty($_POST['ballot']) && !empty($_POST['candidate'])) {
$candidate_selected = (int) $_POST['candidate'];
$voter_selected = (int) $_POST['voter'];
@@ -18,7 +24,7 @@ if (!empty($_POST['ballot']) && !empty($_POST['candidate'])) {

if ($candidate_selected != $_POST['candidate']) $error = "An eccor occurred while processing your ballot. Please retry.";
if ($voter_selected != $_POST['voter']) $error = "An eccor occurred while processing your ballot. Please retry.";
if ($ballot !== "VICEPRESIDENT" && $ballot !== "SECRETARY" && $ballot !== "DIRECTOR") $error = "An eccor occurred while processing your ballot. Please retry.";
if (!array_key_exists($ballot, $positions)) $error = "An eccor occurred while processing your ballot. Please retry.";

if (empty($error)) {
$result = $db->query("INSERT INTO votes (candidate_id, position, member_id, vote_type, submitter_id) SELECT $candidate_selected, \"$ballot\", $voter_selected, 'IN PERSON', $voter_selected UNION SELECT $candidate_selected, \"$ballot\", voting_id, 'PROXY IN PERSON', delegate_id from proxy where delegate_id=$voter_selected");
@@ -40,13 +46,7 @@ if (!empty($_POST['ballot']) && !empty($_POST['candidate'])) {
}
}

$positions = [
'VICEPRESIDENT' => 'Vice President',
'SECRETARY' => 'Secretary',
'DIRECTOR' => 'Director'
];

$header = new Header("2022 Michigan Flyers Election : Poll Worker");
$header = new Header("Michigan Flyers Election : Poll Worker");
$header->addStyle("/styles/style.css");
$header->addStyle("/styles/admin.css");
$header->addStyle("/styles/vote.css");
@@ -54,11 +54,11 @@ $header->addScript("/js/jquery-1.11.3.min.js");
$header->addScript("/js/search.js");
$header->addScript("/js/admin-search.js");
$header->setAttribute('title', 'Michigan Flyers');
$header->setAttribute('tagline', '2022 Election Administration');
$header->setAttribute('tagline', 'Election Poll Worker Tools');
$header->output();

$candidates = $db->fetchAssoc('select skymanager_id, name, username, md5(coalesce(email, "")) as `gravatar_hash` from members where voting_id is not null');
$voters = $db->fetchAssoc('select ANY_VALUE(skymanager_id) as `skymanager_id`, ANY_VALUE(members.voting_id) as `voting_id`, ANY_VALUE(name) as `name`, ANY_VALUE(username) as `username`, group_concat(proxy.voting_id) as `proxies`, ANY_VALUE(upstream_proxy.delegate_id) as `delegate`, md5(coalesce(ANY_VALUE(email), "")) as `gravatar_hash` from members left join proxy on (members.voting_id=proxy.delegate_id) left join proxy as upstream_proxy on (upstream_proxy.voting_id=members.voting_id) where members.voting_id is not null group by members.voting_id UNION select skymanager_id, voting_id, name, username, NULL as `proxies`, NULL as `delegate`, md5(coalesce(email, "")) as `gravatar_hash` from members where members.voting_id is null');
$voters = $db->fetchAssoc('select MIN(skymanager_id) as `skymanager_id`, MIN(members.voting_id) as `voting_id`, MIN(name) as `name`, MIN(username) as `username`, group_concat(proxy.voting_id) as `proxies`, MIN(upstream_proxy.delegate_id) as `delegate`, md5(coalesce(MIN(email), "")) as `gravatar_hash` from members left join proxy on (members.voting_id=proxy.delegate_id) left join proxy as upstream_proxy on (upstream_proxy.voting_id=members.voting_id) where members.voting_id is not null group by members.voting_id UNION select skymanager_id, voting_id, name, username, NULL as `proxies`, NULL as `delegate`, md5(coalesce(email, "")) as `gravatar_hash` from members where members.voting_id is null');
?>
<script type="text/javascript">
var voters = <?= json_encode($voters); ?>;
@@ -81,20 +81,18 @@ var candidates = <?= json_encode($candidates); ?>;
</label>
</div>
</div>
<?php if (empty($positions)): ?>
<h3>No positions are open for voting.</h3>
<a href="/admin/voting.php">Create a position</a>
<?php else: ?>
<div class="form-row">
<div class="selector">
<?php foreach ($positions as $code => $label): ?>
<label class="radio">
<input type="radio" id="vote-vicepresident" name="ballot" value="VICEPRESIDENT" checked />
<span class="radio-button-label">Vice President</span>
</label>
<label class="radio">
<input type="radio" id="vote-secretary" name="ballot" value="SECRETARY" checked />
<span class="radio-button-label">Secretary</span>
</label>
<label class="radio">
<input type="radio" id="vote-director" name="ballot" value="DIRECTOR" />
<span class="radio-button-label">Director-At-Large</span>
<input type="radio" id="vote-<?= $code ?>" name="ballot" value="<?= $code ?>" checked />
<span class="radio-button-label"><?= $label ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="form-row">
@@ -156,6 +154,7 @@ var candidates = <?= json_encode($candidates); ?>;
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
<?php
$footer = new Footer();
$footer->output();

+ 2
- 2
web/admin/results.php 查看文件

@@ -16,14 +16,14 @@ if (!empty($_POST['voter']) && ((int) $_POST['voter']) == $_POST['voter']) {
$result = $db->query("update members set checkedin=true where voting_id=$voter");
}

$header = new Header("2022 Michigan Flyers Election : Poll Worker");
$header = new Header("Michigan Flyers Election : Poll Worker");
$header->addStyle("/styles/style.css");
$header->addStyle("/styles/admin.css");
$header->addStyle("/styles/vote.css");
$header->addScript("/js/jquery-1.11.3.min.js");
$header->addScript("/js/admin-search.js");
$header->setAttribute('title', 'Michigan Flyers');
$header->setAttribute('tagline', '2022 Election Administration');
$header->setAttribute('tagline', 'Election Poll Worker Tools');
$header->output();

$checkedin = $db->fetchAssoc('select name, username, voting_id, NULL as `proxy` from members where checkedin=true UNION select voter.name, voter.username, voter.voting_id, members.voting_id as `proxy` from members inner join proxy on (proxy.delegate_id=members.voting_id) left join members as `voter` on (voter.voting_id=proxy.voting_id) where members.checkedin = true');

+ 202
- 0
web/admin/voting.php 查看文件

@@ -0,0 +1,202 @@
<?php
include('../inc/inc.php');

if (!$user->loggedin()) {
header('Location: /login.php');
die();
}

if ($user->getRole() !== "admin") {
header('Location: /index.php');
die();
}

function loadPositions() {
global $db;

$_pos = $db->fetchAssoc("select position as code, description as label, active from positions");
$positions = [];
foreach ($_pos as $position)
$positions[$position['code']] = [ "label" => $position['label'], "active" => $position['active']];

return $positions;
}

function loadVoters() {
global $db;

return $db->fetchAssoc('
select
MIN(skymanager_id) as `skymanager_id`,
MIN(members.voting_id) as `voting_id`,
MIN(name) as `name`,
MIN(username) as `username`,
group_concat(proxy.voting_id) as `proxies`,
MIN(upstream_proxy.delegate_id) as `delegate`,
md5(coalesce(MIN(email), "")) as `gravatar_hash`
from members
left join proxy on (members.voting_id=proxy.delegate_id)
left join proxy as upstream_proxy on (upstream_proxy.voting_id=members.voting_id)
where members.voting_id is not null
group by members.voting_id
UNION
select skymanager_id, voting_id, name, username, NULL as `proxies`, NULL as `delegate`, md5(coalesce(email, "")) as `gravatar_hash`
from members where members.voting_id is null');
}

$result = null;
// Create Position
if (!empty($_POST['create'])) {
$code = $_POST['add-position'];
$desc = $_POST['add-description'];

if (empty($code) || empty($desc)) $error = "Both code and description are required";

if (empty($error)) {
$result = $db->query('INSERT INTO positions (position, description) VALUES ("' . $db->sanitize($code) . '", "' . $db->sanitize($desc) . '")');
if ($result === false)
$error = "That position already exists.";
else
$error = "Created position " . htmlspecialchars($desc);
}
} else if (!empty($_POST['setActive']) || !empty($_POST['deactivate'])) {
$positions = loadPositions();

$position = $_POST['ballot'];
if (!array_key_exists($position, $positions)) $error = "That position does not exist";
if (!empty($_POST['deactivate']))
$position='';

if (empty($error)) {
$result = $db->query('UPDATE positions set active=(position="' . $db->sanitize($position) . '")');
if ($result === false)
$error = "Failed to set active position";
else if (empty($position))
$error = "Deactivated voting form";
else
$error = "Set " . htmlspecialchars($positions[$_POST['ballot']]['label']) . " as active.";
}
} else if (!empty($_POST['remove'])) {
$positions = loadPositions();

if (!array_key_exists($_POST['ballot'], $positions)) $error = "That position does not exist";

if (empty($error)) {
$result = $db->query('DELETE FROM positions WHERE position="' . $db->sanitize($_POST['ballot']) . '"');
if ($result === false)
$error = "Failed to remove position";
else
$error = "Removed " . htmlspecialchars($positions[$_POST['ballot']]['label']) . " and discarded cast ballots.";
}
} else if (!empty($_POST['force'])) {
$voters = loadVoters();
$values = [];
$vid = [];
foreach ($voters as $voter) {
$vid[$voter['skymanager_id']] = $voter;

if ($voter['voting_id'])
array_push($values, $voter['voting_id']);
}

if (!array_key_exists($_POST['voter-smid'], $vid))
$error = "Voter does not exist";

sort($values);
$count = count($values);
$rand = rand(100, 999-$count);
for ($i = 0; $i < $count && $values[$i] <= $rand; $i++) {
$rand++;
}

if (empty($error)) {
$result = $db->query('UPDATE members set voting_id=' . ((int) $rand) . ' where skymanager_id=' . ((int) $_POST['voter-smid']));
if ($result === false)
$error = "Failed to force check-in. Please try again.";
else
$error = "Assigned voting id $rand to {$vid[$_POST['voter-smid']]['name']}";
}
}

$positions = loadPositions();
$voters = loadVoters();

$header = new Header("Michigan Flyers Election : Admin");
$header->addStyle("/styles/style.css");
$header->addStyle("/styles/admin.css");
$header->addStyle("/styles/vote.css");
$header->addScript("/js/jquery-1.11.3.min.js");
$header->addScript("/js/search.js");
$header->addScript("/js/admin-search.js");
$header->setAttribute('title', 'Michigan Flyers');
$header->setAttribute('tagline', 'Election Administration Tools');
$header->output();

?>
<script type="text/javascript">
var voters = <?= json_encode($voters); ?>;
</script>
<form action="voting.php" method="POST">
<?php if (!empty($error) || !empty($result)): ?>
<div id="vote-result">
<div id="status" class="<?= $result ? "success" : "failure"; ?>"></div>
<div id="message" class="<?= $result ? "success" : "failure"; ?>">
<?= !empty($error) ? $error : ($result ? "This Ballot has been successfully Submitted" :
"This ballot has already been submitted.") ?>
</div>
</div>
<?php endif; ?>
<?php if (!empty($positions)): ?>
<div class="form-section">
<h3>Manage Positions</h3>
<div class="form-row">
<div class="selector">
<?php foreach ($positions as $code => $position): ?>
<label class="radio">
<input type="radio" id="vote-<?= $code ?>" name="ballot" value="<?= $code ?>" <?= ($position['active']) ? 'checked' : '' ?> />
<span class="radio-button-label"><?= $position['label'] ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="form-row split-button">
<input class="submit danger" type="submit" name="remove" value="Remove Position & Ballots" />
<input class="submit" type="submit" name="deactivate" value="Deactivate" />
<input class="submit" type="submit" name="setActive" value="Set Active" />
</div>
</div>
<?php endif; ?>
<div class="form-section">
<h3>Add Position</h3>
<div class="form-row">
<label for="add-position">Add Position Code</label>
<input type="text" placeholder="PRES" id="add-position" name="add-position" value="" />
</div>
<div class="form-row">
<label for="add-description">Add Position Description</label>
<input type="text" placeholder="President" id="add-description" name="add-description" value="" />
</div>
<div class="form-row">
<input class="submit" type="submit" name="create" value="Create Position" />
</div>
</div>
<div class="form-section">
<h3>Force Check-In</h3>
<div class="form-row">
<input type="text" placeholder="Voter Search" id="voter-searchbox" name="voter-searchbox" value="" />
<div id="voter-results"></div>
<input type="hidden" name="voter" id="voter-input" value="0" />
<input type="hidden" name="voter-smid" id="voter-smid" value="0" />
<div id="selectedVoter" class="selected candidate voter">
<span class="placeholder">No Selected Voter</span>
</div>
</div>
<div class="form-row">
<input class="submit" type="submit" name="force" value="Force Check In" />
</div>
</div>
</form>
<?php
$footer = new Footer();
$footer->output();

+ 162
- 0
web/configure.php 查看文件

@@ -0,0 +1,162 @@
<?php
define('BASE', __DIR__);
define('BASEURL', $_SERVER['SERVER_NAME']);

$config = json_decode(file_get_contents(BASE . "/inc/config.json"));
if (!empty($config)) {
header('Location: /index.php');
die();
}

require_once(BASE . '/inc/db.php');
require_once(BASE . '/inc/user.php');

$required = ['db-host', 'db-username', 'db-password', 'db-database', 'flyers-user', 'flyers-password'];

function test_config($params) {
global $required, $db, $user;

if (!empty($params) && count($params) != count($required))
return "All fields are required";

mysqli_report(MYSQLI_REPORT_OFF);
$mysql = mysqli_connect($params['db-host'], $params['db-username'], $params['db-password']);
if (!$mysql)
return "Unable to connect to the database.";

mysqli_select_db($mysql, $params['db-database']);
if (mysqli_error($mysql))
return "Unable to access database '" . htmlspecialchars($params['db-database']) . "': " . mysqli_error($mysql);

mysqli_multi_query($mysql, "
CREATE TABLE IF NOT EXISTS `members` (
`skymanager_id` integer NOT NULL PRIMARY KEY,
`name` varchar(128) NOT NULL,
`username` varchar(64) NOT NULL,
`voting_id` int DEFAULT NULL UNIQUE,
`email` varchar(128) DEFAULT NULL,
`pollworker` BOOLEAN NOT NULL DEFAULT false,
`checkedin` BOOLEAN NOT NULL DEFAULT false);

CREATE TABLE IF NOT EXISTS `proxy` (
`voting_id` integer NOT NULL,
`delegate_id` integer NOT NULL,
PRIMARY KEY (`voting_id`, `delegate_id`));

CREATE TABLE IF NOT EXISTS `positions` (
`position` varchar(64) NOT NULL PRIMARY KEY,
`description` varchar(128) NOT NULL UNIQUE,
`active` BOOLEAN NOT NULL DEFAULT false
);

CREATE TABLE IF NOT EXISTS `votes` (
`candidate_id` integer NOT NULL,
`position` varchar(64) NOT NULL,
`member_id` integer NOT NULL,
`vote_type` enum('IN PERSON','ONLINE','PROXY IN PERSON','PROXY ONLINE','UNANIMOUS') NOT NULL DEFAULT 'ONLINE',
`submitted_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`submitter_id` integer NOT NULL,
PRIMARY KEY (`position`,`member_id`),
FOREIGN KEY (`position`) REFERENCES `positions` (`position`) ON DELETE CASCADE)
");

do {
if (mysqli_error($mysql))
return "Unable to set up tables: " . mysqli_error($mysql);
} while (mysqli_next_result($mysql) || mysqli_error($mysql));

$db = DBHandler::wrap($mysql);
$success = $user->login($params['flyers-user'], $params['flyers-password']);
if (!$success)
return "Login Failed";

$db->query("UPDATE members SET `pollworker`=TRUE where skymanager_id=" . ((int) $user->getUserId()));
if ($db->getError())
return "Failed to update user permissions";

$conf = json_encode([
'host' => $params['db-host'],
'user' => $params['db-username'],
'pass' => $params['db-password'],
'db' => $params['db-database']
], JSON_PRETTY_PRINT);

if (file_put_contents(BASE . "/inc/config.json", $conf) === false)
return "Failed to write configuration.";

return false;
}

$params = [];
foreach ($required as $field) {
if (array_key_exists($field, $_POST) && !empty($_POST[$field]))
$params[$field] = $_POST[$field];
}

$error = null;
if (!empty($params))
$error = test_config($params);

if ($error === false) {
header('Location: /index.php');
die();
}

?>
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;600;800&display=swap" />
<link rel="stylesheet" type="text/css" href="/styles/style.css" />
</head>
<body>
<div id="container">
<div class="header">
<h1>Michigan Flyers</h1>
<h2>Voting System Setup</h2>
</div>
<div class="content">
<div class="page">
<?php if(!empty($error)) echo "<span class=\"errormessage\">$error</span>"; ?>
<form action="configure.php" method="POST">
<div class="form-section">
<h3>Database Setup</h3>
<div class="form-row">
<label for="db-host">Host</label>
<input type="text" id="db-host" name="db-host" value="localhost" />
</div>
<div class="form-row">
<label for="db-database">Database Name</label>
<input type="text" id="db-database" name="db-database" />
</div>
<div class="form-row">
<label for="db-username">Username</label>
<input type="text" id="db-username" name="db-username" />
</div>
<div class="form-row">
<label for="db-password">Password</label>
<input type="password" id="db-password" name="db-password" />
</div>
</div>
<div class="form-section">
<h3>Flyers Access Setup</h3>
<div class="form-row">
<label for="flyers-user">Voting Administrator</label>
<input type="text" id="flyers-user" name="flyers-user" />
</div>
<div class="form-row">
<label for="flyers-password">Password</label>
<input type="password" name="flyers-password" />
</div>
<div class="form-row">
<input type="submit" name="login" value="Setup!" />
</div>
</div>
</form>
</div>
</div>
</div>
</body>
</html>

+ 19
- 8
web/inc/db.php 查看文件

@@ -2,13 +2,22 @@
class DBHandler{
private $mysql;

function __construct($hostname, $username, $password, $database){
global $dbs;
private function __construct() {}

$this->mysql = mysqli_connect($hostname, $username, $password);
if(!$this->mysql) die("MySql error: " . mysql_error());
public static function Connect($hostname, $username, $password, $database){
$handler = new DBHandler();

mysqli_select_db($this->mysql, $database);
$handler->mysql = mysqli_connect($hostname, $username, $password);
if(!$handler->mysql) die("MySql error: " . mysql_error());

mysqli_select_db($handler->mysql, $database);
return $handler;
}

public static function Wrap($mysql) {
$handler = new DBHandler();
$handler->mysql = $mysql;
return $handler;
}

public function sanitize($text){
@@ -16,7 +25,11 @@ class DBHandler{
}

public function query($query){
return mysqli_query($this->mysql, $query);
try {
return mysqli_query($this->mysql, $query);
} catch (Throwable $err) {
return false;
}
}

public function fetchRow($query){
@@ -77,5 +90,3 @@ class DBHandler{
return base64_encode($str);
}
}

$db = new DBHandler('localhost', '2022mfelection', '<password>', '2022mfelection');

+ 9
- 0
web/inc/inc.php 查看文件

@@ -3,11 +3,20 @@
define('BASE', dirname(__DIR__));
define('BASEURL', $_SERVER['SERVER_NAME']);

$config = json_decode(file_get_contents(BASE . "/inc/config.json"));
if (empty($config)) {
header('Location: /configure.php');
die();
}

// Start the session
session_start();

// Database and Authentication
require_once(BASE . '/inc/db.php');

$db = DBHandler::Connect($config->host, $config->user, $config->pass, $config->db);

require_once(BASE . '/inc/user.php');

// Templates

+ 10
- 3
web/inc/user.php 查看文件

@@ -55,16 +55,23 @@ class User{
$this->username = $obj->preferred_username;
$this->name = $obj->name;
$this->uid = $obj->sub;
$this->email = $obj->email;
$this->email = $obj->email ?? null;
$this->loggedin = true;

// Create user automatically on login
$_ = $db->query('insert ignore into members (skymanager_id, name, username, email) VALUES (' . ((int) $this->uid) . ', "' . $db->sanitize($this->name) . '", "' . $db->sanitize($this->username) . '", ' . (empty($this->email) ? 'NULL' : '"' . $db->sanitize($this->email) . '"') . ')');

// Get voter ID
$result = $db->fetchRow('select members.voting_id from members left join proxy on (members.voting_id=proxy.voting_id) where proxy.delegate_id is null and skymanager_id=' . ((int) $this->uid));

$admincheck = $db->fetchRow('select members.pollworker from members where skymanager_id=' . ((int) $this->uid));
if ($result)
if ($result) {
$this->voterId = $result['voting_id'];
else
// Auto check in
$_ = $db->query('update members set checkedin=1 where voting_id is not null and skymanager_id=' . ((int) $this->uid));
} else {
$this->voterId = null;
}

if ($admincheck)
$this->role = $admincheck['pollworker'];

+ 11
- 36
web/index.php 查看文件

@@ -11,34 +11,27 @@ if (!$user->voterId()) {
die();
}

$header = new Header("2022 Michigan Flyers Election");
$header = new Header("Michigan Flyers Election");
$header->addStyle("/styles/style.css");
$header->addScript("/js/jquery-1.11.3.min.js");
$header->addScript("/js/search.js");
$header->setAttribute('title', 'Michigan Flyers');
$header->setAttribute('tagline', '2022 Online Ballot');
$header->setAttribute('tagline', 'Online Ballot');
$header->output();

$candidates = $db->fetchAssoc('select skymanager_id, name, username, md5(coalesce(email, "")) as `gravatar_hash` from members where voting_id is not null');
$votes = $db->fetchAssoc("select position from votes where member_id={$user->voterId()}");
$position = $db->fetchRow("select position as code, description as label from positions where active<>0 limit 1");

foreach ($votes as &$vote) {
$vote = $vote['position'];
}
unset($vote);

$vicepresident_voted = in_array("VICEPRESIDENT", $votes);
$secretary_voted = in_array("SECRETARY", $votes);
$director_voted = in_array("DIRECTOR", $votes);

$vicepresident_disabled = $vicepresident_voted;
$secretary_disabled = $secretary_voted || !$vicepresident_voted;
$director_disabled = $director_voted || !$secretary_voted || !$vicepresident_voted;

$vicepresident_disabled_reason = $vicepresident_voted ? "You have already voted for Vice President." : "";
$secretary_disabled_reason = $secretary_disabled ? ($secretary_voted ? "You have already voted for Secretary." : "You must vote for Vice President first.") : "";
$director_disabled_reason = $director_disabled ? ($director_voted ? "You have already voted for Director." : "You must vote for Vice President and Secretary first.") : "";
?>

<?php if (empty($position)): ?>
<h3>There are no active votes. Reload this page once voting starts.</h3>
<?php else: ?>
<script type="text/javascript">
var candidates = <?= json_encode($candidates); ?>;
</script>
@@ -46,28 +39,9 @@ var candidates = <?= json_encode($candidates); ?>;
<div class="form-row">
<div class="selector">
<label class="radio">
<input type="radio" id="vote-vicepresident" name="ballot"
value="VICEPRESIDENT" <?= $vicepresident_disabled ? "disabled" : "checked"; ?> />
<span class="radio-button-label">Vice President</span>
<?php if ($vicepresident_disabled_reason): ?>
<div class="hover-tooltip"><?= $vicepresident_disabled_reason; ?></div>
<?php endif; ?>
</label>
<label class="radio">
<input type="radio" id="vote-director" name="ballot"
value="SECRETARY" <?= $secretary_disabled ? "disabled" : ($vicepresident_disabled ? "checked" : ""); ?> />
<span class="radio-button-label">Secretary</span>
<?php if ($secretary_disabled_reason): ?>
<div class="hover-tooltip"><?= $secretary_disabled_reason; ?></div>
<?php endif; ?>
</label>
<label class="radio">
<input type="radio" id="vote-director" name="ballot"
value="DIRECTOR" <?= $director_disabled ? "disabled" : ($vicepresident_disabled && $secretary_disabled ? "checked" : ""); ?> />
<span class="radio-button-label">Director-At-Large</span>
<?php if ($director_disabled_reason): ?>
<div class="hover-tooltip"><?= $director_disabled_reason; ?></div>
<?php endif; ?>
<input type="radio" id="vote-<?= $position['code']; ?>" name="ballot"
value="<?= $position['code']; ?>" checked />
<span class="radio-button-label"><?= $position['label'] ?></span>
</label>
</div>
</div>
@@ -84,5 +58,6 @@ var candidates = <?= json_encode($candidates); ?>;
</div>
</form>
<?php
endif;
$footer = new Footer();
$footer->output();

+ 2
- 0
web/js/admin-search.js 查看文件

@@ -63,6 +63,8 @@ $(function(){

csc.appendChild(profileimgsect);
csc.appendChild(profiletext);
var smid = document.getElementById('voter-smid');
if (smid) smid.value = candidate.skymanager_id;
document.getElementById('voter-input').value = candidate.voting_id;
document.getElementById('voter-searchbox').value = "";
search('');

+ 1
- 1
web/login.php 查看文件

@@ -25,7 +25,7 @@ if (isset($_GET['denied'])) {
$header = new Header("Login Required");
$header->addStyle("/styles/style.css");
$header->setAttribute('title', 'Michigan Flyers');
$header->setAttribute('tagline', '2022 Online Ballot');
$header->setAttribute('tagline', 'Online Ballot');
$header->output();
?>
<h3 id="login-help">Sign in with your Skymanager Account</h3>

+ 58
- 1
web/styles/style.css 查看文件

@@ -255,13 +255,70 @@ form .form-row {
margin: 0px -20px;
}

form .form-row:nth-child(2n+1) {
form .form-row:nth-child(2n+1 of .form-row) {
background-color: #eee;
}

form .form-section > .form-row {
border-left: 4px solid #6ac;
}

form .form-section {
margin-bottom: 20px;
}

form .form-section h3 {
background-color: #6ac;
color: #fff;
margin: 0px -20px;
padding: 10px;
padding-left: 16px;
border-left: 4px solid #6ac;
}

/*
form .form-section:before, form .form-section:after {
content: '';
display: block;
border: 0px solid #6ac;
width: 10px;
margin-left: -20px;
}

form .form-section:before { border-width: 4px 0 0 0; }
form .form-section:after { border-width: 0 0 4px 0; }
*/

form input {
box-sizing: border-box;
width: 100%;
display:block;
padding: 10px;
}

form .split-button {
display: flex;
flex-direction: row;
align-content: stretch;
gap: 20px;
}

form .split-button > input {
flex-grow: 1;
}

input[type=submit] {
background: linear-gradient(#eee,#e7e7e7);
border: 1px solid rgba(0,0,0,.2);
border-radius: 3px;
}


input[type=submit].danger {
background: linear-gradient(#900, #800);
color: #fff;
font-weight: bold;
}

input[type=submit]:hover { background: linear-gradient(#ddd,#d7d7d7); }
input[type=submit].danger:hover { background: linear-gradient(#800,#700); }

+ 4
- 2
web/templates/header.php 查看文件

@@ -97,8 +97,10 @@ class Header{
if ($user->loggedin()) {
$html .= "\t\t\t\t" . $user->name() . "\n";
$html .= "\t\t\t\t" . " <a href=\"/login.php?logout\">Log Out</a>\n";
if ($user->getRole() === "admin")
$html .= "\t\t\t\t" . " <a href=\"/admin/checkin.php\">Poll Worker</a>\n";
if ($user->getRole() === "admin") {
$html .= "\t\t\t\t" . " <a href=\"/admin/checkin.php\">Volunteer</a>\n";
$html .= "\t\t\t\t" . " <a href=\"/admin/voting.php\">Admin</a>\n";
}
$html .= "\t\t\t\t<a href=\"/index.php\">Home</a>\n";
} else if (basename($_SERVER['PHP_SELF']) != 'login.php') {
$html .= "\t\t\t\t<a href=\"/login.php\">Log In</a>\n";

+ 11
- 27
web/vote.php 查看文件

@@ -11,15 +11,19 @@ if (!$user->voterId()) {
die();
}

$active_position = $db->fetchRow("select position as code, description as label from positions where active<>0 limit 1");
$candidate_selected = (int) $_POST['candidate'];
$ballot = $_POST['ballot'];

if ($candidate_selected != $_POST['candidate']) $error = "An eccor occurred while processing your ballot. Please retry.";
if ($ballot !== "VICEPRESIDENT" && $ballot !== "SECRETARY" && $ballot !== "DIRECTOR") $error = "An eccor occurred while processing your ballot. Please retry.";
if (empty($active_position) || $ballot !== $active_position['code']) $error = "An eccor occurred while processing your ballot. Please retry.";

if (!$error) {
//$result = $db->query("INSERT INTO votes (candidate_id, position, member_id) values ($candidate_selected, \"$ballot\", {$user->voterId()})");
$result = $db->query("INSERT INTO votes (candidate_id, position, member_id, vote_type, submitter_id) SELECT $candidate_selected, \"$ballot\", {$user->voterId()}, 'ONLINE', {$user->voterId()} UNION SELECT $candidate_selected, \"$ballot\", voting_id, 'PROXY ONLINE', delegate_id from proxy where delegate_id={$user->voterId()}");
$result = false;
try {
$result = $db->query("INSERT INTO votes (candidate_id, position, member_id, vote_type, submitter_id) SELECT $candidate_selected, \"$ballot\", {$user->voterId()}, 'ONLINE', {$user->voterId()} UNION SELECT $candidate_selected, \"$ballot\", voting_id, 'PROXY ONLINE', delegate_id from proxy where delegate_id={$user->voterId()}");
} catch (Throwable $ignore) {}
$candidate = $db->fetchRow('select skymanager_id, name, username, md5(coalesce(email, "")) as `gravatar_hash` from members where skymanager_id=' . $candidate_selected);
if ($result) {
$to = 'mf2022elec@gmail.com';
@@ -52,31 +56,13 @@ if (!$error) {
}
}

$votes = $db->fetchAssoc("select position from votes where member_id={$user->voterId()}");

foreach ($votes as &$vote) {
$vote = $vote['position'];
}
unset($vote);

$positions = [
'VICEPRESIDENT' => 'Vice President',
'SECRETARY' => 'Secretary',
'DIRECTOR' => 'Director'
];

$voteFor = null;
if (count($votes) < count($positions)) {
$voteFor = array_values($positions)[count($votes)];
}

$header = new Header("2022 Michigan Flyers Election");
$header = new Header("Michigan Flyers Election");
$header->addStyle("/styles/style.css");
$header->addStyle("/styles/vote.css");
$header->addScript("/js/jquery-1.11.3.min.js");
$header->addScript("/js/search.js");
$header->setAttribute('title', 'Michigan Flyers');
$header->setAttribute('tagline', '2022 Online Ballot');
$header->setAttribute('tagline', 'Online Ballot');
$header->output();
?>
<div id="vote-result">
@@ -86,14 +72,12 @@ $header->output();
"Your ballot has already been submitted.") ?>
</div>
</div>
<?php if ($voteFor): ?>
<a href="/" id="vote-again">Vote for <?= $voteFor; ?></a>
<?php endif; ?>
<a href="/" id="vote-again">Return to voting</a>
<?php if ($result): ?>
<div id="ballot">
<div class="ballot-section">
<h4 class="section-heading">Vice Position</h4>
<h2 class="ballot-position"><?= $positions[$ballot]; ?></h2>
<h4 class="section-heading">Position</h4>
<h2 class="ballot-position"><?= $active_position['label']; ?></h2>
</div>
<div class="ballot-section">
<h4 class="section-heading">Candidate</h4>

Loading…
取消
儲存