| @@ -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 $ - .*$%"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 | |||
| } | |||
| @@ -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/' | |||
| @@ -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 | |||
| @@ -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"; ?>"> | |||
| @@ -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(); | |||
| @@ -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'); | |||
| @@ -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(); | |||
| @@ -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> | |||
| @@ -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'); | |||
| @@ -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 | |||
| @@ -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,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(); | |||
| @@ -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(''); | |||
| @@ -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> | |||
| @@ -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); } | |||
| @@ -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,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> | |||