浏览代码

Initial Commit

master
Tyler Dence 4 年前
当前提交
a5193ff0c1
签署人:: Tyler Dence <tyzoid.d@gmail.com> GPG 密钥 ID: 3B08EFC6BA974CFC
共有 18 个文件被更改,包括 1418 次插入0 次删除
  1. 52
    0
      web/admin/checkin.php
  2. 147
    0
      web/admin/paper.php
  3. 二进制
      web/images/delete.png
  4. 2
    0
      web/inc/.htaccess
  5. 81
    0
      web/inc/db.php
  6. 15
    0
      web/inc/inc.php
  7. 112
    0
      web/inc/user.php
  8. 77
    0
      web/index.php
  9. 97
    0
      web/js/admin-search.js
  10. 5
    0
      web/js/jquery-1.11.3.min.js
  11. 83
    0
      web/js/search.js
  12. 50
    0
      web/login.php
  13. 7
    0
      web/styles/admin.css
  14. 267
    0
      web/styles/style.css
  15. 120
    0
      web/styles/vote.css
  16. 63
    0
      web/templates/footer.php
  17. 122
    0
      web/templates/header.php
  18. 118
    0
      web/vote.php

+ 52
- 0
web/admin/checkin.php 查看文件

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

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

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

$header = new Header("2021 Michigan Flyers Election : Poll Worker");
$header->addStyle("/styles/style.css");
$header->addStyle("/styles/admin.css");
$header->addScript("/js/jquery-1.11.3.min.js");
$header->addScript("/js/admin-search.js");
$header->setAttribute('title', 'Michigan Flyers');
$header->setAttribute('tagline', '2021 Election Administration');
$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');
?>
<script type="text/javascript">
var voters = <?= json_encode($voters); ?>;
</script>
<form>
<div class="form-row">
<div class="selector">
<label class="radio">
<input type="radio" name="button" value="ci" checked />
<a class="radio-button-label" href="#">Check-In</a>
</label>
<label class="radio">
<input type="radio" name="button" value="pe" />
<a class="radio-button-label" href="/admin/paper.php">Paper Entry</a>
</label>
</div>
</div>
<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" />
<div id="selectedVoter" class="selected candidate voter">
<span class="placeholder">No Selected Voter</span>
</div>
</div>
</form>
<?php
$footer = new Footer();
$footer->output();

+ 147
- 0
web/admin/paper.php 查看文件

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

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

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

if (!empty($_POST['ballot']) && !empty($_POST['candidate'])) {
$candidate_selected = (int) $_POST['candidate'];
$voter_selected = (int) $_POST['voter'];
$ballot = $_POST['ballot'];

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 !== "PRESIDENT" && $ballot !== "DIRECTOR") $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");
$candidate = $db->fetchRow('select skymanager_id, name, username, md5(coalesce(email, "")) as `gravatar_hash` from members where skymanager_id=' . $candidate_selected);

if ($result) {
$proxy_votes = $db->fetchAssoc("SELECT member_id, submitter_id from votes where submitter_id={$user->voterId()} and position=\"$ballot\"");
$num_affected_rows = count($proxy_votes);
if ($num_affected_rows > 1) {
$proxy_str = "";
foreach ($proxy_votes as $proxy_vote) {
if ($proxy_vote['member_id'] === $proxy_vote['submitter_id'])
continue;

$proxy_str .= "#{$proxy_vote['member_id']} ";
}
}
}
}
}

$header = new Header("2021 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/search.js");
$header->addScript("/js/admin-search.js");
$header->setAttribute('title', 'Michigan Flyers');
$header->setAttribute('tagline', '2021 Election Administration');
$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');
?>
<script type="text/javascript">
var voters = <?= json_encode($voters); ?>;
var candidates = <?= json_encode($candidates); ?>;
</script>
<form action="paper.php" method="POST">
<div class="form-row">
<div class="selector">
<label class="radio">
<input type="radio" name="button" value="ci" />
<a class="radio-button-label" href="/admin/checkin.php">Check-In</a>
</label>
<label class="radio">
<input type="radio" name="button" value="pe" checked />
<a class="radio-button-label" href="#">Paper Entry</a>
</label>
</div>
</div>
<div class="form-row">
<div class="selector">
<label class="radio">
<input type="radio" id="vote-president" name="ballot" value="PRESIDENT" checked />
<span class="radio-button-label">President</span>
</label>
<label class="radio">
<input type="radio" id="vote-director" name="ballot" value="DIRECTOR" />
<span class="radio-button-label">Director-At-Large</span>
</label>
</div>
</div>
<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" />
<div id="selectedVoter" class="selected candidate voter">
<span class="placeholder">No Selected Voter</span>
</div>
</div>
<div class="form-row">
<input type="text" placeholder="Candidate Search" id="searchbox" name="searchbox" value="" />
<div id="results"></div>
<input type="hidden" name="candidate" id="candidate-input" value="0" />
<div id="selectedCandidate" class="selected candidate">
<span class="placeholder">No Candidate Selected</span>
</div>
</div>
<div class="form-row">
<input class="submit" type="submit" name="submit" value="Submit Ballot" />
</div>
</form>
<?php if (!empty($_POST['ballot'])): ?>
<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 ($result): ?>
<div id="ballot">
<div class="ballot-section">
<h4 class="section-heading">Position</h4>
<h2 class="ballot-position"><?= ucwords(strtolower($ballot)); ?></h2>
</div>
<div class="ballot-section">
<h4 class="section-heading">Candidate</h4>
<div id="vote-profile" class="candidate">
<div class="profile-icon">
<img src="https://www.gravatar.com/avatar/<?= $candidate['gravatar_hash']; ?>.png?d=mp&s=64" />
</div>
<div class="profile">
<h2 class="profile-name"><?= $candidate['name']; ?></h2>
<h4 class="profile-id"><?= $candidate['skymanager_id']; ?></h4>
</div>
</div>
</div>
<div class="ballot-section">
<h4 class="section-heading">Voter ID</h4>
<h4 id="ballot-voter-id">#<?= $user->voterId(); ?></h4>
</div>
<?php if ($proxy_str): ?>
<div class="ballot-section">
<h4 class="section-heading">Proxy Votes</h4>
<h4 id="ballot-voter-id"><?= $proxy_str; ?></h4>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php
$footer = new Footer();
$footer->output();

二进制
web/images/delete.png 查看文件


+ 2
- 0
web/inc/.htaccess 查看文件

@@ -0,0 +1,2 @@
order allow,deny
deny from all

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

@@ -0,0 +1,81 @@
<?php
class DBHandler{
private $mysql;

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

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

mysqli_select_db($this->mysql, $database);
}

public function sanitize($text){
return mysqli_real_escape_string($this->mysql, $text);
}

public function query($query){
return mysqli_query($this->mysql, $query);
}

public function fetchRow($query){
$result = $this->query($query);
if($result === false || $result === true) die(mysqli_error($this->mysql));//return $result;

return mysqli_fetch_assoc($result);
}

public function fetchAssoc($query){
$result = $this->query($query);
if($result === false || $result === true) die(mysqli_error($this->mysql));//return $result;

$data = array();
while($row = mysqli_fetch_assoc($result)){
$data[] = $row;
}

return $data;
}

public function getError() {
return mysqli_error($this->mysql);
}

public function lastInsertId(){
return mysqli_insert_id($this->mysql);
}

public function getAffectedRows() {
return mysqli_affected_rows($this->mysql);
}

public function verifyInteger($input, $minimum = 1){
/*
* Pretty hacky solution.
*
* First checks if the integer cast of the input is equal to itself.
* This filters out decimals, alternate bases, and exponents.
* Then checks if the input is numeric, which filters out other strings that slip by the first check.
* This guarantees that it's in a numeric format, which combined with the first filter, should guarantee that it is an integer
*/
return (((int) $input == $input) && is_numeric($input) && (($minimum === false) || (int) $input >= $minimum));
}

// Always returns a key of length 40. TODO: Add arbitrary length
public function randomKey(){
//Cryptographically Secure Key
if(function_exists('openssl_random_pseudo_bytes')) return base64_encode(openssl_random_pseudo_bytes(30));


//Fallback (Not cryptographically secure)
$str = "";
for($i=0; $i<30; $i++){
$str .= chr(mt_rand(0,255));
}

return base64_encode($str);
}
}

$db = new DBHandler('localhost', '2021mfelection', 'SHa9SGdAlNmafLvJSWjZ', '2021mfelection');

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

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

// Start the session
session_start();

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

// Templates
require_once(BASE . '/templates/header.php');
require_once(BASE . '/templates/footer.php');

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

@@ -0,0 +1,112 @@
<?php
require_once('db.php');

class User{
private $username = "";
private $email = "";
private $name = "";
private $uid = -1;
private $voterId = -1;
private $loggedin = false;
private $role = 0;

function __construct(){
if(isset($_SESSION['token']) && strlen($_SESSION['token']) > 41) {
$this->parseToken($_SESSION['token']);
}
}

public function login($username, $password){
$data = http_build_query([
'username' => $username,
'password' => $password,
'grant_type' => 'password'
]);

$opt = [
'http' => [
'method' => 'POST',
'header' => "Content-type: application/x-www-form-urlencoded\r\n"
. "Content-Length: " . strlen($data) . "\r\n",
'content' => $data
]
];

$ctx = stream_context_create($opt);
$token = file_get_contents('https://beta.schedule.michiganflyers.org/api/oauth/token', false, $ctx);

if (!empty($token)) {
$_SESSION['token'] = json_decode($token)->access_token;
return $this->parseToken($_SESSION['token']);
}

return false;
}

private function parseToken($token) {
global $db;

$data = explode('.', $token);
if (count($data) != 3)
return false;

$obj = json_decode(base64_decode($data[1]));

$this->username = $obj->preferred_username;
$this->name = $obj->name;
$this->uid = $obj->sub;
$this->email = $obj->email;
$this->loggedin = true;

// 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));
if ($result)
$this->voterId = $result['voting_id'];
else
$this->voterId = null;

return true;
}

public function username(){
return $this->username;
}

public function name(){
return $this->name;
}

public function voterId(){
return $this->voterId;
}

public function email(){
return $this->email;
}

public function gravatarUrl($size = 128){
return 'https://www.gravatar.com/avatar/' . md5($this->email) . ".png?r=pg&s=$size";
}

public function loggedin(){
return $this->loggedin;
}

public function getRole(){
return $this->username === 'tyzoid' ? 'admin' : 'voter';
//return $this->role;
}

public function logout(){
$_SESSION['token'] = "";
$this->username = "";
$this->uid = -1;
$this->loggedin = false;
}

public function getUserId(){
return $this->uid;
}
}

$user = new User();

+ 77
- 0
web/index.php 查看文件

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

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

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

$header = new Header("2021 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', '2021 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()}");

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

$president_voted = in_array("PRESIDENT", $votes);
$director_voted = in_array("DIRECTOR", $votes);

$president_disabled = $president_voted;
$director_disabled = $director_voted || !$president_voted;

$president_disabled_reason = $president_voted ? "You have already voted for President." : "";
$director_disabled_reason = $director_disabled ? ($director_voted ? "You have already voted for Director." : "You must vote for President first.") : "";
?>
<script type="text/javascript">
var candidates = <?= json_encode($candidates); ?>;
</script>
<form action="vote.php" method="POST">
<div class="form-row">
<div class="selector">
<label class="radio">
<input type="radio" id="vote-president" name="ballot"
value="PRESIDENT" <?= $president_disabled ? "disabled" : "checked"; ?> />
<span class="radio-button-label">President</span>
<?php if ($president_disabled_reason): ?>
<div class="hover-tooltip"><?= $president_disabled_reason; ?></div>
<?php endif; ?>
</label>
<label class="radio">
<input type="radio" id="vote-director" name="ballot"
value="DIRECTOR" <?= $director_disabled ? "disabled" : ($president_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; ?>
</label>
</div>
</div>
<div class="form-row">
<input type="text" placeholder="Candidate Search" id="searchbox" name="searchbox" value="" />
<div id="results"></div>
<input type="hidden" name="candidate" id="candidate-input" value="0" />
<div id="selectedCandidate" class="selected candidate">
<span class="placeholder">No Candidate Selected</span>
</div>
</div>
<div class="form-row">
<input class="submit" type="submit" name="submit" value="Submit Ballot" />
</div>
</form>
<?php
$footer = new Footer();
$footer->output();

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

@@ -0,0 +1,97 @@
$(function(){
function isMatch(candidate, text) {
if (candidate.name.toLowerCase().includes(text))
return true;

if (candidate.username.toLowerCase().includes(text))
return true;

if (("" + candidate.skymanager_id).includes(text))
return true;

if (candidate.voting_id && ("" + candidate.voting_id).includes(text))
return true;

return false;
}

function createChild(candidate) {
var li = document.createElement('li');
li.className = 'candidate';

var profileimgsect = document.createElement('div');
profileimgsect.className = 'profile-icon';

var profileimg = document.createElement('img');
profileimg.src = 'https://www.gravatar.com/avatar/' + candidate.gravatar_hash + '.png?d=mp&s=64';
profileimgsect.appendChild(profileimg);

var profiletext = document.createElement('div');
profiletext.className = 'profile';

var profilename = document.createElement('h2');
profilename.className = 'profile-name';
profilename.textContent = candidate.name;

var profileid = document.createElement('h4');
profileid.className = 'profile-id';
profileid.textContent = candidate.voting_id;
if (!candidate.voting_id || candidate.delegate) {
profiletext.className += ' ineligible';
profileid.textContent += " (Ineligible)";
}

profiletext.appendChild(profilename);
profiletext.appendChild(profileid);

if (candidate.proxies) {
var proxies = document.createElement('div');
proxies.className = 'proxies';
proxies.textContent = 'Proxy Votes: ' + candidate.proxies.replace(',', ', ');
profiletext.appendChild(proxies);
}


li.appendChild(profileimgsect);
li.appendChild(profiletext);

li.addEventListener('click', function() {
var csc = document.getElementById("selectedVoter");
while (csc.firstChild) {
csc.removeChild(csc.firstChild);
}

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


return li;
}

function search(text) {
var list = document.createElement('ul');

var matches = 0;
for (var i = 0; text.length > 0 && i < voters.length; i++) {
if (isMatch(voters[i], text.toLowerCase())) {
list.appendChild(createChild(voters[i]));
if (++matches > 4)
break;
}
}

var container = document.getElementById('voter-results');
while (container.firstChild) {
container.removeChild(container.firstChild);
}

if (matches)
container.appendChild(list);
}

$('#voter-searchbox').bind('textInput input', function() { search(this.value); });
});

+ 5
- 0
web/js/jquery-1.11.3.min.js
文件差异内容过多而无法显示
查看文件


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

@@ -0,0 +1,83 @@
$(function(){
function isMatch(candidate, text) {
if (candidate.name.toLowerCase().includes(text))
return true;

if (candidate.username.toLowerCase().includes(text))
return true;

if (("" + candidate.skymanager_id).includes(text))
return true;

return false;
}

function createChild(candidate) {
var li = document.createElement('li');
li.className = 'candidate';

var profileimgsect = document.createElement('div');
profileimgsect.className = 'profile-icon';

var profileimg = document.createElement('img');
profileimg.src = 'https://www.gravatar.com/avatar/' + candidate.gravatar_hash + '.png?d=mp&s=64';
profileimgsect.appendChild(profileimg);

var profiletext = document.createElement('div');
profiletext.className = 'profile';

var profilename = document.createElement('h2');
profilename.className = 'profile-name';
profilename.textContent = candidate.name;

var profileid = document.createElement('h4');
profileid.className = 'profile-id';
profileid.textContent = candidate.skymanager_id;

profiletext.appendChild(profilename);
profiletext.appendChild(profileid);

li.appendChild(profileimgsect);
li.appendChild(profiletext);

li.addEventListener('click', function() {
var csc = document.getElementById("selectedCandidate");
while (csc.firstChild) {
csc.removeChild(csc.firstChild);
}

csc.appendChild(profileimgsect);
csc.appendChild(profiletext);
document.getElementById('candidate-input').value = candidate.skymanager_id;
document.getElementById('searchbox').value = "";
search('');
});


return li;
}

function search(text) {
var list = document.createElement('ul');

var matches = 0;
for (var i = 0; text.length > 0 && i < candidates.length; i++) {
if (isMatch(candidates[i], text.toLowerCase())) {
list.appendChild(createChild(candidates[i]));
if (++matches > 4)
break;
}
}

var container = document.getElementById('results');
while (container.firstChild) {
container.removeChild(container.firstChild);
}

if (matches)
container.appendChild(list);
}

//$('#searchbox').bind('change keypress keydown keyup', function() { search(this.value); });
$('#searchbox').bind('textInput input', function() { search(this.value); });
});

+ 50
- 0
web/login.php 查看文件

@@ -0,0 +1,50 @@
<?php
require_once("inc/inc.php");

$alsovalid = array('-', '_');

if (isset($_GET['denied'])) {
$error = "Your account is not eligible to submit a ballot.";
} else if (isset($_GET['logout'])) {
$user->logout();
} else if (isset($_POST['login'])){
if(isset($_POST['username']) && isset($_POST['password']) && !empty($_POST['username']) && !empty($_POST['password'])){
$username = $_POST['username'];
$password = $_POST['password'];
if($user->login($username, $password)){
header("Location: /");
die();
} else {
$error = "Incorrect username or password.";
}
} else {
$error = "Must fill in both username and password.";
}
}

$header = new Header("Login Required");
$header->addStyle("/styles/style.css");
$header->setAttribute('title', 'Michigan Flyers');
$header->setAttribute('tagline', '2021 Online Ballot');
$header->output();
?>
<h3 id="login-help">Sign in with your Skymanager Account</h3>
<?php
if(isset($error)) echo "<span class=\"errormessage\">$error</span>";
?>
<form action="login.php" method="POST">
<div class="form-row">
<label for="username">Username</label>
<input class="login" type="text" name="username" />
</div>
<div class="form-row">
<label for="password">Password</label>
<input class="login" type="password" name="password" />
</div>
<div class="form-row">
<input class="loginbutton" type="submit" name="login" value="Log In" />
</div>
</form>
<?php
$footer = new Footer();
$footer->output();

+ 7
- 0
web/styles/admin.css 查看文件

@@ -0,0 +1,7 @@
div#selectedCandidate::before {
content: 'Selected Voter';
}

.profile.ineligible {
color: #c00;
}

+ 267
- 0
web/styles/style.css 查看文件

@@ -0,0 +1,267 @@
body, html{
width:100%;
height:100%;
padding:0px;
margin:0px;

font-family: 'Fira Sans', sans-serif;
font-size:20px;
color: #000;
}

h1, h2, h3, h4, h5, h6{
font-weight: 800;
margin:0px;
padding:0px;
}

h3, h4, h5, h6 {
font-weight: 600;
}

span.errormessage {
color: #c00;
padding: 10px 0px;
font-size: 0.8rem;
}

#login-help {
margin: 0px;
padding: 10px 0px;
}

div#container {
position:relative;
min-height:100%;
}

div.loginbar {
height:36px;
background-color:#11111a;
padding:0px 10px;
line-height:36px;
font-size: 0.75rem;
border-bottom:1px solid #000;
color:#fff;
}

div.loginbar a {
display:block;
padding:6px 10px;
height:24px;
line-height:24px;
float:right;
color:#aaa;
text-decoration:none;
}

div.loginbar a:hover {
background-color:#444;
color:#fff;
text-decoration:underline;
}

div.header {
background-color:#333;
padding:20px;
color:#fff;
border-top:1px solid #444;
border-bottom:4px solid #6ac;
}

div.content {
}

div.page {
padding:0px 20px 20px;
}

div.message {
padding-bottom:5px;
}

div.footercontainer {
position:absolute;
bottom:0px;
width:100%;
height:100px;
}

div.footercontainer div.footer {
border-top:4px solid #6ac;
background-color:#333;
padding:20px;
color:#fff;
height:56px;
}

div#results {
position: absolute;
z-index: 1;
left: 0;
right: 0;
background-color: #fff;
}

div.selected.candidate::before {
position: absolute;
left: 10px;
top: -11px;
height: 20px;
padding: 0px 5px;
line-height: 20px;
background-color: #fff;
content: 'Selected Candidate';
font-size: 0.6rem;
color: rgba(0,0,0,0.6);
display: block;
text-align: center;
}

div.selected.candidate.voter::before {
content: 'Selected Voter';
}

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

div.selected.candidate {
border: 1px solid rgba(0, 0, 0, .2);
position: relative;
margin-top: 20px;
padding: 10px 10px 0px 10px;
border-radius: 10px;
}

div.selected.candidate span.placeholder {
font-size: 0.6rem;
color: rgba(0, 0, 0, .8);
}

.candidate {
border: 0px solid rgba(0,0,0,.2);
border-top-width: 1px;
border-bottom-width: 1px;
display: flex;
}

#results ul {
margin: 0;
padding: 0;
}

li.candidate {
list-style-type: none;
padding: 20px;
cursor: pointer;
}

.candidate > div.profile-icon {
padding: 2px;
margin-right: 20px;
}

.candidate > div.profile-icon img {
border-radius: 100%;
border: 2px solid #fff;
box-shadow: 0px 0px 0px 2px #000;
}

.candidate > div.profile {
flex-grow: 1;
}

.candidate .profile-id::before {
content: 'ID: ';
}

form, form input {
font-size: 1rem;
}

form label {
display: block;
}

form div.selector {
display: flex;
}

form div.selector label {
margin: 0px 10px;
flex-grow: 1;
}

form label.radio input {
display: none;
}

form label.radio .radio-button-label {
text-decoration: none;
color: #000;
text-align: center;
display: block;
padding: 10px;
border: 2px solid rgba(0,0,0,.2);
border-radius: 10px;
}

form label.radio input:checked+.radio-button-label {
background-color: #000;
color: #fff;
border: 2px solid #fff;
box-shadow: 0px 0px 0px 2px #000;
}

form label.radio {
position: relative;
}

form label.radio div.hover-tooltip {
position: absolute;
pointer-events: none;
opacity: 0;
z-index: 2;
transition: opacity 300ms ease-in-out;
margin-top: 5px;
padding: 10px;
border-radius: 10px;
background-color: #fff;
border: 2px solid rgba(0, 0, 0, .2);
}

form label.radio div.hover-tooltip::before {
position:absolute;
content: ' ';
background-color: #fff;
top: -10px;
left: 20px;
width: 16px;
height: 16px;
transform: rotate(45deg);
border: 2px solid rgba(0, 0, 0, .2);
border-bottom-color: transparent;
border-right-color: transparent;
}

form label.radio:hover div.hover-tooltip {
position: absolute;
opacity: 1;
}

form .form-row {
padding: 10px 20px;
margin: 0px -20px;
}

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

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

+ 120
- 0
web/styles/vote.css 查看文件

@@ -0,0 +1,120 @@
div#vote-result {
padding: 20px 0px;
display: flex;
align-items: center;
justify-content: center;
}

div#vote-result div#status{
flex-shrink: 0;
flex-grow: 0;
flex-basis: 60px;
width: 60px;
height: 60px;
border-radius: 100%;
line-height: 60px;
text-align: center;
border: 2px solid transparent;
font-size: 1.4rem;
}

div#vote-result div#status.success::before { content: '\2714'; }
div#vote-result div#status.failure::before { content: '\2718'; }

div#vote-result div#status.success {
border-color: #0a0;
color: #0a0;
}

div#vote-result div#status.failure {
background-color: #a00;
color: #fff;
}

div#vote-result div#message {
margin-left: 20px;
}

div#ballot {
border: 2px solid rgba(0, 0, 0, .2);
padding: 0px 10px;
border-radius: 10px;
overflow: hidden;
}

div#ballot div.ballot-section {
border-bottom: 2px solid rgba(0, 0, 0, .2);
margin: 0px -10px;
padding: 0px 10px;
}

div#ballot div.ballot-section:last-child {
border-bottom-width: 0px;
}

div#ballot h4#ballot-voter-id {
padding: 5px 0px;
}

div.ballot-section .candidate {
border: 0;
}

div.ballot-section h4.section-heading {
display: inline-block;
height: 30px;
line-height: 30px;
width: auto;
background-color: #000;
color: #fff;
padding: 0px 30px 0px 10px;
margin: 0px -10px;
position:relative;
}

div.ballot-section h4.section-heading::after {
position: absolute;
right: -29px;
content: ' ';
height: 0px;
width: 0px;
border: 15px solid transparent;
border-left: 15px solid #000;
border-top: 15px solid #000;
}


div.ballot-section h2.ballot-position {
margin: 0px -10px;
padding: 5px 10px;
}

div#ballot div#vote-profile {
padding: 12px 10px 10px;
margin: 0px -10px;
max-width: 600px;
align-items: center;
justify-content: center;
margin-bottom: 10px;
}

a#vote-again {
text-decoration: none;
font-size: 1rem;
text-align: center;
margin: 0px auto 10px;
display: inline-block;
position: relative;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
border: 2px solid #6ac;
background-color: rgba(102, 170, 204, .2);
border-radius: 10px;
color: #000;
transition: background-color 125ms ease-in-out;
}

a#vote-again:hover {
background-color: rgba(102, 170, 204, .6);
}

+ 63
- 0
web/templates/footer.php 查看文件

@@ -0,0 +1,63 @@
<?php
/**
* Footer Template
*
* Uses HTML5, but all template HTML is (should be) xHTML 1.1 compatable
*/
class Footer{
private $scripts = array();

/**
* Constructor
*
* @param string $title - the title of the page
* @param mixed $scripts - the url (or array of urls) of the script(s) to include
*/
public function __construct($title = null, $stylesheets = null){
// Page Title
if (! empty($title)) $this->title = $title;

if (! empty($scripts) && is_array($scripts)) $this->scripts = array_merge($scripts, $this->scripts);
else if (! empty($scripts)) $this->addScript($scripts);
}

public function setTitle($title = "Ranks"){
$this->title = $title;
}

public function addScript($script){
if (empty($script) || strncmp($script, '/', 1) !== 0) return false;

$scripts[] = $script;
return true;
}

public function output($return = false){
// End Content/page div
$html = "\t\t\t\t</div>\n";
$html = "\t\t\t</div>\n";

// Footer content
/*
$html .= "\t\t\t<div class=\"footercontainer\">\n";
$html .= "\t\t\t\t<div class=\"footer\">\n";
$html .= "\t\t\t\t\t<h2>Copyright &copy; 2015. All Rights Reserved</h2>\n";
$html .= "\t\t\t\t</div>\n";
$html .= "\t\t\t</div>\n";
*/

// End Container
$html .= "\t\t</div>\n";

// Add any scripts to be included at the bottom of the page.
foreach ($this->scripts as $script){
echo "<script type=\"text/javascript\" src=\"$script\"></script>";
}

$html .= "\t</body>\n";
$html .= "</html>\n";

if($return === true) return $html;
echo $html;
}
}

+ 122
- 0
web/templates/header.php 查看文件

@@ -0,0 +1,122 @@
<?php
/**
* Header Template
*
* Uses HTML5, but all template HTML is (should be) xHTML 1.1 compatable
*/
class Header{
private $title = "Template";
private $scripts = array();
private $stylesheets = array(
"https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;600;800&display=swap"
);

/*
* The Attributes array. Holds data for the page.
*/
private $attributes = array(
"title" => "Template",
"tagline" => "A Tyzoid Production",
"showbar" => true,
"loginbar" => ""
);

/**
* Constructor
*
* @param string $title - the title of the page
* @param mixed $scripts - the url (or array of urls) of the script(s) to include
* @param mixed $stylesheets - the url (or array of urls) of the stylesheet(s) to include
*/
public function __construct($title = null, $scripts = null, $stylesheets = null){
// Page Title
if (! empty($title)) $this->title = $title;

if (! empty($scripts) && is_array($scripts)) $this->scripts = array_merge($scripts, $this->scripts);
else if (! empty($scripts)) $this->addScript($scripts);

if (! empty($stylesheets) && is_array($stylesheets)) $this->stylesheets = array_merge($stylesheets, $this->stylesheets);
else if (! empty($stylesheets)) $this->addStyle($stylesheets);
}

public function setTitle($title){
$this->title = $title;
}

public function addScript($script){
if (empty($script) || strncmp($script, '/', 1) !== 0) return false;

$this->scripts[] = $script;
return true;
}

public function addStyle($stylesheet){
if (empty($stylesheet) || strncmp($stylesheet, '/', 1) !== 0) return false;

$this->stylesheets[] = $stylesheet;
return true;
}

public function setAttribute($attribute, $value){
$this->attributes[$attribute] = $value;
}

public function output($return = false){
global $user;

// Doctype
$html = "<!doctype html>\n";
$html .= "<html>\n";
$html .= "\t<head>\n";

// Page Attributes/Head information
$html .= "\t\t<title>" . $this->title . "</title>";

// Set mobile-friendly
$html .= '<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />';

// Stylesheet import
foreach ($this->stylesheets as $style){
$html .= "\t\t<link rel=\"stylesheet\" type=\"text/css\" href=\"$style\" />\n";
}

// Script import
foreach ($this->scripts as $script){
$html .= "\t\t<script type=\"text/javascript\" src=\"$script\"></script>\n";
}

// Start Body of the page
$html .= "\t</head>\n";
$html .= "\t<body>\n";

// Page layout
$html .= "\t\t<div id=\"container\">\n";

// Login/Statusbar
$html .= "\t\t\t<div class=\"loginbar\">\n";
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";
$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";
} else {
$html .= "\t\t\t\t<a href=\"/index.php\">Home</a>\n";
}
$html .= "\t\t\t</div>\n";

$html .= "\t\t\t<div class=\"header\">\n";
$html .= "\t\t\t\t<h1>{$this->attributes['title']}</h1>\n";
$html .= "\t\t\t\t<h2>{$this->attributes['tagline']}</h2>\n";
$html .= "\t\t\t</div>\n";

$html .= "\t\t\t<div class=\"content\">\n";
$html .= "\t\t\t\t<div class=\"page\">\n";
//End divs should be closed in the footer template

if($return === true) return $html;
echo $html;
}
}

+ 118
- 0
web/vote.php 查看文件

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

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

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

$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 !== "PRESIDENT" && $ballot !== "DIRECTOR") $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()}");
$candidate = $db->fetchRow('select skymanager_id, name, username, md5(coalesce(email, "")) as `gravatar_hash` from members where skymanager_id=' . $candidate_selected);
if ($result) {
$to = 'mf2021elec@gmail.com';
$from = 'noreply@tyzoid.com';
$subject = "[TEST] Ballot Submitted ({$user->voterId()} -> {$candidate['skymanager_id']})";
$headers =
"From: {$from}\r\n" .
"Message-ID: 2021election-voter-{$user->voterId()}-{$ballot}-" . mt_rand() . "@tyzoid.com\r\n";

$body = "Position: " . ucwords(strtolower($ballot)) . "\r\n" .
"Candidate: {$candidate['name']} (ID #{$candidate['skymanager_id']})\r\n" .
"Voter #{$user->voterId()}\r\n";

$proxy_votes = $db->fetchAssoc("SELECT member_id, submitter_id from votes where submitter_id={$user->voterId()} and position=\"$ballot\"");
$num_affected_rows = count($proxy_votes);
if ($num_affected_rows > 1) {
$proxy_str = "";
foreach ($proxy_votes as $proxy_vote) {
if ($proxy_vote['member_id'] === $proxy_vote['submitter_id'])
continue;

$proxy_str .= "#{$proxy_vote['member_id']} ";
}

$body .= "Proxies: $proxy_str\r\n";
}

if (!mail($to, $subject, $body, $headers, "-f$from"))
$error = "Ballot audit record failed to create";
}
}

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

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

$voteFor = null;
if (count($votes) == 1) {
$voteFor = $votes[0] == "PRESIDENT" ? "Director" : "President";
}

$header = new Header("2021 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', '2021 Online Ballot');
$header->output();
?>
<div id="vote-result">
<div id="status" class="<?= $result ? "success" : "failure"; ?>"></div>
<div id="message" class="<?= $result ? "success" : "failure"; ?>">
<?= $error ? $error : ($result ? "Your Ballot has been successfully Submitted" :
"Your ballot has already been submitted.") ?>
</div>
</div>
<?php if ($voteFor): ?>
<a href="/" id="vote-again">Vote for <?= $voteFor; ?></a>
<?php endif; ?>
<?php if ($result): ?>
<div id="ballot">
<div class="ballot-section">
<h4 class="section-heading">Position</h4>
<h2 class="ballot-position"><?= ucwords(strtolower($ballot)); ?></h2>
</div>
<div class="ballot-section">
<h4 class="section-heading">Candidate</h4>
<div id="vote-profile" class="candidate">
<div class="profile-icon">
<img src="https://www.gravatar.com/avatar/<?= $candidate['gravatar_hash']; ?>.png?d=mp&s=64" />
</div>
<div class="profile">
<h2 class="profile-name"><?= $candidate['name']; ?></h2>
<h4 class="profile-id"><?= $candidate['skymanager_id']; ?></h4>
</div>
</div>
</div>
<div class="ballot-section">
<h4 class="section-heading">Voter ID</h4>
<h4 id="ballot-voter-id">#<?= $user->voterId(); ?></h4>
</div>
<?php if ($proxy_str): ?>
<div class="ballot-section">
<h4 class="section-heading">Proxy Votes</h4>
<h4 id="ballot-voter-id"><?= $proxy_str; ?></h4>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php
$footer = new Footer();
$footer->output();

正在加载...
取消
保存