summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.htaccess33
-rwxr-xr-xclosevotes.php8
-rw-r--r--closevotes.php-script2
-rw-r--r--database.php160
-rw-r--r--database.sql4
-rw-r--r--denied.php12
-rw-r--r--index.php5
-rw-r--r--motion.php136
-rw-r--r--motions.php174
-rw-r--r--proxy.php154
-rwxr-xr-xremind.php43
-rw-r--r--styles.css31
-rw-r--r--vote.php104
13 files changed, 866 insertions, 0 deletions
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..403e132
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,33 @@
+<IfModule mod_php5.c>
+php_flag display_errors Off
+php_flag log_errors On
+php_value error_log syslog
+
+php_flag safe_mode On
+php_flag safe_mode_gid On
+php_value open_basedir /var/www/board
+php_value safe_mode_exec_dir /var/empty
+</IfModule>
+
+<FilesMatch "^(database.*|remind.php|closevotes.php.*)$">
+ Order Deny,Allow
+ Deny from all
+</FilesMatch>
+
+
+
+
+<FilesMatch "^(motions?|vote|proxy)\.php$">
+ # these files require authentication
+ <IfModule mod_ssl.c>
+ SSLOptions +OptRenegotiate +StdEnvVars +ExportCertData
+ SSLUserName SSL_CLIENT_S_DN_Email
+ SSLVerifyClient require
+# <IfModule mod_rewrite.c>
+# RewriteEngine on
+# RewriteCond %{SSL:SSL_CLIENT_VERIFY} !=SUCCESS
+# RewriteRule .? - [F]
+# ErrorDocument 403 "You need a client side certificate issued by CAcert to access this url"
+# </IfModule>
+ </IfModule>
+</FilesMatch>
diff --git a/closevotes.php b/closevotes.php
new file mode 100755
index 0000000..ca95905
--- /dev/null
+++ b/closevotes.php
@@ -0,0 +1,8 @@
+#!/usr/bin/php
+<?
+require_once("database.php");
+$db = new DB();
+
+$db->closeVotes();
+
+?>
diff --git a/closevotes.php-script b/closevotes.php-script
new file mode 100644
index 0000000..5246205
--- /dev/null
+++ b/closevotes.php-script
@@ -0,0 +1,2 @@
+# echo "select strftime('%H:%M %m%d%Y',due) from decisions where status=0;" | sqlite3 database.sqlite | xargs -n1 -I^ sudo -u www-data at -f closevotes.php-script ^ +1minute
+/var/www/board/closevotes.php
diff --git a/database.php b/database.php
new file mode 100644
index 0000000..e226c5a
--- /dev/null
+++ b/database.php
@@ -0,0 +1,160 @@
+<?php
+ class DB {
+ var $board = "cacert-board@lists.cacert.org";
+ var $notices = "testsympa@lists.cacert.org";
+
+ function __construct() {
+ $this->dbh = new PDO("sqlite:".dirname(__FILE__)."/database.sqlite");
+ $this->statement = array();
+ $this->statement['list decisions'] = $this->dbh->prepare("SELECT decisions.id AS id, decisions.tag AS tag, voters.name AS proposer, decisions.proponent, decisions.proposed, decisions.title, decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=1) AS ayes, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=-1) AS nayes, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=0) AS abstains FROM decisions, voters WHERE decisions.proponent=voters.id ORDER BY proposed DESC LIMIT 10 OFFSET 10 * (:page - 1);");
+ $this->statement['list my unvoted decisions'] = $this->dbh->prepare("SELECT * FROM (SELECT decisions.id AS id, decisions.tag AS tag, voters.name AS proposer, decisions.proponent AS proponent, decisions.proposed AS proposed, decisions.title AS title, decisions.content AS content, decisions.votetype AS votetype, decisions.status AS status, decisions.due AS due, decisions.modified AS modified,(SELECT COUNT(*) AS ayes FROM votes WHERE decision=decisions.id AND vote=1), (SELECT COUNT(*) AS nayes FROM votes WHERE decision=decisions.id AND vote=-1), (SELECT COUNT(*) AS abstains FROM votes WHERE decision=decisions.id AND vote=0) FROM decisions, voters WHERE decisions.proponent=voters.id AND decisions.status=0) WHERE NOT EXISTS (SELECT vote FROM votes WHERE votes.decision=id AND votes.voter=:id) ORDER BY proposed DESC LIMIT 10 OFFSET 10 * (:page - 1);");
+ $this->statement['list decision'] = $this->dbh->prepare("SELECT decisions.id AS id, decisions.tag AS tag, voters.name AS proposer, decisions.proponent, decisions.proposed, decisions.title, decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=1) AS ayes, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=-1) AS nayes, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=0) AS abstains FROM decisions, voters WHERE decisions.proponent=voters.id AND decisions.tag=:id ORDER BY proposed DESC;");
+ $this->statement['closed decisions'] = $this->dbh->prepare("SELECT decisions.id, decisions.tag, voters.name AS proposer, decisions.proponent, decisions.proposed, decisions.title, decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=1) AS ayes, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=-1) AS nayes, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=0) AS abstains FROM decisions, voters WHERE decisions.proponent=voters.id AND decisions.status=0 AND datetime('now','utc') > datetime(due);");
+ $this->statement['get decision'] = $this->dbh->prepare("SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title, decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=1) AS ayes, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=-1) AS nayes, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=0) AS abstains FROM decisions, voters WHERE decisions.proponent=voters.id AND decisions.id=:decision;");
+ $this->statement['get new decision'] = $this->dbh->prepare("SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title, decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=1) AS ayes, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=-1) AS nayes, (SELECT COUNT(*) FROM votes WHERE decision=decisions.id AND vote=0) AS abstains FROM decisions, voters WHERE decisions.proponent=voters.id AND decisions.id=last_insert_rowid();");
+ $this->statement['get voter'] = $this->dbh->prepare("SELECT voters.id, voters.name FROM voters, emails WHERE voters.id=emails.voter AND emails.address=? AND voters.enabled=1");
+ $this->statement['get voter by id'] = $this->dbh->prepare("SELECT voters.id, voters.name FROM voters WHERE id=:id;");
+ $this->statement['get voters'] = $this->dbh->prepare("SELECT voters.id, voters.name FROM voters WHERE voters.enabled=1 ORDER BY name ASC;");
+ $this->statement['get reminder voters'] = $this->dbh->prepare("SELECT voters.id, voters.name, voters.reminder AS email FROM voters WHERE voters.enabled=1 AND voters.reminder!='' ORDER BY name ASC;");
+ $this->statement['del vote'] = $this->dbh->prepare("DELETE FROM votes WHERE decision=:decision AND voter=:voter;");
+ $this->statement['do vote'] = $this->dbh->prepare("INSERT INTO votes (decision, voter, vote, voted, notes) VALUES (:decision, :voter, :vote, datetime('now','utc'), :notes);");
+ $this->statement['stats'] = $this->dbh->prepare("SELECT COUNT(*) AS voters FROM voters WHERE enabled=1;");
+ $this->statement['list votes'] = $this->dbh->prepare("SELECT voters.name AS name, votes.vote AS vote FROM voters,votes WHERE voters.id=votes.voter AND votes.decision=:id;");
+ $this->statement['create decision'] = $this->dbh->prepare("INSERT INTO decisions (proposed, proponent, title, content, votetype, status, due, modified,tag) VALUES (datetime('now','utc'), :proponent, :title, :content, :votetype, 0, datetime(date('now','utc'),'utc', :due,'+1 day','-1 second'), datetime('now','utc'),'m' || strftime('%Y%m%d','now') || '.' || (select count(*)+1 as num from decisions where proposed between date('now') and date('now','1 day')));");
+ $this->statement['update decision'] = $this->dbh->prepare("UPDATE decisions SET proposed=datetime('now','utc'), proponent=:proponent, title=:title, content=:content, votetype=:votetype, status=0, due=datetime(date('now','utc'),'utc', :due,'+1 day','-1 second'), modified=datetime('now','utc') WHERE id=:id;");
+ $this->statement['close decision'] = $this->dbh->prepare("UPDATE decisions SET status=:status, modified=datetime('now','utc') WHERE id=:decision");
+ ini_set('mbstring.internal_encoding', 'UTF-8');
+ }
+ function getStatement($name) {
+ return $this->statement[$name];
+ }
+ function closeVotes() {
+ $stmt = $this->getStatement("closed decisions");
+ $upd = $this->getStatement("close decision");
+ if ($stmt->execute()) {
+ while ($decision = $stmt->fetch()) {
+ switch ($decision['votetype']) {
+ case 0: // motion
+ $quorum = 3; $majority = 50; break;
+ case 1: // veto
+ default:
+ $quorum = 1; $majority = 99; break;
+ }
+ $votes = $decision['ayes'] + $decision['nayes'] + $decision['abstains'];
+ if ($votes < $quorum) {
+ $decision['status'] = -1;
+ } else {
+ $votes = $decision['ayes'] + $decision['nayes'];
+ if (($decision['ayes'] / $votes) > ($majority / 100)) {
+ $decision['status'] = 1;
+ } else {
+ $decision['status'] = -1;
+ }
+ }
+ $upd->bindParam(":decision",$decision['id']);
+ $upd->bindParam(":status",$decision['status']);
+ $upd->execute();
+ $state = $decision['status']==1?"accepted":"declined";
+ $tag = $decision['tag'];
+ $title = $decision['title'];
+ $content = $decision['content'];
+ $votetype = !$decision['votetype']?'motion':'veto';
+ $ayes = $decision['ayes'];
+ $nayes = $decision['nayes'];
+ $abstains = $decision['abstains'];
+ $totalvotes = $decision['ayes']+$decision['nayes'];
+ if ($totalvotes <= 0) $percent = 0;
+ else $percent = $decision['ayes'] * 100 / $totalvotes;
+ $body = <<<BODY
+Dear Board,
+
+The motion with the identifier $tag has been $state.
+
+Motion:
+ $title
+ $content
+
+Vote type: $votetype
+
+ Ayes: $ayes
+ Nayes: $nayes
+ Abstentions: $abstains
+
+ Percentage: $percent%
+
+Kind regards,
+the voting system.
+
+BODY;
+ $this->notify("Re: ".$decision['tag']." - ".$decision['title']." - finalised",$body,$decision['tag']);
+ }
+ }
+ }
+ function notify($subject,$body,$tag,$first=FALSE)
+ {
+ $header = "Content-Type: text/plain; charset=UTF-8\r\n";
+ if ($first) {
+ $header .= "Message-id: <".$tag.">\r\n";
+ } else {
+ $header .= "References: <".$tag.">\r\nIn-reply-to: <".$tag.">\r\n";
+ }
+ mail($this->board, mb_encode_mimeheader($subject,"UTF-8", "B", "\n"),$body,$header."From: Voting System <returns@cacert.org>");
+ }
+ function vote_notify($subject,$body,$tag)
+ {
+ $header = "Content-Type: text/plain; charset=UTF-8\r\n";
+ $header .= "References: <".$tag.">\r\nIn-reply-to: <".$tag.">\r\n";
+ mail($this->notices, mb_encode_mimeheader($subject,"UTF-8", "B", "\n"),$body,$header."From: Voting System <returns@cacert.org>");
+ }
+ function remind_notify($email,$subject,$body)
+ {
+ $header = "Content-Type: text/plain; charset=UTF-8\r\n";
+ mail($email,$subject,$body,$header."From: Voting System <returns@cacert.org>");
+ }
+ function auth()
+ {
+ $stmt = $this->getStatement("get voter");
+ $stmt->execute(array($_SERVER['REMOTE_USER']));
+ $user = $stmt->fetch();
+ if ($user) return $user;
+ if ($_SERVER['SSL_CLIENT_S_DN_Email']) {
+ $stmt->execute(array($_SERVER['SSL_CLIENT_S_DN_Email']));
+ $user = $stmt->fetch();
+ if ($user) return $user;
+ }
+ $d=0;
+ while ($email=$_SERVER["SSL_CLIENT_S_DN_Email_$d"]) {
+ $stmt->execute(array($email));
+ $user = $stmt->fetch();
+ if ($user) return $user;
+ ++$d;
+ }
+ $dn=$_SERVER['SSL_CLIENT_S_DN'];
+ if (preg_match_all('/\/emailAddress=([^\/]*)/',$dn,$reg,PREG_SET_ORDER)) {
+ foreach ($reg as $emailarr) {
+ $stmt->execute(array($emailarr[1]));
+ $user = $stmt->fetch();
+ if ($user) return $user;
+ }
+ }
+ if ($_SERVER['SSL_CLIENT_CERT']) {
+ # subjectAltName unpresented by Apache http://httpd.apache.org/docs/trunk/mod/mod_ssl.html
+ # subjectAltName http://tools.ietf.org/html/rfc5280#section-4.2.1.6
+ # WARNING WARNING openssl_x509_parse is an unstable PHP API
+ $x509 = openssl_x509_parse($_SERVER['SSL_CLIENT_CERT']);
+ $subjectAltName = $x509['extensions']['subjectAltName']; // going off https://foaf.me/testSSL.php
+ #print_r(split("[, ]",$subjectAltName));
+ #print_r($x509);
+ #echo $subjectAltName;
+ if (preg_match_all('/email:([^, ]*)/',$subjectAltName,$reg,PREG_SET_ORDER)) {
+ foreach ($reg as $emailarr) {
+ $stmt->execute(array($emailarr[1]));
+ $user = $stmt->fetch();
+ if ($user) return $user;
+ }
+ }
+ }
+ return FALSE;
+ }
+ }
+?>
diff --git a/database.sql b/database.sql
new file mode 100644
index 0000000..12e9b84
--- /dev/null
+++ b/database.sql
@@ -0,0 +1,4 @@
+CREATE TABLE decisions (id INTEGER PRIMARY KEY, proposed DATETIME, proponent INTEGER, title VARCHAR(255), content TEXT, quorum INTEGER, majority INTEGER, status INTEGER, due DATETIME, modified DATETIME, tag varchar(255), votetype INT4 DEFAULT 0 NOT NULL);
+CREATE TABLE emails (voter INT4, address VARCHAR(255));
+CREATE TABLE voters (id INTEGER PRIMARY KEY, name VARCHAR(255), enabled INTEGER default 0, reminder VARCHAR(255));
+CREATE TABLE votes (decision INT4, voter INT4, vote INT4, voted DATETIME, notes text default '');
diff --git a/denied.php b/denied.php
new file mode 100644
index 0000000..9bb72d6
--- /dev/null
+++ b/denied.php
@@ -0,0 +1,12 @@
+<html>
+ <head>
+ <title>CAcert Board Decisions</title>
+ <meta http-equiv="Content-Type" content="text/html; charset='UTF-8'" />
+ <link rel="stylesheet" type="text/css" href="styles.css" />
+ </head>
+ <body>
+ <b>You are not authorized to act here!</b><br/>
+ <i>If you think this is in error, please contact the administrator</i>
+ <i>If you don't know who that is, it is definitely not an error ;)</i>
+ </body>
+</html> \ No newline at end of file
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..3363496
--- /dev/null
+++ b/index.php
@@ -0,0 +1,5 @@
+<?php
+ header("HTTP/1.0 301 Redirect");
+ header("Location: motions.php");
+ exit();
+?> \ No newline at end of file
diff --git a/motion.php b/motion.php
new file mode 100644
index 0000000..536de03
--- /dev/null
+++ b/motion.php
@@ -0,0 +1,136 @@
+<?php
+ if ($_SERVER['HTTPS'] != 'on') {
+ header("HTTP/1.0 302 Redirect");
+ header("Location: https://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
+ exit();
+ }
+ require_once("database.php");
+ $db = new DB();
+ if (!($user = $db->auth())) {
+ header("HTTP/1.0 302 Redirect");
+ header("Location: denied.php");
+ exit();
+ }
+ $db->getStatement("stats")->execute();
+ $stats = $db->getStatement("stats")->fetch();
+?>
+<html>
+ <head>
+ <title>CAcert Board Decisions</title>
+ <meta http-equiv="Content-Type" content="text/html; charset='UTF-8'" />
+ <link rel="stylesheet" type="text/css" href="styles.css" />
+ </head>
+ <body>
+ <?php
+ if ($_REQUEST['action'] == "store") {
+ $stmt = $db->getStatement("create decision");
+ $stmt->bindParam(":proponent",$user['id']);
+ $stmt->bindParam(":title",$_POST['title']);
+ $stmt->bindParam(":content",$_POST['content']);
+ $stmt->bindParam(":votetype",$_POST['votetype']);
+ $stmt->bindParam(":due",$_POST['due']);
+ if ($stmt->execute()) {
+ ?>
+ <b>The motion has been proposed!</b><br/>
+ <a href="motions.php">Back to motions</a><br/>
+ <br/>
+ <br/>
+ <?php
+ $decision = $db->getStatement("get new decision")->execute()?$db->getStatement("get new decision")->fetch():array();
+ $name = $user['name'];
+ $tag = $decision['tag'];
+ $title = $decision['title'];
+ $content =$decision['content'];
+ $due = $decision['due']." UTC";
+ $votetype = !$decision['votetype'] ? 'motion' : 'veto';
+ $baseurl = "https://".$_SERVER['HTTP_HOST'].":".$_SERVER['SERVER_PORT'].preg_replace('/motion\.php/','',$_SERVER['REQUEST_URI']);
+ $voteurl = $baseurl."vote.php?motion=".$decision['id'];
+ $unvoted = $baseurl."motions.php?unvoted=1";
+ $body = <<<BODY
+Dear Board,
+
+$name has made the following motion:
+
+$title
+$content
+
+Vote type: $votetype
+
+Voting will close $due.
+
+To vote please choose:
+
+Aye: $voteurl&vote=1
+Naye: $voteurl&vote=-1
+Abstain: $voteurl&vote=0
+
+To see all your outstanding votes : $unvoted
+
+Kind regards,
+the voting system
+BODY;
+ $db->notify("$tag - $title",$body,$tag,TRUE);
+ } else {
+ ?>
+ <b>The motion has NOT been proposed!</b><br/>
+ <a href="motions.php">Back to motions</a><br/>
+ <i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i><br/>
+ <br/>
+ <br/>
+ <?php
+ }
+ }
+
+ if (is_numeric($_REQUEST['motion'])) {
+ $stmt = $db->getStatement("get decision");
+ if ($stmt->execute(array($_REQUEST['motion']))) {
+ $motion = $stmt->fetch();
+ }
+ if (!is_numeric($motion['id'])) {
+ $motion = array();
+ foreach (array("title","content") as $column) {
+ $motion[$column] = "";
+ }
+ $motion["proposer"] = $user['name'];
+ $motion["votetype"] = 0; // defaults to motion
+ }
+ } else {
+ $motion = array();
+ foreach (array("title","content") as $column) {
+ $motion[$column] = "";
+ }
+ $motion["proposer"] = $user['name'];
+ $motion["votetype"] = 0; // defaults to motion
+ }
+ ?>
+ <form <?php if (is_numeric($_REQUEST['motion'])) { echo(" action=\"?\""); } ?> method="POST">
+ <input type="hidden" name="action" value="store" />
+ <?php
+ if (is_numeric($_REQUEST['motion'])) {
+ ?><input type="hidden" name="motion" value="<?php echo($_REQUEST["motion"]); ?>" /><?php
+ }
+ ?>
+ <table>
+ <tr><td>ID:</td><td><?php echo htmlentities($motion['tag']); ?></td></tr>
+ <tr><td>Proponent:</td><td><?php echo htmlentities($motion['proposer']); ?></td></tr>
+ <tr><td>Proposed date/time:</td><td><?php echo htmlentities($motion['proposed'] ? $motion['proposed']." UTC" : '(auto filled to current date/time)'); ?></td></tr>
+ <tr><td>Title:</td><td><input name="title" value="<?php echo htmlentities($motion['title'])?>"></td></tr>
+ <tr><td>Text:</td><td><textarea name="content"><?php echo htmlspecialchars($motion['content'])?></textarea></td></tr>
+ <tr><td>Vote type:</td><td><select name="votetype">
+ <option value="0" <?php if(!$motion['votetype']) { echo(" selected=\"selected\""); } ?>>Motion</option>
+ <option value="1" <?php if($motion['votetype']) { echo(" selected=\"selected\""); } ?>>Veto</option>
+ </select></td></tr>
+ <tr><td rowspan="2">Due:</td><td><?php echo($motion['due'] ? $motion['due'].' UTC' : '(autofilled from option below)')?></td></tr>
+ <tr><td><select name="due">
+ <option value="+3 days">In 3 Days</option>
+ <option value="+7 days">In 1 Week</option>
+ <option value="+14 days">In 2 Weeks</option>
+ <option value="+28 days">In 4 Weeks</option>
+ </select></td></tr>
+ <tr><td>&nbsp;</td><td><input type="submit" value="Propose" /></td></tr>
+ </table>
+ </form>
+ <br/>
+ <a href="motions.php">Back to motions</a>
+ </body>
+</html>
diff --git a/motions.php b/motions.php
new file mode 100644
index 0000000..b508385
--- /dev/null
+++ b/motions.php
@@ -0,0 +1,174 @@
+<?php
+ require_once("database.php");
+ $db = new DB();
+ $page = is_numeric($_REQUEST['page'])?$_REQUEST['page']:1;
+ $user = $db->auth();
+
+ if ($_REQUEST['withdrawl'] && $_REQUEST['confirm'] && $_REQUEST['id']) {
+ $stmt = $db->getStatement("get decision");
+ $stmt->bindParam(":decision",$_REQUEST['id']);
+ $stmt->execute();
+ $decision=$stmt->fetch();
+
+ if (!$decision || !$user || $user['id'] != $decision['proponent']) {
+ header("HTTP/1.0 302 Redirect");
+ header("Location: denied.php");
+ exit();
+ }
+ $name = $user['name'];
+ $tag = $decision['tag'];
+ $title = $decision['title'];
+ $content = $decision['content'];
+ $body = <<<BODY
+Dear Board,
+
+$name has withdrawn the motion $tag that was as follows:
+
+$title
+$content
+
+Kind regards,
+the voting system
+BODY;
+ $db->notify("Re: $tag - $title - withdrawn",$body,$tag);
+
+ $stmt = $db->getStatement("close decision");
+ $status = -2;
+ $stmt->bindParam(":status",$status);
+ $stmt->bindParam(":decision",$_REQUEST['id']);
+ $stmt->execute();
+ }
+?>
+<html>
+ <head>
+ <title>CAcert Board Decisions</title>
+ <meta http-equiv="Content-Type" content="text/html; charset='UTF-8'" />
+ <link rel="stylesheet" type="text/css" href="styles.css" />
+ </head>
+ <body>
+ <?php
+ if ($user) echo '<a href="?unvoted=1">Show my outstanding votes</a><br/>';
+ ?>
+ <table class="list">
+ <tr>
+ <th>Status</th>
+ <th>Motion</th>
+ <th>Actions</th>
+ </tr>
+ <?php
+ if ($_REQUEST['motion']) {
+ $stmt = $db->getStatement("list decision");
+ $stmt->execute(array($_REQUEST['motion']));
+ } else {
+ if ($user && $_REQUEST['unvoted']) {
+ $stmt = $db->getStatement("list my unvoted decisions");
+ $stmt->bindParam(":id",$user['id']);
+ } else {
+ $stmt = $db->getStatement("list decisions");
+ }
+ $stmt->bindParam(":page",$page);
+ $stmt->execute();
+ }
+ $items = 0;
+ $id = -1;
+ while ($row = $stmt->fetch()) {
+ $items++;
+ $id = $row['id'];
+ ?><tr>
+ <td class="<?php switch($row['status']) { case 0: echo "pending"; break; case 1: echo "approved"; break; case -1: echo "declined"; break; case -2: echo "withdrawn"; break; }?>">
+ <?php
+ switch($row['status']) {
+ case 0: echo "Pending<br/><i>".$row['due']." UTC</i>"; break;
+ case 1: echo "Approved<br/><i>".$row['modified']." UTC</i>"; break;
+ case -1: echo "Declined<br/><i>".$row['modified']." UTC</i>"; break;
+ case -2: echo "Withdrawn<br/><i>".$row['modified']." UTC</i>"; break;
+ }
+ ?>
+ </td>
+ <td>
+ <i><a href="motions.php?motion=<?php echo $row['tag'].'">'.$row['tag']; ?></a></i><br/>
+ <b><?php echo htmlspecialchars($row['title']); ?></b><br/>
+ <pre><?php echo wordwrap(htmlspecialchars($row['content'])); ?></pre>
+ <br/>
+ <i>Due: <?php echo($row['due']); ?> UTC</i><br/>
+ <i>Proposed: <?php echo($row['proposer']); ?> (<?php echo($row['proposed']); ?> UTC)</i><br/>
+ <i>Vote type: <?php echo(!$row['votetype']?'motion':'veto'); ?></i><br/>
+ <i>Aye|Naye|Abstain: <?php echo($row['ayes']); ?>|<?php echo($row['nayes']); ?>|<?php echo($row['abstains']); ?></i><br/>
+ <?php
+ if ($row['status'] ==0 || $_REQUEST['showvotes']) {
+ $state = array('Naye','Abstain','Aye');
+ $vstmt = $db->getStatement("list votes");
+ $vstmt->execute(array($row['id']));
+ echo "<i>Votes:</i><br/>";
+ while ($vrow = $vstmt->fetch()) {
+ echo "<i>".$vrow['name'].": ".$state[$vrow['vote']+1]."</i><br/>";
+ }
+ } else {
+ echo '<i><a href="motions.php?motion='.$row['tag'].'&showvotes=1">Show Votes</a></i><br/>';
+ }
+ ?>
+ </td>
+ <td class="actions">
+ <?php
+ if ($row['status'] == 0 && $user ) {
+ ?>
+ <ul>
+ <li><a href="vote.php?motion=<?php echo($row['id']); ?>&amp;vote=1">Aye</a></li>
+ <li><a href="vote.php?motion=<?php echo($row['id']); ?>&amp;vote=0">Abstain</a></li>
+ <li><a href="vote.php?motion=<?php echo($row['id']); ?>&amp;vote=-1">Naye</a></li>
+ <li><a href="proxy.php?motion=<?php echo($row['id']); ?>">Proxy Vote</a></li>
+ <?php
+ if ($user && $user['id'] == $row['proponent']) {
+ ?>
+ <li><a href="motions.php?motion=<?php echo($row['tag']); ?>&amp;withdrawl=1">Withdraw</a></li>
+ <?php
+ }
+ ?>
+ </ul>
+ <?php
+ } else {
+ ?>
+ &nbsp;
+ <?php
+ }
+ ?>
+ </td>
+ </tr><?php
+ }
+ ?>
+ <tr>
+ <td colspan="2" class="navigation">
+ <?php if ($page>1) { ?><a href="?page=<?php echo($page-1); ?>">&lt;</a><?php } else { ?>&nbsp;<?php } ?>
+ &nbsp;
+ <?php if ($items>9) { ?><a href="?page=<?php echo($page+1); ?>">&gt;</a><?php } else { ?>&nbsp;<?php } ?>
+ </td>
+ <td class="actions">
+ <?php if ($user) echo('<ul><li><a href="motion.php">New Motion</a></li></ul>'); ?>
+ </td>
+ </tr>
+ <?php
+ if ($_REQUEST['withdrawl']) {
+ ?>
+ <tr>
+ <td colspan="3">
+ <?php
+ if ($_REQUEST['confirm'] && $_REQUEST['id']) {
+ ?>
+ <a href="motions.php">Motion Withdrawn</a>
+ <?php
+ } else {
+ ?>
+ <form action="?withdrawl=1&amp;confirm=1&amp;id=<?php echo $id;?>" method="post">
+ <input type="submit" value="Withdraw">
+ </form>
+ <?php
+ }
+ ?>
+ </td>
+ </tr>
+ <?php
+ }
+ ?>
+ </table>
+ </body>
+</html>
diff --git a/proxy.php b/proxy.php
new file mode 100644
index 0000000..f1b837e
--- /dev/null
+++ b/proxy.php
@@ -0,0 +1,154 @@
+<?php
+ if ($_SERVER['HTTPS'] != 'on') {
+ header("HTTP/1.0 302 Redirect");
+ header("Location: https://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
+ exit();
+ }
+ require_once("database.php");
+ $db = new DB();
+ if (!($user = $db->auth())) {
+ header("HTTP/1.0 302 Redirect");
+ header("Location: denied.php");
+ exit();
+ }
+?>
+<html>
+ <head>
+ <title>CAcert Board Decisions</title>
+ <meta http-equiv="Content-Type" content="text/html; charset='UTF-8'" />
+ <link rel="stylesheet" type="text/css" href="styles.css" />
+ </head>
+ <body>
+<?php
+ if (!is_numeric($_REQUEST['motion'])) {
+?>
+ <b>This is not a valid motion!</b><br/>
+ <a href="motions.php">Back to motions</a><br/>
+<?php
+ } else {
+ $stmt = $db->getStatement("get decision");
+ $stmt->bindParam(":decision",$_REQUEST['motion']);
+ if ($stmt->execute() && ($decision=$stmt->fetch()) && ($decision['status'] == 0)) {
+ if (is_numeric($_POST['voter']) && is_numeric($_POST['vote']) && is_numeric($_REQUEST['motion']) && ($_POST['justification'] != "")) {
+ $stmt = $db->getStatement("del vote");
+ $stmt->bindParam(":voter",$_REQUEST['voter']);
+ $stmt->bindParam(":decision",$_REQUEST['motion']);
+ if ($stmt->execute()) {
+ $stmt = $db->getStatement("do vote");
+ $stmt->bindParam(":voter",$_REQUEST['voter']);
+ $stmt->bindParam(":decision",$_REQUEST['motion']);
+ $stmt->bindParam(":vote",$_REQUEST['vote']);
+ $notes = "Proxy-Vote by ".$user['name']."\n\n".$_REQUEST['justification']."\n\n".$_SERVER['SSL_CLIENT_CERT'];
+ $stmt->bindParam(":notes",$notes);
+ if ($stmt->execute()) {
+ ?>
+ <b>The vote has been registered.</b><br/>
+ <a href="motions.php">Back to motions</a>
+ <?php
+ $stmt = $db->getStatement("get voter by id");
+ $stmt->bindParam(":id",$_REQUEST['voter']);
+ if ($stmt->execute() && ($voter=$stmt->fetch())) {
+ $voter = $voter['name'];
+ } else {
+ $voter = "Voter: ".$_REQUEST['voter'];
+ }
+ $name = $user['name'];
+ $justification = $_REQUEST['justification'];
+ $vote = '';
+ switch($_REQUEST['vote']) {
+ case 1 : $vote='Aye'; break;
+ case -1: $vote='Naye'; break;
+ default: $vote='Abstain'; break;
+ }
+ $tag = $decision['tag'];
+ $title = $decision['title'];
+ $content = $decision['content'];
+ $due = $decision['due']." UTC";
+ $body = <<<BODY
+Dear Board,
+
+$name has just registered a proxy vote of $vote for $voter on motion $tag.
+
+The justification for this was:
+$justification
+
+Motion:
+$title
+$content
+
+Kind regards,
+the vote system
+
+BODY;
+ $db->notify("Re: $tag - $title",$body,$tag);
+ } else {
+ ?>
+ <b>The vote has NOT been registered.</b><br/>
+ <a href="motions.php">Back to motions</a>
+ <i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i>
+ <?php
+ }
+ } else {
+ ?>
+ <b>The vote has NOT been registered.</b><br/>
+ <a href="motions.php">Back to motions</a>
+ <i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i>
+ <?php
+ }
+ } else {
+ $stmt = $db->getStatement("get voters");
+ if ($stmt->execute() && ($voters = $stmt->fetchAll())) {
+?>
+ <form method="POST" action="?motion=<?php echo($_REQUEST['motion']); ?>">
+ <table>
+ <tr>
+ <th>Voter</th><th>Vote</th>
+ </tr>
+ <tr>
+ <td><select name="voter"><?php
+ foreach ($voters as $voter) {
+?>
+ <option value="<?php echo($voter['id']); ?>"<?php if ($voter['id'] == $_POST['voter']) { echo(" selected=\"selected\""); } ?>><?php echo($voter['name']); ?></option>
+<?php
+ }
+ ?></select></td>
+ <td><select name="vote">
+ <option value="1"<?php if (1 == $_POST['voter']) { echo(" selected=\"selected\""); } ?>>Aye</option>
+ <option value="0"<?php if (0 == $_POST['voter']) { echo(" selected=\"selected\""); } ?>>Abstain</option>
+ <option value="-1"<?php if (-1 == $_POST['voter']) { echo(" selected=\"selected\""); } ?>>Naye</option>
+ </select></td>
+ </tr>
+ <tr>
+ <th colspan="2">Justification:</th>
+ </tr>
+ <tr>
+ <td colspan="2"><textarea name="justification"><?php echo($_POST['justification']); ?></textarea></td>
+ </tr>
+ <tr>
+ <td colspan="2"><input type="submit" value="Proxy Vote" /></td>
+ </tr>
+ </table>
+ </form>
+<?php
+ } else {
+?>
+ <b>Could not retrieve voters!</b><br/>
+ <a href="motions.php">Back to motions</a><br/>
+ <i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i>
+<?php
+ }
+ }
+?>
+
+<?php
+ } else {
+?>
+ <b>This is not a valid motion!</b><br/>
+ <a href="motions.php">Back to motions</a><br/>
+ <i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i>
+<?php
+ }
+ }
+?>
+ </body>
+</html>
diff --git a/remind.php b/remind.php
new file mode 100755
index 0000000..b3ce74c
--- /dev/null
+++ b/remind.php
@@ -0,0 +1,43 @@
+#!/usr/bin/php
+<?
+require_once("database.php");
+$db = new DB();
+
+$id = 0;
+$page = 1;
+
+$voters = $db->getStatement('get reminder voters');
+$voters->execute();
+
+$outstanding = $db->getStatement('list my unvoted decisions');
+$outstanding->bindParam(':id',$id);
+$outstanding->bindParam(':page',$page);
+
+while ($v = $voters->fetch()) {
+ $id = $v['id'];
+ $outstanding->execute();
+ $msg ='';
+ while ($row=$outstanding->fetch()) {
+ $msg .= ($row['votetype'] ? 'vote ' : 'motion ') . $row['tag'] . ' ' . $row['title'] . "\nDue: " . $row['due'] . "\nhttps://community.cacert.org/board/motions.php?motion=" . $row['tag'] . "\n\n";
+ }
+ if ($msg) {
+ // form email
+ $name = $v['name'];
+ $body = <<<BODY
+Dear $name,
+
+You have not voted in the following CAcert Board vote(s)/motion(s):
+
+$msg
+
+
+To view all your outstanding motions: https://community.cacert.org/board/motions.php?unvoted=1
+
+Kind regards,
+the vote system
+
+BODY;
+ $db->remind_notify($v['email'],"Outstanding CAcert board votes",$body);
+ }
+}
+?>
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..1cf3d72
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,31 @@
+html, body, th, td {
+ font-family: Verdana, Arial, Sans-Serif;
+ font-size:10px;
+}
+table, tr, td, th {
+ vertical-align:top;
+ border:1px solid black;
+ border-collapse: collapse;
+}
+td.navigation {
+ text-align:center;
+}
+td.approved {
+ color:green;
+}
+td.declined {
+ color:red;
+}
+td.withdrawn {
+ color:red;
+}
+td.pending {
+ color:blue;
+}
+textarea {
+ width:400px;
+ height:150px;
+}
+input {
+ width:400px;
+}
diff --git a/vote.php b/vote.php
new file mode 100644
index 0000000..ef1b6fd
--- /dev/null
+++ b/vote.php
@@ -0,0 +1,104 @@
+<?php
+ if ($_SERVER['HTTPS'] != 'on') {
+ header("HTTP/1.0 302 Redirect");
+ header("Location: https://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
+ exit();
+ }
+ require_once("database.php");
+ $db = new DB();
+ if (!($user = $db->auth())) {
+ header("HTTP/1.0 302 Redirect");
+ header("Location: denied.php");
+ exit();
+ }
+?>
+<html>
+ <head>
+ <title>CAcert Board Decisions</title>
+ <meta http-equiv="Content-Type" content="text/html; charset='UTF-8'" />
+ <link rel="stylesheet" type="text/css" href="styles.css" />
+ </head>
+ <body>
+ <?php
+ if (is_numeric($_REQUEST['motion']) && is_numeric($_REQUEST['vote'])) {
+ $stmt = $db->getStatement("get decision");
+ $stmt->bindParam(":decision",$_REQUEST['motion']);
+ if ($stmt->execute() && ($decision=$stmt->fetch())) {
+ if ($decision['status'] == 0) {
+ $stmt = $db->getStatement("del vote");
+ $stmt->bindParam(":voter",$user['id']);
+ $stmt->bindParam(":decision",$_REQUEST['motion']);
+ if ($stmt->execute()) {
+ $stmt = $db->getStatement("do vote");
+ $stmt->bindParam(":voter",$user['id']);
+ $stmt->bindParam(":decision",$_REQUEST['motion']);
+ $stmt->bindParam(":vote",$_REQUEST['vote']);
+ $notes="Direct Vote\n\n".$_SERVER['SSL_CLIENT_CERT'];
+ $stmt->bindParam(":notes",$notes);
+ if ($stmt->execute()) {
+ ?>
+ <b>Your vote has been registered.</b><br/>
+ <a href="motions.php">Back to motions</a>
+ <?php
+ $name = $user['name'];
+ $vote = '';
+ switch($_REQUEST['vote']) {
+ case 1 : $vote='Aye'; break;
+ case -1: $vote='Naye'; break;
+ default: $vote='Abstain'; break;
+ }
+ $tag = $decision['tag'];
+ $title = $decision['title'];
+ $content = $decision['content'];
+ $due = $decision['due']." UTC";
+ $body = <<<BODY
+Dear Board,
+
+$name has just voted $vote on motion $tag.
+
+Motion:
+ $title
+ $content
+
+Kind regards,
+the vote system
+
+BODY;
+ $db->vote_notify("Re: $tag - $title",$body,$tag);
+ } else {
+ ?>
+ <b>Your vote has NOT been registered.</b><br/>
+ <a href="motions.php">Back to motions</a>
+ <i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i>
+ <?php
+ }
+ } else {
+ ?>
+ <b>Your vote has NOT been registered.</b><br/>
+ <a href="motions.php">Back to motions</a>
+ <i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i>
+ <?php
+ }
+ } else {
+ ?>
+ <b>Your vote has NOT been registered.</b><br/>
+ <b>Voting is alread closed!</b><br/>
+ <a href="motions.php">Back to motions</a>
+ <?php
+ }
+ } else {
+ ?>
+ <b>Your vote has NOT been registered.</b><br/>
+ <b>Could not find the motion to be voted!</b><br/>
+ <a href="motions.php">Back to motions</a>
+ <?php
+ }
+ } else {
+ ?>
+ <b>This call is not a valid vote!</b><br/>
+ <a href="motions.php">Back to motions</a>
+ <?php
+ }
+ ?>
+ </body>
+</html>