forked from cambridgesu/bob
-
Notifications
You must be signed in to change notification settings - Fork 0
/
BOB.php
4298 lines (3583 loc) · 191 KB
/
BOB.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
/*
* This file is part of the Basic Online Ballot-box (BOB).
* https://github.com/cusu/bob
* License: GPL; see below
* Copyright David Eyers, Martin Lucas-Smith and contributors 2005-2021
*
* Significant contributions (but almost certainly not responsible for any nasty code) :
* David Turner, Simon Hopkins, Robert Whittaker
*
* Requires : index.php as a bootstrap file; see installation notes below
* Requires : Container-managed authentication
* Uses : MySQL
*
* Token word list Copyright The Internet Society (1998).
*
* Version 1.10.2
*
* Copyright (C) authors as above
*
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/*
* INSTALLATION
*
* Create a file index.html or index.php (whichever is allowed in a DirectoryIndex) like this,
* which defines the settings then instantiates the BOB class (which must be in your include_path) with those settings.
*
<?php
## Config file for BOB ##
## All settings must be specified, except for these (which will revert to internal defaults if omitted): dbHostname,countingInstallation,countingMethod,urlMoreInfo,adminDuringElectionOK,randomisationInfo,referendumThresholdPercent,referendumThresholdIsYesVoters,frontPageMessageHtml,afterVoteMessageHtml,voterReceiptDisableable,disableListWhoVoted,organisationName,organisationUrl,organisationLogoUrl,headerLocation,footerLocation,additionalVotesCsvDirectory
# Unique identifier for this ballot
$config['id'] = 'testelection';
# Database connection details
$config['dbHostname'] = 'localhost';
$config['dbDatabase'] = 'testvote';
$config['dbPassword'] = 'your_password_goes_here';
$config['dbDatabaseStaging'] = false; // or a different database name if the configuration is shifted from a staging database before the vote opens to the main append-only database
$config['dbUsername'] = 'testvote';
$config['dbSetupUsername'] = 'testvotesetup';
# Counting installation config; must end /openstv/ (slash-terminated)
$config['countingInstallation'] = '%documentroot/openstv/';
$config['countingMethod'] = 'ERS97STV';
# Title and info about the ballot
$config['title'] = "Some electronic ballot"; // Text, no HTML
$config['urlMoreInfo'] = 'http://www.example.com/'; // Or false if none
# Details of Returning Officer, Sysadmins, and usernames of the election officials
$config['emailReturningOfficer'] = 'returningOfficer@localhost'; // In a managed hosting scenario, this might be a master mailbox rather than the officials' e-mail account(s)
$config['emailTech'] = 'adminperson@localhost';
$config['officialsUsernames'] = 'abc12 xyz98'; // Space-separated
# Start and end of the ballot and when the votes can be viewed
$config['ballotStart'] = '2009-02-13 00:00:00';
$config['ballotEnd'] = '2009-02-18 00:01:00';
$config['paperVotingEnd'] = false;
$config['ballotViewableDelayed'] = false;
# Textual information about any randomisation which may have been made
$config['randomisationInfo'] = false; // Will have htmlspecialchars applied to it
# Percentage of voters who must cast a vote in a referendum for the referendum to be countable
$config['referendumThresholdPercent'] = 10;
# Referenda pass if simple majority, but also requires either: (false, the default) x% to turn up to vote, or (true) yes-vote count not less than x% of eligible voters
$config['referendumThresholdIsYesVoters'] = false;
# Extra messages (as HTML), if any, which people will see on the front page before voting, and when they have voted
$config['frontPageMessageHtml'] = false;
$config['afterVoteMessageHtml'] = false;
# Whether users can choose to disable the e-mail vote receipt
$config['voterReceiptDisableable'] = false;
# Whether to disable the list of usernames who voted that appears on the show votes page afterwards (the default increases voter assurance but at expense of some privacy)
$config['disableListWhoVoted'] = false;
# Whether the administrator can access certain admin pages during the election
$config['adminDuringElectionOK'] = false;
# Organisation name, logo and link (all optional; set to false if not wanted)
$config['organisationName'] = "Some organisation"; // Will have htmlspecialchars applied to it
$config['organisationUrl'] = 'http://www.example.com/';
$config['organisationLogoUrl'] = 'https://www.example.com/somelogo.png'; // Will be resized to height=60; Also, you are advised to put this on an https host to avoid security warnings
# Location in the URL space of optional header and footer file; must start with /
$config['headerLocation'] = '/style/header.html';
$config['footerLocation'] = '/style/footer.html';
# Directory where additional votes cast on paper transcribed into a CSV file are stored; must have filename <id>.draft.csv (for testing of the result) and <id>.final.csv (which is made public)
$config['additionalVotesCsvDirectory'] = false;
# Number of posts being elected; each position and the candidate names; each block separated by one line break
# If any contain accented/etc. characters, ensure this file is saved as UTF-8 without a Byte Order Mark (BOM)
$config['electionInfo'] = <<<ENDOFDATA
1
Position 1 - perhaps "President"
Some Candidate
Another Candidate
1
Another Position
A candidate for this position
Hopefully you get the idea
2
Each separate block
Represents another vote
First line of a block is the position title
All other lines are Candidate names
1
Referendum: Do you agree with the proposed changes to the Constitution?
referendum
ENDOFDATA;
## End of config; now run the system ##
# Load and run the BOB class
require_once ('BOB.php');
new BOB ($config);
*
* Alternatively create a file index.html or index.php (whichever is allowed in a DirectoryIndex) like this,
* but this time with most of the config being in a database table, and just the following defined in the settings file:
*
<?php
## Config file for BOB ##
## All settings must be specified, except for these (which will revert to internal defaults if omitted): dbHostname,countingInstallation,countingMethod
# Unique name for this ballot
$config['id'] = 'testelection';
# Database connection details
$config['dbHostname'] = 'localhost';
$config['dbDatabase'] = 'testvote';
$config['dbPassword'] = 'your_password_goes_here';
$config['dbDatabaseStaging'] = false;
$config['dbUsername'] = 'testvote';
$config['dbSetupUsername'] = 'testvotesetup';
# Optional database table containing the config which the dbSetupUsername has SELECT rights on
$config['dbConfigTable'] = 'instances';
# Counting installation config; must end /openstv/ (slash-terminated)
$config['countingInstallation'] = '%documentroot/openstv/';
$config['countingMethod'] = 'ERS97STV';
# The database table should contain these fields, in addition to id as above:
# title,urlMoreInfo,emailReturningOfficer,emailTech,officialsUsernames,ballotStart,ballotEnd,paperVotingEnd,ballotViewableDelayed,randomisationInfo,referendumThresholdPercent,frontPageMessageHtml,afterVoteMessageHtml,voterReceiptDisableable,disableListWhoVoted,adminDuringElectionOK,organisationName,organisationUrl,organisationLogoUrl,headerLocation,footerLocation,additionalVotesCsvDirectory,electionInfo
# However, urlMoreInfo,referendumThresholdPercent,referendumThresholdIsYesVoters,frontPageMessageHtml,afterVoteMessageHtml,voterReceiptDisableable,disableListWhoVoted,adminDuringElectionOK,headerLocation,footerLocation,additionalVotesCsvDirectory are optional fields which need not be created
## End of config; now run the system ##
# Load and run the BOB class
require_once ('BOB.php');
new BOB ($config);
*
E.g. to create the instances table, use the following.
Note that, in a managed GUI voting scenario, the items commented out with -- may be wanted. They are not needed by BOB itself.
There are other fields, e.g. adminDuringElectionOK,headerLocation,additionalVotesCsvDirectory,footerLocation,voterReceiptDisableable,disableListWhoVoted are best set as a global config file option.
CREATE TABLE IF NOT EXISTS `instances` (
`id` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'Generated globally-unique ID',
-- `url` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'Computed URL location of this ballot',
-- `academicYear` varchar(5) COLLATE utf8_unicode_ci NOT NULL COMMENT 'Computed academic year string',
-- `urlSlug` varchar(20) COLLATE utf8_unicode_ci NOT NULL COMMENT 'Unique identifier for this ballot',
-- `provider` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'Provider name',
-- `organisation` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'Organisation name',
`title` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'Title of this ballot',
`urlMoreInfo` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'URL for more info about the ballot',
`frontPageMessageHtml` text COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'Optional front-page message',
`afterVoteMessageHtml` text COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'An extra message, if any, which people will see when they have voted',
`emailReturningOfficer` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'E-mail address of Returning Officer / mailbox',
`emailTech` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'E-mail address of Technical Administrator',
`officialsUsernames` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'Usernames of Returning Officer + Sysadmins',
`randomisationInfo` enum('','Candidate order has been automatically randomised','Candidate order has been automatically alphabetised','Candidates have been entered by the Returning Officer in the order shown') COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'Candidate ordering/randomisation',
`organisationName` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'Organisation name',
`organisationUrl` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'Organisation URL',
`organisationLogoUrl` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'URL of organisation''s logo',
-- `addRon` enum('','Yes','No') COLLATE utf8_unicode_ci NOT NULL COMMENT 'Should Re-Open Nominations (RON) be automatically added as an additional candidate in each election?',
`electionInfo` text COLLATE utf8_unicode_ci NOT NULL COMMENT 'Election info: Number of positions being elected; Position title; Names of candidates; each block separated by one line break',
-- `electionInfoAsEntered` text COLLATE utf8_unicode_ci NOT NULL COMMENT 'Election info',
`referendumThresholdPercent` int(2) DEFAULT '10' COMMENT 'Percentage of voters who must cast a vote in a referendum for the referendum to be countable',
`referendumThresholdIsYesVoters` int(1) DEFAULT NULL COMMENT 'Whether the threshold refers to yes-vote level (rather than all voter turnout)',
`ballotStart` datetime NOT NULL COMMENT 'Start date/time of the ballot',
`ballotEnd` datetime NOT NULL COMMENT 'End date/time of the ballot',
`paperVotingEnd` datetime DEFAULT NULL COMMENT 'End time of paper voting, if paper voting is also taking place',
`ballotViewableDelayed` datetime DEFAULT NULL COMMENT 'End date/time for delayed viewing of results by voters',
`leaderboardTemplate` VARCHAR(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'Leaderboard template',
-- `instanceCompleteTimestamp` datetime DEFAULT NULL COMMENT 'Timestamp for when the instance (configuration and voters list) is complete',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
*
*/
/*
BOB: Threats and risks
----------------------
We focus on the relevance to BOB of two of the primary risks inherent in
common balloting systems, both paper-based and electronic:
(a) Manipulation of the results of an election
(b) Compromised anonymity of voters
Although strenuous efforts have been made to avoid the BOB software
containing flaws, it has always been a design goal to shun approaches that
cannot have their results verified independently of the software.
The catch-all for handling (a) is the post-election verification process
(PEV). During the PEV all recorded votes and corresponding pseudononymous
tokens are displayed to the electorate. Voters can choose to reveal the
link between their recorded preferences and their otherwise secret
pseudononymous token, in order to demonstrate a flawed ballot record. For
the PEV to be effective, it is key that a sufficient number of voters
check that their votes really have been recorded correctly. In contentious
cases, it may sometimes make sense to go further and have a trusted third
party (e.g. the returning officer) keep a list of people signing that they
have verified their votes.
The PEV itself must be carried out in an appropriate manner - e.g. both
electronic and hard-copy output produced, with the latter placed in an
accessible but secure location, in which manipulation is likely to be
detected.
In terms of (b), voters can choose to compromise their own anonymity, e.g.
to sell votes. This is an unfortunate but unavoidable side-effect of the
PEV. BOB was designed in the view that the value of the PEV outweighed the
risks of vote-selling.
== Participants ==
Types of people in the BOB process:
(*) Voters.
(*) Returning officer: receives electronic votes by email.
(*) Sysadmins: those who have super-user privileges on the computer
running the voting system code. In cases where the sysadmins are directly
involved in the election - as opposed to operating a shared
general-purpose computing infrastructure - the sysadmins perform the key
security-related aspects of the traditional role of a Returning Officer;
they are effectively the transporters and guards of votes.
(*) Election admins: those who install and/or configure the voting system.
They may be overlap with the sysadmin group.
== Some technological risk areas ==
All of the technologies on which BOB relies need to be considered for
risks to voting process and voter anonymity. Consider possible privileges
over ---and the side-channels available from---at least the following:
* The BOB configuration and code. * The webserver. Access logs, webserver
config, PHP's "helpfulness", etc. * The database server. Logs, etc. * The
MTA for email. Logs, etc. * Server OS, file system, physical equipment,
etc. * The network in which the server is placed: also issues such as DNS.
At a technical level, anonymity can be compromised by having access to the
webserver, database server, MTA or OS (e.g. log file monitoring, network
monitoring).
The BOB software contains a large number of checks and balances, and will
attempt to highlight accidental misconfiguration of an election. Assuming
trustworthy sysadmins and an uncompromised base system, it is easy for the
BOB software to provide an indication to voters, e.g. through the
presentation of MD5 checksums, as to whether its configuration or code has
changed, for example under the control of election admins.
In terms of malicious damage, if the sysadmins are party to this
deception, or intruders gain sysadmin privileges, there is no current
mechanism by which BOB can inform voters of election admins of this
reliably. However, the election outcome will be protected by the PEV.
== Recommended deployment checks ==
Some checks recommended for BOB deployment are:
* That access control to the DNS entries for the voting server be
audited.
* That the MTA correctly strips BCC headers from messages. (The
decision was made to send one message from BOB to both the voter and
the returning officer. Sending two emails carries the risk that the
system fails between the two message sends (of course some
unavoidable, pathological failure conditions remain).)
* The set of users for MySQL be examined to ensure that only
appropriate users have access to the necessary BOB tables. (Two
database users are employed per ballot to ensure that the privileges
for setting up a ballot database are isolated from the privileges
required to run a ballot. Consider that MySQL replication could make
it easier for attackers to compromise voter anonymity.)
* The webserver access and error logs need to be protected.
* The cookie-based authentication scheme, and all web interactions,
run over HTTPS. (BOB itself should enforce this also.) Authentication
cookies are password-equivalent to an active attacker over HTTP links.
* The Returning Officer is encouraged to enlist people to perform PEV.
== Common BOB deployment scenarios ==
BOB can run - and has been run - in many different system configurations.
Some configurations carry more risk than others. For example:
(1) An externally-managed service. Election officials, the returning
officer and voters have no access to logs or configuration settings of the
webserver, database server, MTA information or machine OS, etc. In this
situation both election officials and voters need to trust the sysadmins,
and that the managed setup is safe in terms of vulnerabilities and leaks
in all areas. Since one system of this type will probably host multiple
elections, the data of each election must be isolated (particularly
regarding votes being configured in parallel to ballots being open, and
parallel ballots). Printed diagnostics from the code (e.g. MD5 checksums,
configuration variables, etc) can be selectively faked if the hosting is
compromised. On a non-compromised system, these diagnostics can give
voters confidence that the configuration of the election is valid and
unmodified during the course of the election.
(2) Local installation (e.g. on equipment managed by the organisation
carrying out the vote). Sysadmins on that equipment can trivially and
undetectably violate anonymity if they are able to observe voters voting
at the same time as monitoring the voting server. Software can be
maliciously modified to return fake MD5 sums, etc. If the software
operates as specified, then the voters can trust that the election
officials (assuming that they are not sysadmins) cannot undetectably
(until PEV) manipulate the configuration or results of an election.
(3) Restricted hosting (e.g. on multi-user systems). The election admins
in these contexts are assumed to have very limited administrative rights.
They may not be able to set up entities to run with less privilege than
themselves. This breaks the potential to enforce desirable isolation
properties in terms of BOB configuration and BOB databases. Anonymity is
probably easily breakable by other users running on a multi-user system
who are able to observe voters in real life simultaneously. As in all
cases, though, the PEV can determine whether manipulation of the balloting
process occurred. This kind of scenario is generally not recommended.
While the above considerations may appear quite onerous, most paper-based
balloting systems also carry serious risks of manipulation: we note that
they will usually preserve voter anonymity more effectively than being
able to guarantee the election outcome.
- The BOB team, February 2009.
*/
/*
Explanation of BLT format
=========================
4 2 # four candidates are competing for two seats
-2 # Bob has withdrawn (optional)
1 4 1 3 2 0 # first ballot
1 2 4 1 3 0
1 1 4 2 3 0 # The first number is the ballot weight (>= 1).
1 1 2 4 3 0 # The last 0 is an end of ballot marker.
1 1 4 3 0 # Numbers in between correspond to the candidates
1 3 2 4 1 0 # on the ballot.
1 3 4 1 2 0
1 3 4 1 2 0 # Chuck, Diane, Amy, Bob
1 4 3 2 0
1 2 3 4 1 0 # last ballot
0 # end of ballots marker
"Amy" # candidate 1
"Bob" # candidate 2
"Chuck" # candidate 3
"Diane" # candidate 4
"Gardening Club Election" # title
*/
/*
* TODO
*
* - Reverse the looping design in vote()
* - Upgrade more queries to use prepared statements
* - Convert all pages to return $html rather than echo it directly
* - Add the ability to disable the verifyRuntimeDatabasePrivileges() check, as two users may not be possible in some hosting environments; that will reduce security to some extent
* - Consider changing the duringElection, afterElection, afterBallotView flags to a state model and set this using NOW() in the database call
* - In the registeredVoters() routine, when there are no users, as well as the SQL, list the same thing but in a format directly invocable at shell, with a --extra-defaults-file=mysql.ini flag; then change the openDatabaseConnection()'s call of file_get_contents to an ini read
* - Enable the string 'Raven' and the .htaccess example to be non-Raven specific (or show the example as one of a number of examples)
* - Other items marked with #!# below
*/
# PHP5 class to implement an online election system
class BOB
{
# Config defaults (setting both structure and default values; NULL means that the instantiator must supply a value); see the above under 'INSTALLATION' for what these each mean
private $defaults = array (
'id' => NULL,
'dbHostname' => 'localhost',
'dbPassword' => NULL,
'dbDatabase' => NULL,
'dbDatabaseStaging' => false,
'dbUsername' => NULL,
'dbSetupUsername' => NULL,
'dbConfigTable' => false,
'countingInstallation' => '%documentroot/openstv/', // Absolute path to counting installation; must end /openstv/ (slash-terminated) ; %documentroot can be used as a starting placeholder which will be substituted if present
'countingMethod' => 'ERS97STV', // Method as per the list at https://github.com/cusu/openstv/tree/master/openstv/MethodPlugins
);
# Config defaults (setting both structure and default values; NULL means that the instantiator must supply a value) that can come from a database table; if not these will get merged with the above main defaults
private $defaultsDatabaseable = array (
'title' => NULL,
'urlMoreInfo' => false,
'emailReturningOfficer' => NULL,
'emailTech' => NULL,
'officialsUsernames' => NULL,
'randomisationInfo' => false,
'referendumThresholdPercent' => 10,
'referendumThresholdIsYesVoters' => false,
'adminDuringElectionOK' => false,
'ballotStart' => NULL,
'ballotEnd' => NULL,
'paperVotingEnd' => false,
'ballotViewableDelayed' => false,
'frontPageMessageHtml' => false,
'afterVoteMessageHtml' => false,
'voterReceiptDisableable' => false,
'disableListWhoVoted' => false,
'organisationName' => false,
'organisationUrl' => false,
'organisationLogoUrl' => false,
'headerLocation' => false,
'footerLocation' => false,
'additionalVotesCsvDirectory' => false,
'electionInfo' => NULL,
'leaderboardTemplate' => false,
'trackStatusDemographic' => false,
'selfIdentification' => false, // Or associative array
);
# Registry of available actions
private $actions = array (
/* Functions available to normal users */
'home' => array (
'description' => 'Summary',
'administrator' => false,
),
/* Functions available to normal users */
'vote' => array (
#!# The description should really change to "Your vote has been cast" once cast
'description' => 'Cast your vote',
'administrator' => false,
),
'showvotes' => array (
'description' => 'View votes',
'administrator' => false,
),
'results' => array (
'description' => 'View results',
'administrator' => false,
),
'viewsource' => array (
'description' => 'Show the source code of this program',
'administrator' => false,
'disableGui' => true,
),
'leaderboard' => array (
'description' => 'Show leaderboard',
'administrator' => false,
'disableGui' => true,
),
/* Admin functions */
'admin' => array (
'description' => 'Admin menu',
'administrator' => true,
),
'admin_rollcheck' => array (
'description' => 'Roll check for a user',
'administrator' => true,
),
'admin_viewform' => array (
'description' => 'View the ballot form',
'administrator' => true,
),
'admin_paperroll' => array (
'description' => 'Electoral roll for paper voting (following online voting)',
'administrator' => true,
'require' => 'paperVotingFollowsOnlineVoting',
),
'admin_ballotpapers' => array (
'description' => 'Printable ballot papers for paper voting (following online voting)',
'administrator' => true,
'require' => 'paperVotingFollowsOnlineVoting',
),
'admin_additionalvotes' => array (
'description' => 'Enter additional votes (from paper voting)',
'administrator' => true,
'require' => 'splitElection',
),
);
# State variables
private $databaseConnection = NULL; // Database object
private $errors = array (); // Array of setup errors
private $config = array (); // Global config (i.e. $this->config; not $config which comes into the constructor, which is local)
private $username = NULL; // Username of voter
private $beforeElection = false; // Whether we are before the election has opened
private $duringElection = false; // Whether we are during the election (online voting)
private $afterElection = false; // Whether we are after the election has closed
private $afterBallotView = false; // Whether we are after the point when the ballot is viewable
private $registeredVoters = 0; // Number of registered voters
private $totalVoted = 0; // Number of people that have voted
private $userIsRegisteredVoter = false; // Whether the user is on the electoral roll
private $userHasVoted = false; // Whether the user has voted
# Other class variables
#!# Some of these could be made into per-instance settings, e.g. logoutLocation and convertTo_CandidateToNumber
private $logoutLocation = 'logout.html'; // Logout location which will get inserted (unprocessed) into strings mentioning this
private $documentRoot = false; // Document root, which will be derived from DOCUMENT_ROOT
private $headerHtml = ''; // HTML for the header
private $footerHtml = ''; // HTML for the footer
private $pageTitle = false; // Title of the page
private $voterTable = false; // Name of the table of voters
private $votesTable = false; // Name of the table of votes
private $ballotStartFormatted; // Formatted date for ballot start
private $ballotEndFormatted; // Formatted date for ballot end
private $ballotViewableFormatted; // Formatted date for ballot being viewable
private $positionInfo; // Positions available per election; derived from electionInfo
private $bobMd5; // MD5 of the BOB program (this file)
private $configMd5; // MD5 of the config being used
private $convertTo_CandidateToNumber = true; // In the admin ballot printing mode, whether to convert to candidate=>number format
private $additionalVotePrefix = 'additionalvote'; // Prefix for additional votes
private $additionalVotesFile = false; // Additional votes file - filename in use (if any)
private $additionalVotesFileFinal = false; // Additional votes file - whether the data is finalised
# Define what a referendum looks like in terms of the available candidates
private $referendumCandidates = array ('0' => '(blank)', '1' => 'Yes', '2' => 'No');
# Contants
const MINIMUM_PHP_VERSION = '5';
/* START OF SETUP, SANITY-CHECKING AND INSTANTIATION SECTION */
# Constructor (front controller)
function __construct ($config = array ()) // $config is an array coming from a launching file such as index.php or index.html which instantiates the class
{
# Assign the load time
$this->loadtime = time ();
# Load required libraries
require_once (__DIR__ . '/database.php');
# Create an HTML representation of the config structure so it can be echoed to screen below
$configHtml = $this->configHtml ($config, 'coming into the system');
# Assign the configuration
if (!$this->assignConfiguration ($config)) {
$this->showErrors ();
return false;
}
# Process parts of the configuration
if (!$this->processConfiguration ()) {
$this->showErrors ();
return false;
}
# Create an HTML representation of the config structure so it can be echoed to screen below
$configHtml .= $this->configHtml ($this->config, 'after it has been processed and sanitised, as used by the runtime voting workflow');
# Ensure a clean server environment (e.g. register_globals off, etc.)
if (!$this->environmentIsOk ()) {
$this->showErrors ();
return false;
}
# Ensure that the DOCUMENT_ROOT is not slash-terminated
$this->documentRoot = $_SERVER['DOCUMENT_ROOT'];
if (substr ($_SERVER['DOCUMENT_ROOT'], -1) == '/') {
$this->documentRoot = substr ($_SERVER['DOCUMENT_ROOT'], 0, -1);
}
# Ensure there is a username and assign it
if (!$this->username = $this->getUsername ()) {
$this->showErrors ();
return false;
}
# Set the table names for this vote
$this->voterTable = $this->config['id'] . '_voter'; // Username + voted flag
$this->votesTable = $this->config['id'] . '_votes'; // Storage of votes + vote token
# If status demographic tracking is enabled, define the demographics table
if ($this->config['trackStatusDemographic']) {
$this->demographicsTable = $this->config['id'] . '_demographics';
}
# Set up the tables if they do not exist, complaining if they exist but are incorrect
if (!$this->setupTables ()) {
$this->showErrors ();
return false;
}
# Connect to the database at the runtime user privilege level
if (!$this->openDatabaseConnection ($this->config['dbUsername'])) {
$this->errors[] = "... therefore the runtime database connection could not be established.";
$this->showErrors ();
return false;
}
# Ensure the correct privileges for the runtime database user are in place
if (!$this->verifyRuntimeDatabasePrivileges ()) {
$this->showErrors ();
return false;
}
# Ensure that there are users in the voter table
#!# registeredVoters should probably be renamed votersOnOnlineRoll, or similar, for clarity
if (!$this->registeredVoters = $this->registeredVoters ()) {
$this->showErrors ();
return false;
}
# Ensure the server environment provides sufficient memory
if (!$this->environmentProvidesSufficientMemory ()) {
$this->showErrors ();
return false;
}
# Ensure the counting program is correctly installed, if specified
if (!$this->countingProgramConfigValid ()) {
$this->showErrors ();
return false;
}
# Get the total number of people that have voted; this also performs a database consistency check
$this->totalVoted = $this->totalVoted ();
if ($this->totalVoted === false) {
$this->showErrors ();
return false;
}
# Set whether the user is on the electoral roll and whether they have voted
list ($this->userIsRegisteredVoter, $this->userHasVoted) = $this->userRegisteredVoted ();
# Set whether the user is an election official
$this->userIsElectionOfficial = $this->userIsElectionOfficial ();
# Set time-based states
$this->beforeElection = $this->beforeElection ();
$this->duringElection = $this->duringElection ();
$this->afterElection = $this->afterElection ();
$this->afterBallotView = $this->afterBallotView ();
$this->afterBallotViewDelayed = $this->afterBallotViewDelayed ();
# Ensure there are no votes before the start of the election
if ($this->beforeElection && $this->totalVoted) {
$this->errors[] = "There are people marked as having voted, but the election has not yet started.";
$this->showErrors ();
return false;
}
# Set whether this is a split election (online and paper)
$this->splitElection = $this->splitElection ();
# Set whether, for a split election, whether paper voting follows online voting (rather than being concurrent)
$this->paperVotingFollowsOnlineVoting = ($this->splitElection && ($this->config['paperVotingEnd'] > $this->config['ballotEnd']));
# Ensure correct setup of any CSV file(s) for additional votes transcribed from paper
if (!$this->additionalVotesSetupOk ()) {
$this->showErrors ();
return false;
}
# Unset actions failing to meet a required property
foreach ($this->actions as $action => $attributes) {
if (array_key_exists ('require', $attributes)) {
$requirementProperty = $attributes['require'];
if (!$this->{$requirementProperty}) {
unset ($this->actions[$action]);
}
}
}
# Validate and set the action
$defaultAction = 'home';
$requestedAction = (strlen ($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : $defaultAction);
$action = (isSet ($this->actions[$requestedAction]) ? $requestedAction : false);
# Set the title
if ($action) {
$this->pageTitle = htmlspecialchars ($this->config['title']) . ($this->actions[$action]['description'] ? ':<br />' . htmlspecialchars ($this->actions[$action]['description']) : '');
} else {
$this->pageTitle = 'Error 404: Page not found';
}
# Assign the header and footer
if (!$this->assignHeaderAndFooter ()) {
$this->showErrors ();
return false;
}
# Determine whether to disable the GUI
$disableGui = ($action && isSet ($this->actions[$action]) && isSet ($this->actions[$action]['disableGui']) && $this->actions[$action]['disableGui']);
# Show the header, and echo out the config in the HTML comments
if (!$disableGui) {
echo $this->headerHtml;
// echo $configHtml; // View source to see this
}
# Show the title
if (!$disableGui) {
echo "\n<p class=\"loggedinas\">You are logged in as: <strong>{$this->username}</strong> [<a href=\"{$this->logoutLocation}\" class=\"logout\">log out</a>]</p>";
echo "\n" . '<p class="navigationmenu"><a href="./">Ballot home</a>' . ($this->userIsElectionOfficial ? ' | <a href="./?admin">Admin section</a>' : '') . '</p>';
echo "\n\n\n<h1>{$this->pageTitle}</h1>\n\n";
}
# Take the action
if ($action) {
# Ensure the user has rights
$actionRequiresElectionOfficial = $this->actions[$action]['administrator'];
$userAuthorised = ($this->userIsElectionOfficial || !$actionRequiresElectionOfficial);
if ($userAuthorised) {
# Reassure admins about admin-only pages
if ($this->userIsElectionOfficial && $actionRequiresElectionOfficial) {
echo "\n<p><em>Note: You can only access this page because you are an election official. It is not available to ordinary voters.</em></p>";
}
# Run the (now validated and authorised) function
$this->$action ();
} else {
echo "\n<p>Sorry, you do not have rights to access this section.</p>";
}
} else {
echo "\n<p>There is no such page. Please check the URL and try again.</p>";
#!# Ideally this would throw a 404 but that cannot be done until each action returns HTML rather than echos it directly (because then the code surrounding the current block can just build then echo $html;
// header ('HTTP/1.0 404 Not Found');
}
# Show the footer
if (!$disableGui) {
echo $this->footerHtml;
}
# Explicitly the database connection
$this->closeDatabaseConnection ();
}
# Function to convert the config to an HTML comment so a user can verify what is coming into the system
private function configHtml ($config, $sourceDescription)
{
# Blank out the password field; and fields containing machine paths
$config['dbPassword'] = '[...]';
$config['countingInstallation'] = '[...]';
$config['additionalVotesCsvDirectory'] = '[...]';
# Build the HTML
$html = "\n\n<!-- This is the config {$sourceDescription}, with entity conversion added:\n\n";
$html .= htmlspecialchars (print_r ($config, true)); // HTML specialchars is essential to avoid a --> string closing the comment prematurely
$html .= "\n-->\n\n";
# Return the HTML
return $html;
}
# Function to merge the config
private function assignConfiguration ($suppliedArguments) // Supplied arguments comes from the config stub launching file
{
# If database config is used, retrieve that additional config
$defaults = $this->defaults;
if (isSet ($suppliedArguments['dbConfigTable']) && $suppliedArguments['dbConfigTable'] && is_string ($suppliedArguments['dbConfigTable'])) {
# Merge the core defaults (id and database connectivity), so that there is enough to connect to the database and select the config
if (!$this->config = $this->mergeConfiguration ($defaults, $suppliedArguments)) {
return false;
}
# Connect to the database at the setup user privilege level
if (!$this->openDatabaseConnection ($this->config['dbSetupUsername'])) {
$this->errors[] = "... therefore the setup database connection could not be established.";
return false;
}
# Obtain the current fields; no error handling needed as we know that the table exists; the quoting is used just in case the admin has specified a stupid table name in the config, though this is not a security issue
# Note there is no problem if this table has additional fields - these will be ignored in the mergeConfiguration() routine and will never get past that into the rest of the system
$query = "SELECT * FROM `{$this->config['dbDatabase']}`.`{$this->config['dbConfigTable']}` WHERE id = :id LIMIT 1;";
if (!$data = $this->databaseConnection->getData ($query, array ('id' => $this->config['id']))) {
# If there is no staging database specified, throw an error
if (!$this->config['dbDatabaseStaging']) {
$this->errors[] = "A database-stored configuration in the '<strong>" . htmlspecialchars ("{$this->config['dbDatabase']}.{$this->config['dbConfigTable']}") . "</strong>' table for an election with id '<strong>" . htmlspecialchars ($this->config['id']) . "</strong>' was specified but it could not be retrieved.";
return false;
} else {
# Now try to fallback to the staging database, ensuring that it is for a ballot that has not yet opened (which therefore prevents the use of the staging database for live votes)
$query = "SELECT * FROM `{$this->config['dbDatabaseStaging']}`.`{$this->config['dbConfigTable']}` WHERE id = :id AND NOW() < ballotStart LIMIT 1;";
if (!$data = $this->databaseConnection->getData ($query, array ('id' => $this->config['id']))) {
$this->errors[] = "A database-stored configuration in the '<strong>" . htmlspecialchars ("{$this->config['dbDatabaseStaging']}.{$this->config['dbConfigTable']}") . "</strong>' staging table for an election with id '<strong>" . htmlspecialchars ($this->config['id']) . "</strong>' was specified but it could not be retrieved.";
return false;
}
# Set the staging database as the database to be used by the runtime
$suppliedArguments['dbDatabase'] = $this->config['dbDatabaseStaging'];
# Force reconnection later with the new database name
$this->closeDatabaseConnection ();
}
}
# Ensure there is only one config; if not, the instances table doesn't have a unique key for id
if (count ($data) != 1) {
$this->errors[] = "More than one database-stored configuration in the '<strong>" . htmlspecialchars ("{$this->config['dbDatabase']}.{$this->config['dbConfigTable']}") . "</strong>' table was retrieved for a configuration with id '<strong>" . htmlspecialchars ($this->config['id']) . "</strong>'. Please ensure the id field has the UNIQUE KEY specifier.";
return false;
}
# Merge the data into the supplied arguments, with any specified in the physical config file taking precedence for security
$suppliedArguments = array_merge ($data[0], $suppliedArguments);
}
# Merge in the non-core ('databaseable') defaults; file (non-databaseable) defaults take priority
$defaults += $this->defaultsDatabaseable;
# Merge the full configuration (or end if failure) and make the full config available as a class variable
if (!$this->config = $this->mergeConfiguration ($defaults, $suppliedArguments)) {
return false;
}
# Unset specification of the staging database as it is no longer required in the configuration
unset ($this->config['dbDatabaseStaging']);
# Signal success
return true;
}
# Function used by assignConfiguration to merge defaults with supplied config
private function mergeConfiguration ($defaults, $suppliedArguments)
{
# Start a list of errors (so that all setup errors are shown at once)
$errors = array ();
# Merge the defaults
$arguments = array ();
foreach ($defaults as $argument => $defaultValue) {
# Sanity check: fields marked NULL or array() in the defaults MUST be supplied in the config and must not be an empty string
if ((is_null ($defaultValue) || $defaultValue === array ()) && (!isSet ($suppliedArguments[$argument]) || !strlen ($suppliedArguments[$argument]))) {
$errors[] = "No '<strong>{$argument}</strong>' has been set in the configuration.";
# Having passed the check, reverting to the default value if no value is specified in the supplied config
} else {
$arguments[$argument] = (isSet ($suppliedArguments[$argument]) ? $suppliedArguments[$argument] : $defaultValue);
}
}
# Assign and return the errors if there are any
if ($errors) {
$this->errors += $errors;
return false;
}
# Return the arguments
return $arguments;
}
# Function to process parts of the config
private function processConfiguration ()
{
# Force the unique ID is lowercased (to ensure compatibility with MySQL tables when running on Windows)
if (!preg_match ('/^([-a-z0-9]+)$/D', $this->config['id'])) {
$this->errors[] = "The '<strong>id</strong>' setting in the configuration contains characters other than lower-case a-z, numbers or hyphens.";
return false;
}
# Convert the string containing the list of places/position-name/candidates into an array of places and an array of (position-name, candidate1, candidate2, etc.)
$string = trim ($this->config['electionInfo']); // Trim whitespace at edges
$this->config['electionInfo'] = array ();
$this->positionInfo = array ();
$string = str_replace ("\r\n", "\n", $string); // Standardise to Unix newlines
if (substr_count ($string, "\n\n\n")) {
$this->errors[] = "The '<strong>ename</strong>' setting in the configuration contains triple-line breaks. There must only be a single extra line between each election.";
return false;
}
$elections = explode ("\n\n", $string); // Split into each election, by finding the double line-breaks (if any - if not, there is only a single election)
foreach ($elections as $index => $election) {
$election = trim ($election); // Just in case but should never arise
$this->config['electionInfo'][$index] = explode ("\n", $election);
# Extract the first item in the block, and make that the number of positions available
$this->positionInfo[] = array_shift ($this->config['electionInfo'][$index]);
}
# Convert the times to unixtime
$timeSettings = array ('ballotStart', 'ballotEnd', 'paperVotingEnd', 'ballotViewableDelayed');
foreach ($timeSettings as $timeSetting) {
# The paperVotingEnd setting, is not required; only perform the check and conversion to UNIX time below if not false/empty
if (($timeSetting == 'paperVotingEnd') || ($timeSetting == 'ballotViewableDelayed')) {
if ($this->config[$timeSetting] == '0000-00-00 00:00:00') {$this->config[$timeSetting] = false;} // Deal with 32/64 bit compatibility; see: http://stackoverflow.com/questions/141315
if (!$this->config[$timeSetting]) {
$this->config[$timeSetting] = false; // Explicitly cast false/NULL/0/''/'0'/etc. (see http://php.net/types.comparisons ) as false, to avoid persistence as some other equivalent of false
continue;
}
}
# Check formatting as SQLtime string, e.g. '2013-11-06 17:00:00'
if (!preg_match ('/^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})$/D', $this->config[$timeSetting], $matches)) {
$this->errors[] = "The '<strong>{$timeSetting}</strong>' setting in the configuration is not formatted correctly; it should be similar to this: " . date ('Y') . '-01-01 00:00:00'; // date('Y') just used to make pretty documentation but Jan 1st so it's an obviously "example" date
return false;
}
# Convert to UNIX timestamp
list ($wholeString, $year, $month, $day, $hour, $minute, $second) = $matches;
$this->config[$timeSetting] = mktime ($hour, $minute, $second, $month, $day, $year);
}
# Validate that the ballot ends after it opens (and is not the same)
if ($this->config['ballotEnd'] <= $this->config['ballotStart']) {
$this->errors[] = "The time settings for this ballot in the configuration are wrong; the end time must be after the start time.";
return false;
}
# Validate that any paper voting time is after the ballot start point
if ($this->config['paperVotingEnd']) {
if ($this->config['paperVotingEnd'] <= $this->config['ballotStart']) {
$this->errors[] = "The time settings for this ballot in the configuration are wrong; the close of paper voting time must be after the start time.";
return false;
}
}
# Determine the ballotViewable time (is the later of online and paper voting)
#!# Rename this variable to ballotsViewable for clarity perhaps
$this->ballotViewable = max ($this->config['ballotEnd'], $this->config['paperVotingEnd']);
# Set the ballotViewableDelayed to be the same as ballotViewable if not specified
$this->ballotViewableDelayed = ($this->config['ballotViewableDelayed'] ? $this->config['ballotViewableDelayed'] : $this->ballotViewable);
# Validate that any delayed ballot viewable time is not before the ballot viewable time