jenkins-bot has submitted this change and it was merged.

Change subject: Add editing and logging and other general work
......................................................................


Add editing and logging and other general work

* (bug 71694) Allow for editing existing poll configuration.
* (bug 71695) Add a SecurePoll namespace (hidden behind a config
  variable) where poll configurations are dumped for logging purposes.
  This namespace uses a custom content handler to prevent direct
  editing.
* (bug 71693) Restore "none" encryption option, so entering a GPG
  encryption key is no longer required.
* Allow for static configuration of gpg-sign-key.
* Actually validate input GPG keys.
* CLI utility to delete a poll.

Bug: 71693
Bug: 71694
Bug: 71695
Change-Id: Ic975dc9a2fba2bf0cc9c4fb8ad4ed906458f76b4
---
A SecurePoll.namespaces.php
M SecurePoll.php
A cli/delete.php
M i18n/en.json
M i18n/qqq.json
M includes/crypt/Crypt.php
M includes/main/Base.php
A includes/main/SecurePollContent.php
A includes/main/SecurePollContentHandler.php
M includes/pages/CreatePage.php
M includes/pages/EntryPage.php
M includes/pages/TranslatePage.php
M includes/pages/VoterEligibilityPage.php
13 files changed, 1,197 insertions(+), 297 deletions(-)

Approvals:
  Tim Starling: Looks good to me, approved
  Anomie: Looks good to me, but someone else must approve
  jenkins-bot: Verified



diff --git a/SecurePoll.namespaces.php b/SecurePoll.namespaces.php
new file mode 100644
index 0000000..1131f4d
--- /dev/null
+++ b/SecurePoll.namespaces.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * Internationalisation file for extension SecurePoll.
+ *
+ * @file
+ * @ingroup Extensions
+ */
+
+$namespaceNames = array();
+
+$namespaceNames['en'] = array(
+       830 => 'SecurePoll',
+       831 => 'SecurePoll_talk',
+);
diff --git a/SecurePoll.php b/SecurePoll.php
index 8009888..d4058fe 100644
--- a/SecurePoll.php
+++ b/SecurePoll.php
@@ -64,6 +64,17 @@
  */
 $wgSecurePollCreateRemoteScriptPath = 'https:$wgServer/w';
 
+/**
+ * Whether to register and log to the SecurePoll namespace
+ */
+$wgSecurePollUseNamespace = false;
+
+/**
+ * If set, SecurePoll_GpgCrypt will use this instead of prompting the user for
+ * a signing key.
+ */
+$wgSecurePollGpgSignKey = null;
+
 ### END CONFIGURATON ###
 
 
@@ -72,6 +83,7 @@
 $wgMessagesDirs['SecurePoll'] = __DIR__ . '/i18n';
 $wgExtensionMessagesFiles['SecurePoll'] = "$dir/SecurePoll.i18n.php";
 $wgExtensionMessagesFiles['SecurePollAlias'] = "$dir/SecurePoll.alias.php";
+$wgExtensionMessagesFiles['SecurePollNamespaces'] = $dir . 
'/SecurePoll.namespaces.php';
 
 $wgSpecialPages['SecurePoll'] = 'SecurePoll_BasePage';
 
@@ -136,6 +148,10 @@
        # Jobs
        'SecurePoll_PopulateVoterListJob' => 
"$dir/includes/jobs/PopulateVoterListJob.php",
 
+       # ContentHandler
+       'SecurePollContentHandler' => 
$dir.'/includes/main/SecurePollContentHandler.php',
+       'SecurePollContent' => $dir.'/includes/main/SecurePollContent.php',
+
        # HTMLForm additions
        'SecurePoll_HTMLDateField' => 
"$dir/includes/htmlform/HTMLDateField.php",
        'SecurePoll_HTMLDateRangeField' => 
"$dir/includes/htmlform/HTMLDateRangeField.php",
@@ -157,6 +173,8 @@
 $wgHooks['UserLogout'][] = 'wfSecurePollLogout';
 
 $wgJobClasses['securePollPopulateVoterList'] = 
'SecurePoll_PopulateVoterListJob';
+
+$wgContentHandlers['SecurePoll'] = 'SecurePollContentHandler';
 
 $wgAvailableRights[] = 'securepoll-create-poll';
 
@@ -191,3 +209,37 @@
        }
        return true;
 }
+
+define( 'NS_SECUREPOLL', 830 );
+define( 'NS_SECUREPOLL_TALK', 831 );
+$wgNamespacesWithSubpages[NS_SECUREPOLL] = true;
+$wgNamespacesWithSubpages[NS_SECUREPOLL_TALK] = true;
+
+$wgHooks['CanonicalNamespaces'][] = function ( &$namespaces ) {
+       global $wgSecurePollUseNamespace;
+       if ( $wgSecurePollUseNamespace ) {
+               $namespaces[NS_SECUREPOLL] = 'SecurePoll';
+               $namespaces[NS_SECUREPOLL_TALK] = 'SecurePoll_talk';
+       }
+};
+
+$wgHooks['TitleQuickPermissions'][] = function ( $title, $user, $action, 
&$errors, $doExpensiveQueries, $short ) {
+       global $wgSecurePollUseNamespace;
+       if ( $wgSecurePollUseNamespace && $title->getNamespace() === 
NS_SECUREPOLL &&
+               $action !== 'read'
+       ) {
+               $errors[] = array( 'securepoll-ns-readonly' );
+               return false;
+       }
+
+       return true;
+};
+
+$wgHooks['ContentHandlerDefaultModelFor'][] = function ( $title, &$model ) {
+       global $wgSecurePollUseNamespace;
+       if( $wgSecurePollUseNamespace && $title->getNamespace() == 
NS_SECUREPOLL ) {
+               $model = 'SecurePoll';
+               return false;
+       }
+       return true;
+};
diff --git a/cli/delete.php b/cli/delete.php
new file mode 100644
index 0000000..d1783fc
--- /dev/null
+++ b/cli/delete.php
@@ -0,0 +1,77 @@
+<?php
+
+require( dirname( __FILE__ ) . '/cli.inc' );
+
+$usage = <<<EOT
+Delete a poll from the local SecurePoll database.
+
+Usage: delete.php <id>
+
+EOT;
+
+if ( !isset( $args[0] ) ) {
+       echo $usage;
+       exit( 1 );
+}
+$id = intval( $args[0] );
+if ( $args[0] !== (string)$id ) {
+       echo "The specified id \"{$args[0]}\" is not an integer\n";
+       exit( 1 );
+}
+
+$success = spDeleteElection( $id );
+exit( $success ? 0 : 1 );
+
+/**
+ * @param $electionId int|string
+ */
+function spDeleteElection( $electionId ) {
+       $dbw = wfGetDB( DB_MASTER );
+
+       $type = $dbw->selectField( 'securepoll_entity', 'en_type',
+               array( 'en_id' => $electionId ),
+               __METHOD__, array( 'FOR UPDATE' ) );
+       if ( !$type ) {
+               echo "The specified id does not exist.\n";
+               return false;
+       }
+       if ( $type !== 'election' ) {
+               echo "The specified id is for an entity of type \"$type\", not 
\"election\".\n";
+               return false;
+       }
+
+       # Get a list of entity IDs and lock them
+       $questionIds = array();
+       $res = $dbw->select( 'securepoll_questions', array( 'qu_entity' ),
+               array( 'qu_election' => $electionId ),
+               __METHOD__, array( 'FOR UPDATE' ) );
+       foreach ( $res as $row ) {
+               $questionIds[] = $row->qu_entity;
+       }
+
+       $res = $dbw->select( 'securepoll_options', array( 'op_entity' ),
+               array( 'op_election' => $electionId ),
+               __METHOD__, array( 'FOR UPDATE' ) );
+       $optionIds = array();
+       foreach ( $res as $row ) {
+               $optionIds[] = $row->op_entity;
+       }
+
+       $entityIds = array_merge( $optionIds, $questionIds, array( $electionId 
) );
+
+       # Delete the messages and properties
+       $dbw->delete( 'securepoll_msgs', array( 'msg_entity' => $entityIds ) );
+       $dbw->delete( 'securepoll_properties', array( 'pr_entity' => $entityIds 
) );
+
+       # Delete the entities
+       if ( $optionIds ) {
+               $dbw->delete( 'securepoll_options', array( 'op_entity' => 
$optionIds ), __METHOD__ );
+       }
+       if ( $questionIds ) {
+               $dbw->delete( 'securepoll_questions', array( 'qu_entity' => 
$questionIds ), __METHOD__ );
+       }
+       $dbw->delete( 'securepoll_elections', array( 'el_entity' => $electionId 
), __METHOD__ );
+       $dbw->delete( 'securepoll_entity', array( 'en_id' => $entityIds ), 
__METHOD__ );
+
+       return true;
+}
diff --git a/i18n/en.json b/i18n/en.json
index c170fe7..43038f6 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -12,6 +12,7 @@
        "securepoll-invalid-election": "\"$1\" is not a valid election ID.",
        "securepoll-welcome": "<strong>Welcome $1!</strong>",
        "securepoll-not-started": "This election has not yet started.\nIt is 
scheduled to start on $2 at $3.",
+       "securepoll-already-started": "This election has already started.\nIt 
started on $2 at $3",
        "securepoll-finished": "This election has finished, you can no longer 
vote.",
        "securepoll-not-qualified": "You are not qualified to vote in this 
election: $1",
        "securepoll-change-disallowed": "You have voted in this election 
before.\nSorry, you may not vote again.",
@@ -86,6 +87,7 @@
        "securepoll-dump-no-urandom": "Cannot open /dev/urandom. \nTo maintain 
voter privacy, encrypted election records are only publically available when 
they can be shuffled with a secure random number stream.",
        "securepoll-urandom-not-supported": "This server does not support 
cryptographic random number generation.\nTo maintain voter privacy, encrypted 
election records are only publically available when they can be shuffled with a 
secure random number stream.",
        "securepoll-translate-title": "Translate: $1",
+       "securepoll-translate-label-comment": "Reason: ",
        "securepoll-invalid-language": "Invalid language code \"$1\"",
        "securepoll-header-trans-id": "ID",
        "securepoll-submit-translate": "Update",
@@ -99,6 +101,7 @@
        "securepoll-subpage-vote": "Vote",
        "securepoll-subpage-translate": "Translate",
        "securepoll-subpage-list": "List",
+       "securepoll-subpage-edit": "Edit",
        "securepoll-subpage-votereligibility": "Voter Eligibility",
        "securepoll-subpage-dump": "Dump",
        "securepoll-subpage-tally": "Tally",
@@ -183,10 +186,17 @@
        
"securepoll-create-option-election_type-radio-range-comment+histogram-range": 
"Range voting (histogram range) with comment",
        "securepoll-create-option-election_crypt-none": "No encryption",
        "securepoll-create-option-election_crypt-gpg": "GPG",
+       "securepoll-create-label-comment": "Reason",
        "securepoll-create-invalid-username": "The specified user name is not 
valid",
        "securepoll-create-user-does-not-exist": "The specified user does not 
exist",
+       "securepoll-create-fail-bad-id": "The submitted election ID does not 
match the election being edited",
+       "securepoll-create-fail-id-missing": "The election being edited no 
longer exists",
        "securepoll-create-fail-bad-dblist": "The specified wiki or wiki list 
is not valid",
        "securepoll-create-duplicate-title": "The given poll title has already 
been used on $1",
+       "securepoll-edit-title": "{{int:securepoll}}: Edit poll",
+       "securepoll-edit-action": "Edit poll",
+       "securepoll-edit-edited": "{{int:securepoll}}: Poll edited",
+       "securepoll-edit-edited-text": "Your poll has been edited.",
        "securepoll-votereligibility-title": "Voter eligibility configuration",
        "securepoll-votereligibility-redirect": "Voter eligibility for this 
election must be configured on $1",
        "securepoll-votereligibility-redirect-otherwiki": "the main wiki",
@@ -221,6 +231,7 @@
        "securepoll-votereligibility-label-exclude_groups": "Exclude users in 
these groups",
        "securepoll-votereligibility-label-include_groups": "Include users in 
these groups, regardless of edits or other groups",
        "securepoll-votereligibility-label-names": "User names",
+       "securepoll-votereligibility-label-comment": "Reason",
        "securepoll-votereligibility-action": "Save configuration",
        "securepoll-votereligibility-fail-nothing-to-process": "Automatic 
population of the {{int:securepoll-votereligibility-list-voter}} was requested, 
but all filters were left blank.",
        "securepoll-votereligibility-edit-title": "Voter list editing: $1",
@@ -231,6 +242,8 @@
        "securepoll-votereligibility-clear-title": "Voter list removal: $1",
        "securepoll-votereligibility-cleared": "{{int:securepoll}}: Cleared",
        "securepoll-votereligibility-cleared-text": "The $1 has been cleared 
and will not be used by the poll.",
+       "securepoll-votereligibility-cleared-comment": "$1 cleared.",
        "right-securepoll-create-poll": "Create polls",
-       "action-securepoll-create-poll": "create polls"
+       "action-securepoll-create-poll": "create polls",
+       "securepoll-ns-readonly": "The SecurePoll namespace is read-only"
 }
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 2182da2..869c226 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -26,6 +26,7 @@
        "securepoll-invalid-election": "Used as error message. Parameters:\n* 
$1 - invalid election ID",
        "securepoll-welcome": "Used as welcome message for remote voters. 
Parameters:\n* $1 - username of remote voter\n{{Identical|Welcome}}",
        "securepoll-not-started": "Parameters:\n* $1 - (Unused)\n* $2 - the 
date of it\n* $3 - its time",
+       "securepoll-already-started": "Parameters:\n* $1 - (Unused)\n* $2 - the 
date of it\n* $3 - its time",
        "securepoll-not-qualified": "Unused at this time. Paramters:\n* $1 - 
...",
        "securepoll-submit": "{{Identical|Submit}}",
        "securepoll-gpg-receipt": "Parameters:\n* $1 - the receipt. Format: 
\"SPID: (10-digit vote ID)\\n(encrypted vote record)\"\nIf the election doesn't 
use encryption, the following message is used:\n* {{msg-mw|securepoll-thanks}}",
@@ -78,6 +79,7 @@
        "securepoll-dump-no-urandom": "Do not translate 
\"/dev/urandom\".\n\nServers running Microsoft Windows will present 
[[MediaWiki:Securepoll-urandom-not-supported/en|Securepoll-urandom-not-supported]]
 instead.",
        "securepoll-urandom-not-supported": "As to the meaning of 
''cryptographic random number'', see [[:wikipedia:Cryptographically secure 
pseudorandom number generator]] for reference.\n\nThe /dev/urandom 
cryptographic random number generation device is not supported on servers 
running Microsoft Windows. On other platforms the 
[[MediaWiki:Securepoll-dump-no-urandom/en|Securepoll-dump-no-urandom]] message 
is generated if opening of the /dev/urandom device fails.",
        "securepoll-translate-title": "Used as page title. Parameters:\n* $1 - 
title of election\n{{Identical|Translate}}",
+       "securepoll-translate-label-comment": "Label for the \"edit summary\" 
field on Special:SecurePoll/translate.",
        "securepoll-invalid-language": "Parameters:\n* $1 - language code",
        "securepoll-header-trans-id": "{{optional}}\n{{Identical|ID}}",
        "securepoll-submit-translate": "{{Identical|Update}}",
@@ -90,6 +92,7 @@
        "securepoll-subpage-vote": "Link text to a sub page in the SecurePoll 
extension where users can vote.\n{{Identical|Vote}}",
        "securepoll-subpage-translate": "Link text to a sub page in the 
SecurePoll extension where users can translate poll related 
texts.\n{{Identical|Translate}}",
        "securepoll-subpage-list": "Link text to a sub page in the SecurePoll 
extension where users can list poll information.\n{{Identical|List}}",
+       "securepoll-subpage-edit": "Link text to a sub page in the SecurePoll 
extension where users can edit a poll configuration.",
        "securepoll-subpage-votereligibility": "Link text to a sub page in the 
SecurePoll extension where users can configure voter eligibility.",
        "securepoll-subpage-dump": "Link text to a sub page in the SecurePoll 
extension where users can dump results.\n{{Identical|Dump}}",
        "securepoll-subpage-tally": "Link text to a sub page in the SecurePoll 
extension where users can tally.\n{{Identical|Tally}}",
@@ -163,7 +166,14 @@
        "securepoll-create-invalid-username": "Used as error message in 
Special:SecurePoll/create.",
        "securepoll-create-user-does-not-exist": "Used as error message in 
Special:SecurePoll/create.",
        "securepoll-create-fail-bad-dblist": "Used as error message in 
Special:SecurePoll/create.",
+       "securepoll-create-fail-bad-id": "Used as error message in 
Special:SecurePoll/create.",
+       "securepoll-create-fail-id-missing": "Used as error message in 
Special:SecurePoll/create.",
+       "securepoll-create-label-comment": "Label for the \"edit summary\" 
field on Special:SecurePoll/create.",
        "securepoll-create-duplicate-title": "Used as error message in 
Special:SecurePoll/create.",
+       "securepoll-edit-title": "Title for Special:SecurePoll/edit.",
+       "securepoll-edit-action": "Text for submit button on 
Special:SecurePoll/edit.\n{{Identical|Edit poll}}",
+       "securepoll-edit-edited": "Title for the page displayed after poll 
edit.",
+       "securepoll-edit-edited-text": "Text for the page displayed after poll 
edit.",
        "securepoll-votereligibility-title": "Title for 
Special:SecurePoll/votereligibility/#",
        "securepoll-votereligibility-redirect": "Text displayed when trying to 
edit a multi-wiki poll on the wrong wiki.\n\nParameters:\n* $1 - HTML of a link 
to the correct wiki. Displayed text is either the wiki name or 
{{msg-mw|securepoll-votereligibility-redirect-otherwiki}}",
        "securepoll-votereligibility-redirect-otherwiki": "Used with 
{{msg-mw|securepoll-votereligibility-redirect}} when no name is available for 
the target wiki.",
@@ -198,6 +208,7 @@
        "securepoll-votereligibility-label-exclude_groups": "Label for form 
field on Special:SecurePoll/votereligibility.",
        "securepoll-votereligibility-label-include_groups": "Label for form 
field on Special:SecurePoll/votereligibility.",
        "securepoll-votereligibility-label-names": "Label for form field on 
Special:SecurePoll/votereligibility/#/edit/X.\n{{Identical|Username}}",
+       "securepoll-votereligibility-label-comment": "Label for the \"edit 
summary\" field on Special:SecurePoll/votereligibility.",
        "securepoll-votereligibility-action": "Label for form submit button on 
Special:SecurePoll/votereligibility.",
        "securepoll-votereligibility-fail-nothing-to-process": "Used as an 
error message on Special:SecurePoll/votereligibility. Uses 
{{msg-mw|securepoll-votereligibility-list-voter}}.",
        "securepoll-votereligibility-edit-title": "Title for 
Special:SecurePoll/votereligibility/#/edit/X",
@@ -208,6 +219,8 @@
        "securepoll-votereligibility-clear-title": "Title for 
Special:SecurePoll/votereligibility/#/clear/X",
        "securepoll-votereligibility-cleared": "Title for clear confirmation 
page for Special:SecurePoll/votereligibility.",
        "securepoll-votereligibility-cleared-text": "Text for clear 
confirmation page for Special:SecurePoll/votereligibility. $1 is a list name.",
+       "securepoll-votereligibility-cleared-comment": "Text used as the edit 
summary when clearing a list on Special:SecurePoll/votereligibility. $1 is a 
list name.",
        "right-securepoll-create-poll": "{{doc-right|securepoll-create-poll}}",
-       "action-securepoll-create-poll": "{{Doc-action|securepoll-create-poll}}"
+       "action-securepoll-create-poll": 
"{{Doc-action|securepoll-create-poll}}",
+       "securepoll-ns-readonly": "Error message to inform the user that the 
SecurePoll namespace cannot be edited."
 }
diff --git a/includes/crypt/Crypt.php b/includes/crypt/Crypt.php
index cad201e..4c6d5b1 100644
--- a/includes/crypt/Crypt.php
+++ b/includes/crypt/Crypt.php
@@ -26,7 +26,7 @@
        abstract function canDecrypt();
 
        static $cryptTypes = array(
-               //'none' => false,
+               'none' => false,
                'gpg' => 'SecurePoll_GpgCrypt',
        );
 
@@ -79,6 +79,8 @@
        public $recipient, $signer, $homeDir;
 
        static function getCreateDescriptors() {
+               global $wgSecurePollGpgSignKey;
+
                $ret = SecurePoll_Crypt::getCreateDescriptors();
                $ret['election'] += array(
                        'gpg-encrypt-key' => array(
@@ -87,15 +89,55 @@
                                'required' => true,
                                'SecurePoll_type' => 'property',
                                'rows' => 5,
-                       ),
-                       'gpg-sign-key' => array(
-                               'label-message' => 
'securepoll-create-label-gpg_sign_key',
-                               'type' => 'textarea',
-                               'SecurePoll_type' => 'property',
-                               'rows' => 5,
+                               'validation-callback' => 
'SecurePoll_GpgCrypt::checkEncryptKey',
                        ),
                );
+
+               if ( $wgSecurePollGpgSignKey ) {
+                       $ret['election'] += array(
+                               'gpg-sign-key' => array(
+                                       'type' => 'api',
+                                       'default' => $wgSecurePollGpgSignKey,
+                                       'SecurePoll_type' => 'property',
+                               ),
+                       );
+               } else {
+                       $ret['election'] += array(
+                               'gpg-sign-key' => array(
+                                       'label-message' => 
'securepoll-create-label-gpg_sign_key',
+                                       'type' => 'textarea',
+                                       'SecurePoll_type' => 'property',
+                                       'rows' => 5,
+                                       'validation-callback' => 
'SecurePoll_GpgCrypt::checkSignKey',
+                               ),
+                       );
+               }
+
                return $ret;
+       }
+
+       public static function checkEncryptKey( $key ) {
+               $that = new SecurePoll_GpgCrypt( null, null );
+               $status = $that->setupHome();
+               if ( $status->isOK() ) {
+                       $status = $that->importKey( $key );
+               }
+               $that->deleteHome();
+               return $status->isOK() ? true : $status->getMessage();
+       }
+
+       public static function checkSignKey( $key ) {
+               if ( !strval( $key ) ) {
+                       return true;
+               }
+
+               $that = new SecurePoll_GpgCrypt( null, null );
+               $status = $that->setupHome();
+               if ( $status->isOK() ) {
+                       $status = $that->importKey( $key );
+               }
+               $that->deleteHome();
+               return $status->isOK() ? true : $status->getMessage();
        }
 
        /**
@@ -109,7 +151,8 @@
        }
 
        /**
-        * Create a new GPG home directory and import the encryption keys into 
it.
+        * Create a new GPG home directory
+        * @return Status
         */
        function setupHome() {
                global $wgSecurePollTempDir;
@@ -125,6 +168,24 @@
                        return Status::newFatal( 'securepoll-no-gpg-home' );
                }
                chmod( $this->homeDir, 0700 );
+
+               return Status::newGood();
+       }
+
+       /**
+        * Create a new GPG home directory and import keys
+        * @return Status
+        */
+       function setupHomeAndKeys() {
+               $status = $this->setupHome();
+               if ( !$status->isOK() ) {
+                       return $status;
+               }
+
+               if ( $this->recipient ) {
+                       # Already done
+                       return Status::newGood();
+               }
 
                # Fetch the keys
                $encryptKey = strval( $this->election->getProperty( 
'gpg-encrypt-key' ) );
@@ -225,7 +286,7 @@
         * @return Status
         */
        function encrypt( $record ) {
-               $status = $this->setupHome();
+               $status = $this->setupHomeAndKeys();
                if ( !$status->isOK() ) {
                        $this->deleteHome();
                        return $status;
@@ -263,7 +324,7 @@
         * @return Status
         */
        function decrypt( $encrypted ) {
-               $status = $this->setupHome();
+               $status = $this->setupHomeAndKeys();
                if ( !$status->isOK() ) {
                        $this->deleteHome();
                        return $status;
diff --git a/includes/main/Base.php b/includes/main/Base.php
index 6394833..10b0458 100644
--- a/includes/main/Base.php
+++ b/includes/main/Base.php
@@ -8,6 +8,7 @@
 class SecurePoll_BasePage extends UnlistedSpecialPage {
        static $pages = array(
                'create' => 'SecurePoll_CreatePage',
+               'edit' => 'SecurePoll_CreatePage',
                'details' => 'SecurePoll_DetailsPage',
                'dump' => 'SecurePoll_DumpPage',
                'entry' => 'SecurePoll_EntryPage',
diff --git a/includes/main/SecurePollContent.php 
b/includes/main/SecurePollContent.php
new file mode 100644
index 0000000..c0c7445
--- /dev/null
+++ b/includes/main/SecurePollContent.php
@@ -0,0 +1,13 @@
+<?php
+/**
+ * SecurePoll Content Model
+ *
+ * @file
+ * @ingroup Extensions
+ * @ingroup SecurePoll
+ *
+ * @author Brad Jorsch <bjor...@wikimedia.org>
+ */
+
+class SecurePollContent extends JsonContent {
+}
diff --git a/includes/main/SecurePollContentHandler.php 
b/includes/main/SecurePollContentHandler.php
new file mode 100644
index 0000000..12a6b15
--- /dev/null
+++ b/includes/main/SecurePollContentHandler.php
@@ -0,0 +1,179 @@
+<?php
+/**
+ * SecurePoll Content Handler
+ *
+ * @file
+ * @ingroup Extensions
+ * @ingroup SecurePoll
+ *
+ * @author Brad Jorsch <bjor...@wikimedia.org>
+ */
+
+class SecurePollContentHandler extends JsonContentHandler {
+       public function __construct( $modelId = 'SecurePoll' ) {
+               parent::__construct( $modelId );
+       }
+
+       /**
+        * Load data from an election as a PHP array structure
+        *
+        * @param SecurePoll_Election $election
+        * @param string $subpage Subpage to get content for
+        * @param bool $useBlacklist
+        * @return array
+        */
+       public static function getDataFromElection(
+               SecurePoll_Election $election, $subpage = '', $useBlacklist = 
false
+       ) {
+               if ( $subpage === '' ) {
+                       $properties = $election->getAllProperties();
+                       if ( $useBlacklist ) {
+                               $blacklist = array_flip( 
$election->getPropertyDumpBlacklist() ) + array(
+                                       'gpg-encrypt-key' => true,
+                                       'gpg-sign-key' => true,
+                                       'gpg-decrypt-key' => true,
+                               );
+                               foreach ( $properties as $k => $v ) {
+                                       if ( isset( $blacklist[$k] ) ) {
+                                               $properties[$k] = '<redacted>';
+                                       }
+                               }
+                               unset(
+                                       $properties['list_job-key'],
+                                       $properties['list_total-count'],
+                                       $properties['list_complete-count']
+                               );
+                       }
+                       $data = array(
+                               'id' => $election->getId(),
+                               'title' => $election->title,
+                               'ballot' => $election->ballotType,
+                               'tally' => $election->tallyType,
+                               'lang' => $election->getLanguage(),
+                               'startDate' => wfTimestamp( TS_ISO_8601, 
$election->getStartDate() ),
+                               'endDate' => wfTimestamp( TS_ISO_8601, 
$election->getEndDate() ),
+                               'authType' => $election->authType,
+                               'properties' => $properties,
+                               'questions' => array(),
+                       );
+
+                       foreach ( $election->getQuestions() as $question ) {
+                               $properties = $question->getAllProperties();
+                               if ( $useBlacklist ) {
+                                       $blacklist = array_flip( 
$question->getPropertyDumpBlacklist() );
+                                       foreach ( $properties as $k => $v ) {
+                                               if ( isset( $blacklist[$k] ) ) {
+                                                       $properties[$k] = 
'<redacted>';
+                                               }
+                                       }
+                               }
+                               $q = array(
+                                       'id' => $question->getId(),
+                                       'properties' => $properties,
+                                       'options' => array(),
+                               );
+
+                               foreach ( $question->getOptions() as $option ) {
+                                       $properties = 
$option->getAllProperties();
+                                       if ( $useBlacklist ) {
+                                               $blacklist = array_flip( 
$option->getPropertyDumpBlacklist() );
+                                               foreach ( $properties as $k => 
$v ) {
+                                                       if ( isset( 
$blacklist[$k] ) ) {
+                                                               $properties[$k] 
= '<redacted>';
+                                                       }
+                                               }
+                                       }
+                                       $o = array(
+                                               'id' => $option->getId(),
+                                               'properties' => $properties,
+                                       );
+                                       $q['options'][] = $o;
+                               }
+
+                               $data['questions'][] = $q;
+                       }
+               } elseif ( preg_match( '#^msg/(\S+)$#', $subpage, $m ) ) {
+                       $lang = $m[1];
+                       $data = array(
+                               'id' => $election->getId(),
+                               'lang' => $lang,
+                               'messages' => array(),
+                               'questions' => array(),
+                       );
+                       foreach ( $election->getMessageNames() as $name ) {
+                               $value = $election->getRawMessage( $name, $lang 
);
+                               if ( $value !== false ) {
+                                       $data['messages'][$name] = $value;
+                               }
+                       }
+
+                       foreach ( $election->getQuestions() as $question ) {
+                               $q = array(
+                                       'id' => $question->getId(),
+                                       'messages' => array(),
+                                       'options' => array(),
+                               );
+                               foreach ( $question->getMessageNames() as $name 
) {
+                                       $value = $question->getRawMessage( 
$name, $lang );
+                                       if ( $value !== false ) {
+                                               $q['messages'][$name] = $value;
+                                       }
+                               }
+
+                               foreach ( $question->getOptions() as $option ) {
+                                       $o = array(
+                                               'id' => $option->getId(),
+                                               'messages' => array(),
+                                       );
+                                       foreach ( $option->getMessageNames() as 
$name ) {
+                                               $value = 
$option->getRawMessage( $name, $lang );
+                                               if ( $value !== false ) {
+                                                       $o['messages'][$name] = 
$value;
+                                               }
+                                       }
+                                       $q['options'][] = $o;
+                               }
+
+                               $data['questions'][] = $q;
+                       }
+               } else {
+                       throw new MWException( __METHOD__ . ': Unsupported 
subpage format' );
+               }
+
+               return $data;
+       }
+
+       /**
+        * Create a SecurePollContent for an election
+        *
+        * @param SecurePoll_Election $election
+        * @param string $subpage Subpage to get content for
+        * @return array ( Title, SecurePollContent )
+        */
+       public static function makeContentFromElection( SecurePoll_Election 
$election, $subpage = '' ) {
+               $json = FormatJson::encode( self::getDataFromElection( 
$election, $subpage, true ),
+                       false, FormatJson::ALL_OK );
+               $title = Title::makeTitle( NS_SECUREPOLL, $election->getId() .
+                       ( $subpage === '' ? '' : "/$subpage" ) );
+               return array( $title, ContentHandler::makeContent( $json, 
$title, 'SecurePoll' ) );
+       }
+
+       public function canBeUsedOn( Title $title ) {
+               global $wgSecurePollUseNamespace;
+               return $wgSecurePollUseNamespace && $title->getNamespace() == 
NS_SECUREPOLL;
+       }
+
+       public function getActionOverrides() {
+               // Disable write actions
+               return array(
+                       'delete' => false,
+                       'edit' => false,
+                       'info' => false,
+                       'protect' => false,
+                       'revert' => false,
+                       'rollback' => false,
+                       'submit' => false,
+                       'unprotect' => false,
+               );
+       }
+}
diff --git a/includes/pages/CreatePage.php b/includes/pages/CreatePage.php
index 29358f6..e3cba44 100644
--- a/includes/pages/CreatePage.php
+++ b/includes/pages/CreatePage.php
@@ -1,7 +1,7 @@
 <?php
 
 /**
- * Special:SecurePoll subpage for creating a poll
+ * Special:SecurePoll subpage for creating or editing a poll
  */
 class SecurePoll_CreatePage extends SecurePoll_Page {
        /**
@@ -10,9 +10,27 @@
         */
        function execute( $params ) {
                global $wgUser, $wgSecurePollCreateWikiGroupDir, 
$wgSecurePollCreateWikiGroups;
+               global $wgSecurePollUseNamespace;
 
-               if ( !$wgUser->isAllowed( 'securepoll-create-poll' ) ) {
-                       throw new PermissionsError( 'securepoll-create-poll' );
+               $out = $this->parent->getOutput();
+
+               if ( $params ) {
+                       $out->setPageTitle( $this->msg( 'securepoll-edit-title' 
) );
+                       $electionId = intval( $params[0] );
+                       $this->election = $this->context->getElection( 
$electionId );
+                       if ( !$this->election ) {
+                               $out->addWikiMsg( 
'securepoll-invalid-election', $electionId );
+                               return;
+                       }
+                       if ( !$this->election->isAdmin( 
$this->parent->getUser() ) ) {
+                               $out->addWikiMsg( 'securepoll-need-admin' );
+                               return;
+                       }
+               } else {
+                       $out->setPageTitle( $this->msg( 
'securepoll-create-title' ) );
+                       if ( !$wgUser->isAllowed( 'securepoll-create-poll' ) ) {
+                               throw new PermissionsError( 
'securepoll-create-poll' );
+                       }
                }
 
                /** @todo These should be migrated to core, once the jquery.ui
@@ -24,9 +42,6 @@
 
                $this->parent->getOutput()->addModules( 
'ext.securepoll.htmlform' );
                $this->parent->getOutput()->addModules( 'ext.securepoll' );
-
-               $out = $this->parent->getOutput();
-               $out->setPageTitle( $this->msg( 'securepoll-create-title' ) );
 
                # These are for injecting raw HTML into the HTMLForm for the
                # multi-column aspects of the designed layout.
@@ -48,13 +63,19 @@
 
                $formItems = array();
 
+               $formItems['election_id'] = array(
+                       'type' => 'hidden',
+                       'default' => -1,
+                       'output-as-default' => false,
+               );
+
                $formItems['election_title'] = array(
                        'label-message' => 
'securepoll-create-label-election_title',
                        'type' => 'text',
                        'required' => true,
                );
 
-               $wikiNames = $this->getWikiList();
+               $wikiNames = SecurePoll_FormStore::getWikiList();
                $options = array();
                $options['securepoll-create-option-wiki-this_wiki'] = 
wfWikiID();
                if ( count( $wikiNames ) > 1 ) {
@@ -182,6 +203,11 @@
                );
 
                $questionFields = array(
+                       'id' => array(
+                               'type' => 'hidden',
+                               'default' => -1,
+                               'output-as-default' => false,
+                       ),
                        'text' => array(
                                'label-message' => 
'securepoll-create-label-questions-question',
                                'type' => 'text',
@@ -194,6 +220,11 @@
                );
 
                $optionFields = array(
+                       'id' => array(
+                               'type' => 'hidden',
+                               'default' => -1,
+                               'output-as-default' => false,
+                       ),
                        'text' => array(
                                'label-message' => 
'securepoll-create-label-options-option',
                                'type' => 'text',
@@ -254,45 +285,53 @@
                        'fields' => $questionFields,
                );
 
-               $form = new HTMLForm( $formItems, $this->parent->getContext(), 
'securepoll-create' );
-               $form->setDisplayFormat( 'div' );
-               $form->setSubmitTextMsg( 'securepoll-create-action' );
-               $form->setSubmitCallback( array( $this, 'processInput' ) );
-               $result = $form->show();
+               if ( $wgSecurePollUseNamespace ) {
+                       $formItems['comment'] = array(
+                               'type' => 'text',
+                               'label-message' => 
'securepoll-create-label-comment',
+                       );
+               }
 
+               $form = new HTMLForm( $formItems, $this->parent->getContext(),
+                       $this->election ? 'securepoll-edit' : 
'securepoll-create'
+               );
+               $form->setDisplayFormat( 'div' );
+               $form->setSubmitTextMsg( $this->election ? 
'securepoll-edit-action' : 'securepoll-create-action' );
+               $form->setSubmitCallback( array( $this, 'processInput' ) );
+               $form->prepareForm();
+
+               // If this isn't the result of a POST, load the data from the 
election
+               $request = $this->parent->getRequest();
+               if ( $this->election && !( $request->wasPosted() && 
$request->getCheck( 'wpEditToken' ) ) ) {
+                       $form->mFieldData = $this->getFormDataFromElection( 
$this->election );
+               }
+
+               $result = $form->tryAuthorizedSubmit();
                if ( $result === true || ( $result instanceof Status && 
$result->isGood() ) ) {
-                       $out->setPageTitle( $this->msg( 
'securepoll-create-created' ) );
-                       $out->addWikiMsg( 'securepoll-create-created-text' );
+                       if ( $this->election ) {
+                               $out->setPageTitle( $this->msg( 
'securepoll-edit-edited' ) );
+                               $out->addWikiMsg( 'securepoll-edit-edited-text' 
);
+                       } else {
+                               $out->setPageTitle( $this->msg( 
'securepoll-create-created' ) );
+                               $out->addWikiMsg( 
'securepoll-create-created-text' );
+                       }
                        $out->returnToMain( false, SpecialPage::getTitleFor( 
'SecurePoll' ) );
+               } else {
+                       $form->displayForm( $result );
                }
        }
 
        public function processInput( $formData, $form ) {
-               global $wgSecurePollCreateWikiGroupDir, 
$wgSecurePollCreateWikiGroups,
-                       $wgSecurePollCreateRemoteScriptPath;
+               global $wgSecurePollUseNamespace;
 
-               $wikis = isset( $formData['property_wiki'] ) ? 
$formData['property_wiki'] : wfWikiID();
-               if ( $wikis === '*' ) {
-                       $wikis = array_values( $this->getWikiList() );
-               } elseif ( substr( $wikis, 0, 1 ) === '@' ) {
-                       $wikis = false;
+               $context = new SecurePoll_Context;
+               $store = new SecurePoll_FormStore( $formData );
+               $context->setStore( $store );
+               $election = $context->getElection( $store->eId );
 
-                       // HTMLForm already checked this, but let's do it again 
anyway.
-                       if ( isset( $wgSecurePollCreateWikiGroups[$wikis] ) ) {
-                               $wikis = file_get_contents(
-                                       $wgSecurePollCreateWikiGroupDir . 
substr( $wikis, 1 ) . '.dblist'
-                               );
-                       }
-
-                       if ( !$wikis ) {
-                               return Status::newFatal( 
'securepoll-create-fail-bad-dblist' );
-                       }
-                       $wikis = array_map( 'trim', explode( "\n", trim( $wikis 
) ) );
-               } else {
-                       $wikis = (array)$wikis;
+               if ( $this->election && $store->eId !== 
(int)$this->election->getId() ) {
+                       return Status::newFatal( 
'securepoll-create-fail-bad-id' );
                }
-
-               $remoteWikis = array_diff( $wikis, array( wfWikiID() ) );
 
                $dbws = array();
                $dbw = wfGetDB( DB_MASTER );
@@ -301,101 +340,81 @@
                try {
                        $properties = array();
                        $messages = array();
-                       $remoteProperties = array();
 
+                       // Check for duplicate titles
                        $id = $dbw->selectField( 'securepoll_elections', 
'el_entity', array(
-                               'el_title' => $formData['election_title']
-                       ) );
-                       if ( $id ) {
-                               foreach ( $dbws as $dbw ) {
-                                       $dbw->rollback();
+                               'el_title' => $election->title
+                       ), __METHOD__, array( 'FOR UPDATE' ) );
+                       if ( $id && (int)$id !== $election->getId() ) {
+                               throw new SecurePoll_StatusException( 
'securepoll-create-duplicate-title',
+                                       SecurePoll_FormStore::getWikiName( 
wfWikiId() ), wfWikiID()
+                               );
+                       }
+
+                       if ( $election->getId() > 0 ) {
+                               $id = $dbw->selectField( 
'securepoll_elections', 'el_entity', array(
+                                       'el_entity' => $election->getId()
+                               ), __METHOD__, array( 'FOR UPDATE' ) );
+                               if ( !$id ) {
+                                       return Status::newFatal( 
'securepoll-create-fail-id-missing' );
                                }
-                               return Status::newFatal( 
'securepoll-create-duplicate-title',
-                                       $this->getWikiName( wfWikiId() ), 
wfWikiID()
-                               );
                        }
 
-                       $lang = $formData['election_primaryLang'];
-
-                       // Create the entry for the election
-                       list( $ballot,$tally ) = explode( '+', 
$formData['election_type'] );
-                       $crypt = $formData['election_crypt'];
-
-                       $date = new DateTime(
-                               "{$formData['election_dates'][0]}T00:00:00Z",
-                               new DateTimeZone( 'GMT' )
+                       // Insert or update the election entity
+                       $fields = array(
+                               'el_title' => $election->title,
+                               'el_ballot' => $election->ballotType,
+                               'el_tally' => $election->tallyType,
+                               'el_primary_lang' => $election->getLanguage(),
+                               'el_start_date' => $dbw->timestamp( 
$election->getStartDate() ),
+                               'el_end_date' => $dbw->timestamp( 
$election->getEndDate() ),
+                               'el_auth_type' => $election->authType,
                        );
-                       $startDate = $date->format( 'YmdHis' );
-                       $date->add( new DateInterval( 
"P{$formData['election_dates'][1]}D" ) );
-                       $endDate = $date->format( 'YmdHis' );
+                       if ( $election->getId() < 0 ) {
+                               $eId = self::insertEntity( $dbw, 'election' );
+                               $qIds = array();
+                               $oIds = array();
+                               $fields['el_entity'] = $eId;
+                               $dbw->insert( 'securepoll_elections', $fields, 
__METHOD__ );
+                       } else {
+                               $eId = $election->getId();
+                               $dbw->update( 'securepoll_elections', $fields, 
array( 'el_entity' => $eId ), __METHOD__ );
 
-                       $eId = self::insertEntity( $dbw, 'election' );
-                       $dbw->insert( 'securepoll_elections',
-                               array(
-                                       'el_entity' => $eId,
-                                       'el_title' => 
$formData['election_title'],
-                                       'el_ballot' => $ballot,
-                                       'el_tally' => $tally,
-                                       'el_primary_lang' => $lang,
-                                       'el_start_date' => $dbw->timestamp( 
$startDate ),
-                                       'el_end_date' => $dbw->timestamp( 
$endDate ),
-                                       'el_auth_type' => $remoteWikis ? 
'remote-mw' : 'local',
-                               ),
-                               __METHOD__
-                       );
-
-                       // Process election-level properties and messages for 
the selected modules
-                       if ( $remoteWikis ) {
-                               $properties[] = array(
-                                       'pr_entity' => $eId,
-                                       'pr_key' => 'remote-mw-script-path',
-                                       'pr_value' => 
$wgSecurePollCreateRemoteScriptPath,
+                               // Delete any questions or options that weren't 
included in the
+                               // form submission.
+                               $qIds = array();
+                               $res = $dbw->select( 'securepoll_questions', 
'qu_entity', array( 'qu_election' => $eId ) );
+                               foreach ( $res as $row ) {
+                                       $qIds[] = $row->qu_entity;
+                               }
+                               $oIds = array();
+                               $res = $dbw->select( 'securepoll_options', 
'op_entity', array( 'op_election' => $eId ) );
+                               foreach ( $res as $row ) {
+                                       $oIds[] = $row->op_entity;
+                               }
+                               $deleteIds = array_merge(
+                                       array_diff( $qIds, $store->qIds ),
+                                       array_diff( $oIds, $store->oIds )
                                );
-                               $remoteProperties['main-wiki'] = wfWikiID();
-                               $remoteProperties['jump-url'] = 
SpecialPage::getTitleFor( 'SecurePoll' );
-                               $remoteProperties['jump-id'] = $eId;
+                               if ( $deleteIds ) {
+                                       $dbw->delete( 'securepoll_msgs', array( 
'msg_entity' => $deleteIds ), __METHOD__ );
+                                       $dbw->delete( 'securepoll_properties', 
array( 'pr_entity' => $deleteIds ), __METHOD__ );
+                                       $dbw->delete( 'securepoll_questions', 
array( 'qu_entity' => $deleteIds ), __METHOD__ );
+                                       $dbw->delete( 'securepoll_options', 
array( 'op_entity' => $deleteIds ), __METHOD__ );
+                                       $dbw->delete( 'securepoll_entity', 
array( 'en_id' => $deleteIds ), __METHOD__ );
+                               }
                        }
-                       $properties[] = array(
-                               'pr_entity' => $eId,
-                               'pr_key' => 'encrypt-type',
-                               'pr_value' => $crypt,
-                       );
-                       $properties[] = array(
-                               'pr_entity' => $eId,
-                               'pr_key' => 'wikis',
-                               'pr_value' => join( "\n", $wikis ),
-                       );
+                       self::savePropertiesAndMessages( $dbw, $eId, $election 
);
 
-                       $admins = $this->getAdminsList( 
$formData['property_admins'] );
-                       $properties[] = array(
-                               'pr_entity' => $eId,
-                               'pr_key' => 'admins',
-                               'pr_value' => $admins,
-                       );
-                       $remoteProperties['admins'] = $admins;
-
-                       $messages[] = array(
-                               'msg_entity' => $eId,
-                               'msg_lang' => $lang,
-                               'msg_key' => 'title',
-                               'msg_text' => $formData['election_title'],
-                       );
-
-                       self::processFormData( $formData, $properties, 
$messages,
-                               SecurePoll_Ballot::$ballotTypes[$ballot], 
'election', $eId, $lang
-                       );
-                       self::processFormData( $formData, $properties, 
$messages,
-                               SecurePoll_Tallier::$tallierTypes[$tally], 
'election', $eId, $lang
-                       );
-                       self::processFormData( $formData, $properties, 
$messages,
-                               SecurePoll_Crypt::$cryptTypes[$crypt], 
'election', $eId, $lang
-                       );
-
-                       // Process each question
+                       // Now do questions and options
                        $qIndex = 0;
-                       foreach ( $formData['questions'] as $question ) {
-                               $qId = self::insertEntity( $dbw, 'question' );
-                               $dbw->insert( 'securepoll_questions',
+                       foreach ( $election->getQuestions() as $question ) {
+                               $qId = $question->getId();
+                               if ( !in_array( $qId, $qIds ) ) {
+                                       $qId = self::insertEntity( $dbw, 
'question' );
+                               }
+                               $dbw->replace( 'securepoll_questions',
+                                       array( 'qu_entity' ),
                                        array(
                                                'qu_entity' => $qId,
                                                'qu_election' => $eId,
@@ -403,28 +422,15 @@
                                        ),
                                        __METHOD__
                                );
+                               self::savePropertiesAndMessages( $dbw, $qId, 
$question );
 
-                               // Process the properties and messages for this 
question
-                               $messages[] = array(
-                                       'msg_entity' => $qId,
-                                       'msg_lang' => $lang,
-                                       'msg_key' => 'text',
-                                       'msg_text' => $question['text'],
-                               );
-                               self::processFormData( $question, $properties, 
$messages,
-                                       
SecurePoll_Ballot::$ballotTypes[$ballot], 'question', $qId, $lang
-                               );
-                               self::processFormData( $question, $properties, 
$messages,
-                                       
SecurePoll_Tallier::$tallierTypes[$tally], 'question', $qId, $lang
-                               );
-                               self::processFormData( $question, $properties, 
$messages,
-                                       SecurePoll_Crypt::$cryptTypes[$crypt], 
'question', $qId, $lang
-                               );
-
-                               // Process options for this question
-                               foreach ( $question['options'] as $option ) {
-                                       $oId = self::insertEntity( $dbw, 
'option' );
-                                       $dbw->insert( 'securepoll_options',
+                               foreach ( $question->getOptions() as $option ) {
+                                       $oId = $option->getId();
+                                       if ( !in_array( $oId, $oIds ) ) {
+                                               $oId = self::insertEntity( 
$dbw, 'option' );
+                                       }
+                                       $dbw->replace( 'securepoll_options',
+                                               array( 'op_entity' ),
                                                array(
                                                        'op_entity' => $oId,
                                                        'op_election' => $eId,
@@ -432,70 +438,65 @@
                                                ),
                                                __METHOD__
                                        );
-
-                                       // Process the properties and messages 
for this option
-                                       $messages[] = array(
-                                               'msg_entity' => $oId,
-                                               'msg_lang' => $lang,
-                                               'msg_key' => 'text',
-                                               'msg_text' => $option['text'],
-                                       );
-                                       self::processFormData( $option, 
$properties, $messages,
-                                               
SecurePoll_Ballot::$ballotTypes[$ballot], 'option', $oId, $lang
-                                       );
-                                       self::processFormData( $option, 
$properties, $messages,
-                                               
SecurePoll_Tallier::$tallierTypes[$tally], 'option', $oId, $lang
-                                       );
-                                       self::processFormData( $option, 
$properties, $messages,
-                                               
SecurePoll_Crypt::$cryptTypes[$crypt], 'option', $oId, $lang
-                                       );
+                                       self::savePropertiesAndMessages( $dbw, 
$oId, $option );
                                }
                        }
 
-                       // Insert the properties and messages now
-                       $dbw->insert( 'securepoll_properties', $properties, 
__METHOD__ );
-                       $dbw->insert( 'securepoll_msgs', $messages, __METHOD__ 
);
-
                        // Create the "redirect" polls on all the local wikis
-                       foreach ( $remoteWikis as $dbname ) {
-                               $dbw = wfGetDB( DB_MASTER, array(), $dbname );
-                               $dbw->begin();
-                               $dbws[] = $dbw;
+                       if ( $store->rId ) {
+                               $election = $context->getElection( $store->rId 
);
+                               foreach ( $store->remoteWikis as $dbname ) {
+                                       $dbw = wfGetDB( DB_MASTER, array(), 
$dbname );
+                                       $dbw->begin();
+                                       $dbws[] = $dbw;
 
-                               $id = $dbw->selectField( 
'securepoll_elections', 'el_entity', array(
-                                       'el_title' => 
$formData['election_title']
-                               ) );
-                               if ( $id ) {
-                                       foreach ( $dbws as $dbw ) {
-                                               $dbw->rollback();
-                                       }
-                                       return Status::newFatal( 
'securepoll-create-duplicate-title',
-                                               $this->getWikiName( $dbname ), 
$dbname
-                                       );
-                               }
-
-                               $rId = self::insertEntity( $dbw, 'election' );
-                               $dbw->insert( 'securepoll_elections',
-                                       array(
-                                               'el_entity' => $rId,
-                                               'el_title' => 
$formData['election_title'],
-                                               'el_ballot' => $ballot,
-                                               'el_tally' => $tally,
-                                               'el_primary_lang' => $lang,
-                                               'el_start_date' => 
$dbw->timestamp( $startDate ),
-                                               'el_end_date' => 
$dbw->timestamp( $endDate ),
-                                               'el_auth_type' => 'local',
-                                       ),
-                                       __METHOD__
-                               );
-
-                               foreach ( $remoteProperties as $key => $value ) 
{
-                                       $dbw->insert( 'securepoll_properties',
+                                       // Find an existing dummy election, if 
any
+                                       $rId = $dbw->selectField(
+                                               array( 'p1' => 
'securepoll_properties', 'p2' => 'securepoll_properties' ),
+                                               'p1.pr_entity',
                                                array(
-                                                       'pr_entity' => $rId,
-                                                       'pr_key' => $key,
-                                                       'pr_value' => $value,
+                                                       'p1.pr_entity = 
p2.pr_entity',
+                                                       'p1.pr_key' => 
'jump-id',
+                                                       'p1.pr_value' => $eId,
+                                                       'p2.pr_key' => 
'main-wiki',
+                                                       'p2.pr_value' => 
wfWikiID(),
+                                               )
+                                       );
+                                       if ( !$rId ) {
+                                               $rId = self::insertEntity( 
$dbw, 'election' );
+                                       }
+
+                                       // Check for duplicate title
+                                       $id = $dbw->selectField( 
'securepoll_elections', 'el_entity', array(
+                                               'el_title' => 
$formData['election_title']
+                                       ) );
+                                       if ( $id && $id !== $rId ) {
+                                               throw new 
SecurePoll_StatusException( 'securepoll-create-duplicate-title',
+                                                       
SecurePoll_FormStore::getWikiName( $dbname ), $dbname
+                                               );
+                                       }
+
+                                       // Insert it! We don't have to care 
about questions or options here.
+                                       $dbw->replace( 'securepoll_elections',
+                                               array( 'el_entity' ),
+                                               array(
+                                                       'el_entity' => $rId,
+                                                       'el_title' => 
$election->title,
+                                                       'el_ballot' => 
$election->ballotType,
+                                                       'el_tally' => 
$election->tallyType,
+                                                       'el_primary_lang' => 
$election->getLanguage(),
+                                                       'el_start_date' => 
$dbw->timestamp( $election->getStartDate() ),
+                                                       'el_end_date' => 
$dbw->timestamp( $election->getEndDate() ),
+                                                       'el_auth_type' => 
$election->authType,
                                                ),
+                                               __METHOD__
+                                       );
+                                       self::savePropertiesAndMessages( $dbw, 
$rId, $election );
+
+                                       // Fix jump-id
+                                       $dbw->update( 'securepoll_properties',
+                                               array( 'pr_value' => $eId ),
+                                               array( 'pr_entity' => $rId, 
'pr_key' => 'jump-id' ),
                                                __METHOD__
                                        );
                                }
@@ -505,14 +506,137 @@
                        foreach ( $dbws as $dbw ) {
                                $dbw->commit();
                        }
-
-                       return Status::newGood( $eId );
+               } catch ( SecurePoll_StatusException $ex ) {
+                       foreach ( $dbws as $dbw ) {
+                               $dbw->rollback();
+                       }
+                       return $ex->status;
                } catch ( Exception $ex ) {
                        foreach ( $dbws as $dbw ) {
                                $dbw->rollback();
                        }
                        throw $ex;
                }
+
+               // Record this election to the SecurePoll namespace, if so 
configured.
+               if ( $wgSecurePollUseNamespace ) {
+                       // Create a new context to bypass caching
+                       $context = new SecurePoll_Context;
+                       $election = $context->getElection( $eId );
+
+                       list( $title, $content ) = 
SecurePollContentHandler::makeContentFromElection( $election );
+                       $wp = WikiPage::factory( $title );
+                       $wp->doEditContent( $content, $formData['comment'] );
+
+                       list( $title, $content ) = 
SecurePollContentHandler::makeContentFromElection(
+                               $election, 'msg/' . $election->getLanguage() );
+                       $wp = WikiPage::factory( $title );
+                       $wp->doEditContent( $content, $formData['comment'] );
+               }
+
+               return Status::newGood( $eId );
+       }
+
+       /**
+        * Recreate the form data from an election
+        *
+        * @param SecurePoll_Election $election
+        * @return array
+        */
+       private function getFormDataFromElection( $election ) {
+               $lang = $this->election->getLanguage();
+               $data = array_replace_recursive(
+                       SecurePollContentHandler::getDataFromElection( 
$this->election, "msg/$lang" ),
+                       SecurePollContentHandler::getDataFromElection( 
$this->election )
+               );
+               $p = &$data['properties'];
+
+               $startDate = new MWTimestamp( $data['startDate'] );
+               $endDate = new MWTimestamp( $data['endDate'] );
+
+               $ballot = $data['ballot'];
+               $tally = $data['tally'];
+               $crypt = isset( $p['encrypt-type'] ) ? $p['encrypt-type'] : 
'none';
+
+               $formData = array(
+                       'election_id' => $data['id'],
+                       'election_title' => $data['title'],
+                       'property_wiki' => isset( $p['wikis-val'] ) ? 
$p['wikis-val'] : null,
+                       'election_primaryLang' => $data['lang'],
+                       'election_dates' => array(
+                               $startDate->format( 'Y-m-d' ),
+                               $endDate->diff( $startDate )->format( '%a' ),
+                       ),
+                       'return-url' => isset( $p['return-url'] ) ? 
$p['return-url'] : null,
+                       'election_type' => "{$ballot}+{$tally}",
+                       'election_crypt' => $crypt,
+                       'disallow-change' => (bool)isset( $p['disallow-change'] 
) ? $p['disallow-change'] : null,
+                       'property_admins' => array(),
+                       'questions' => array(),
+                       'comment' => '',
+               );
+
+               if ( isset( $data['properties']['admins'] ) ) {
+                       foreach ( explode( '|', $data['properties']['admins'] ) 
as $admin ) {
+                               $formData['property_admins'][] = array( 
'username' => $admin );
+                       }
+               }
+
+               $classes = array();
+               $tallyTypes = array();
+               foreach ( SecurePoll_Ballot::$ballotTypes as $class ) {
+                       $classes[] = $class;
+                       foreach ( call_user_func_array( array( $class, 
'getTallyTypes' ), array() ) as $type ) {
+                               $tallyTypes[$type] = true;
+                       }
+               }
+               foreach ( SecurePoll_Tallier::$tallierTypes as $type => $class 
) {
+                       if ( isset( $tallyTypes[$type] ) ) {
+                               $classes[] = $class;
+                       }
+               }
+               foreach ( SecurePoll_Crypt::$cryptTypes as $class ) {
+                       if ( $class !== false ) {
+                               $classes[] = $class;
+                       }
+               }
+
+               foreach ( $classes as $class ) {
+                       self::unprocessFormData( $formData, $data, $class, 
'election' );
+               }
+
+               foreach ( $data['questions'] as $question ) {
+                       $q = array(
+                               'text' => $question['messages']['text'],
+                       );
+                       if ( isset( $question['id'] ) ) {
+                               $q['id'] = $question['id'];
+                       }
+
+                       foreach ( $classes as $class ) {
+                               self::unprocessFormData( $q, $question, $class, 
'question' );
+                       }
+
+                       // Process options for this question
+                       foreach ( $question['options'] as $option ) {
+                               $o = array(
+                                       'text' => $option['messages']['text'],
+                               );
+                               if ( isset( $option['id'] ) ) {
+                                       $o['id'] = $option['id'];
+                               }
+
+                               foreach ( $classes as $class ) {
+                                       self::unprocessFormData( $o, $option, 
$class, 'option' );
+                               }
+
+                               $q['options'][] = $o;
+                       }
+
+                       $formData['questions'][] = $q;
+               }
+
+               return $formData;
        }
 
        /**
@@ -522,7 +646,7 @@
         * @return int
         */
        private static function insertEntity( $dbw, $type ) {
-               $id = $dbw->selectField( 'securepoll_entity', 'MAX(en_id)' ) + 
1;
+               $id = $dbw->nextSequenceValue( 'securepoll_en_id_seq' );
                $dbw->insert( 'securepoll_entity',
                        array(
                                'en_id' => $id,
@@ -530,7 +654,45 @@
                        ),
                        __METHOD__
                );
+               $id = $dbw->insertId();
                return $id;
+       }
+
+       /**
+        * Save properties and messages for an entity
+        *
+        * @param IDatabase $dbw
+        * @param int $id
+        * @param SecurePoll_Entity $entity
+        * @return array
+        */
+       private static function savePropertiesAndMessages( $dbw, $id, $entity ) 
{
+               $properties = array();
+               foreach ( $entity->getAllProperties() as $key => $value ) {
+                       $properties[] = array(
+                               'pr_entity' => $id,
+                               'pr_key' => $key,
+                               'pr_value' => $value,
+                       );
+               }
+               $dbw->replace( 'securepoll_properties', array( 'pr_entity', 
'pr_key' ), $properties, __METHOD__ );
+
+               $messages = array();
+               $langs = $entity->getLangList();
+               foreach ( $entity->getMessageNames() as $name ) {
+                       foreach ( $langs as $lang ) {
+                               $value = $entity->getRawMessage( $name, $lang );
+                               if ( $value !== false ) {
+                                       $messages[] = array(
+                                               'msg_entity' => $id,
+                                               'msg_lang' => $lang,
+                                               'msg_key' => $name,
+                                               'msg_text' => $value,
+                                       );
+                               }
+                       }
+               }
+               $dbw->replace( 'securepoll_msgs', array( 'msg_entity', 
'msg_lang', 'msg_key' ), $messages, __METHOD__ );
        }
 
        /**
@@ -539,12 +701,16 @@
         * @param array &$outItems Array to insert the descriptors into
         * @param string $field Owning field name, for hide-if
         * @param string|array $types Type value(s) in the field, for hide-if
-        * @param string $class Class with the ::getCreateDescriptors static 
method
+        * @param string|false $class Class with the ::getCreateDescriptors 
static method
         * @param string|null $category If given, ::getCreateDescriptors is
         *    expected to return an array with subarrays for different 
categories
         *    of descriptors, and this selects which subarray to process.
         */
        private static function processFormItems( &$outItems, $field, $types, 
$class, $category = null ) {
+               if ( $class === false ) {
+                       return;
+               }
+
                $items = call_user_func_array( array( $class, 
'getCreateDescriptors' ), array() );
 
                if ( !is_array( $types ) ) {
@@ -578,21 +744,20 @@
        }
 
        /**
-        * Extract the values for the class's properties and messages
+        * Inject form field values for the class's properties and messages
         *
-        * @param array $formData Form data array
-        * @param array &$properties Array to store properties into
-        * @param array &$messages Array to store messages into
-        * @param string $class Class with the ::getCreateDescriptors static 
method
+        * @param array &$formData Form data array
+        * @param array $data Input data array
+        * @param string|false $class Class with the ::getCreateDescriptors 
static method
         * @param string|null $category If given, ::getCreateDescriptors is
         *    expected to return an array with subarrays for different 
categories
         *    of descriptors, and this selects which subarray to process.
-        * @param int $id Entity ID the data belongs to
-        * @param string $lang Language for the messages
         */
-       private static function processFormData(
-               $formData, &$properties, &$messages, $class, $category, $id, 
$lang
-       ) {
+       private static function unprocessFormData( &$formData, $data, $class, 
$category ) {
+               if ( $class === false ) {
+                       return;
+               }
+
                $items = call_user_func_array( array( $class, 
'getCreateDescriptors' ), array() );
 
                if ( $category ) {
@@ -606,40 +771,31 @@
                        if ( !isset( $item['SecurePoll_type'] ) ) {
                                continue;
                        }
-                       $value = $formData[$key];
                        switch ( $item['SecurePoll_type'] ) {
                                case 'property':
-                                       $properties[] = array(
-                                               'pr_entity' => $id,
-                                               'pr_key' => $key,
-                                               'pr_value' => $value,
-                                       );
+                                       if ( isset( $data['properties'][$key] ) 
) {
+                                               $formData[$key] = 
$data['properties'][$key];
+                                       } else {
+                                               $formData[$key] = null;
+                                       }
                                        break;
                                case 'properties':
-                                       foreach ( $value as $k => $v ) {
-                                               $properties[] = array(
-                                                       'pr_entity' => $id,
-                                                       'pr_key' => $k,
-                                                       'pr_value' => $v,
-                                               );
+                                       $formData[$key] = array();
+                                       foreach ( $data['properties'] as $k => 
$v ) {
+                                               $formData[$key][$k] = $v;
                                        }
                                        break;
                                case 'message':
-                                       $messages[] = array(
-                                               'msg_entity' => $id,
-                                               'msg_lang' => $lang,
-                                               'msg_key' => $key,
-                                               'msg_text' => $value,
-                                       );
+                                       if ( isset( $data['messages'][$key] ) ) 
{
+                                               $formData[$key] = 
$data['messages'][$key];
+                                       } else {
+                                               $formData[$key] = null;
+                                       }
                                        break;
                                case 'messages':
-                                       foreach ( $value as $k => $v ) {
-                                               $messages[] = array(
-                                                       'msg_entity' => $id,
-                                                       'msg_lang' => $lang,
-                                                       'msg_key' => $k,
-                                                       'msg_text' => $v,
-                                               );
+                                       $formData[$key] = array();
+                                       foreach ( $data['messages'] as $k => $v 
) {
+                                               $formData[$key][$k] = $v;
                                        }
                                        break;
                        }
@@ -665,6 +821,254 @@
 
                return true;
        }
+}
+
+/**
+ * SecurePoll_Store for loading the form data.
+ */
+class SecurePoll_FormStore extends SecurePoll_MemoryStore {
+       public $eId, $rId = 0;
+       public $qIds = array(), $oIds = array();
+       public $remoteWikis;
+
+       private $lang;
+
+       public function __construct( $formData ) {
+               global $wgSecurePollCreateWikiGroupDir, 
$wgSecurePollCreateWikiGroups,
+                       $wgSecurePollCreateRemoteScriptPath;
+
+               $curId = 0;
+
+               $wikis = isset( $formData['property_wiki'] ) ? 
$formData['property_wiki'] : wfWikiID();
+               if ( $wikis === '*' ) {
+                       $wikis = array_values( self::getWikiList() );
+               } elseif ( substr( $wikis, 0, 1 ) === '@' ) {
+                       $wikis = false;
+
+                       // HTMLForm already checked this, but let's do it again 
anyway.
+                       if ( isset( $wgSecurePollCreateWikiGroups[$wikis] ) ) {
+                               $wikis = file_get_contents(
+                                       $wgSecurePollCreateWikiGroupDir . 
substr( $wikis, 1 ) . '.dblist'
+                               );
+                       }
+
+                       if ( !$wikis ) {
+                               return Status::newFatal( 
'securepoll-create-fail-bad-dblist' );
+                       }
+                       $wikis = array_map( 'trim', explode( "\n", trim( $wikis 
) ) );
+               } else {
+                       $wikis = (array)$wikis;
+               }
+
+               $this->remoteWikis = array_diff( $wikis, array( wfWikiID() ) );
+
+               // Create the entry for the election
+               list( $ballot,$tally ) = explode( '+', 
$formData['election_type'] );
+               $crypt = $formData['election_crypt'];
+
+               $date = new DateTime(
+                       "{$formData['election_dates'][0]}T00:00:00Z",
+                       new DateTimeZone( 'GMT' )
+               );
+               $startDate = $date->format( 'YmdHis' );
+               $date->add( new DateInterval( 
"P{$formData['election_dates'][1]}D" ) );
+               $endDate = $date->format( 'YmdHis' );
+
+               $this->lang = $formData['election_primaryLang'];
+
+               $eId = (int)$formData['election_id'] <= 0 ? --$curId : 
(int)$formData['election_id'];
+               $this->eId = $eId;
+               $this->entityInfo[$eId] = array(
+                       'id' => $eId,
+                       'type' => 'election',
+                       'title' => $formData['election_title'],
+                       'ballot' => $ballot,
+                       'tally' => $tally,
+                       'primaryLang' => $this->lang,
+                       'startDate' => wfTimestamp( TS_MW, $startDate ),
+                       'endDate' => wfTimestamp( TS_MW, $endDate ),
+                       'auth' => $this->remoteWikis ? 'remote-mw' : 'local',
+                       'questions' => array(),
+               );
+               $this->properties[$eId] = array(
+                       'encrypt-type' => $crypt,
+                       'wikis' => join( "\n", $wikis ),
+                       'wikis-val' => isset( $formData['property_wiki'] ) ? 
$formData['property_wiki'] : wfWikiID(),
+                       'return-url' => $formData['return-url'],
+                       'disallow-change' => $formData['disallow-change'] ? 1 : 
0,
+               );
+               $this->messages[$this->lang][$eId] = array(
+                       'title' => $formData['election_title'],
+               );
+
+               $admins = $this->getAdminsList( $formData['property_admins'] );
+               $this->properties[$eId]['admins'] = $admins;
+
+               if ( $this->remoteWikis ) {
+                       $this->properties[$eId]['remote-mw-script-path'] = 
$wgSecurePollCreateRemoteScriptPath;
+
+                       $this->rId = $rId = --$curId;
+                       $this->entityInfo[$rId] = array(
+                               'id' => $rId,
+                               'type' => 'election',
+                               'title' => $formData['election_title'],
+                               'ballot' => $ballot,
+                               'tally' => $tally,
+                               'primaryLang' => $this->lang,
+                               'startDate' => wfTimestamp( TS_MW, $startDate ),
+                               'endDate' => wfTimestamp( TS_MW, $endDate ),
+                               'auth' => 'local',
+                               'questions' => array(),
+                       );
+                       $this->properties[$rId]['main-wiki'] = wfWikiID();
+                       $this->properties[$rId]['jump-url'] = 
SpecialPage::getTitleFor( 'SecurePoll' );
+                       $this->properties[$rId]['jump-id'] = $eId;
+                       $this->properties[$rId]['admins'] = $admins;
+               }
+
+               $this->processFormData( $eId, $formData, 
SecurePoll_Ballot::$ballotTypes[$ballot], 'election' );
+               $this->processFormData( $eId, $formData, 
SecurePoll_Tallier::$tallierTypes[$tally], 'election' );
+               $this->processFormData( $eId, $formData, 
SecurePoll_Crypt::$cryptTypes[$crypt], 'election' );
+
+               // Process each question
+               foreach ( $formData['questions'] as $question ) {
+                       if ( (int)$question['id'] <= 0 ) {
+                               $qId = --$curId;
+                       } else {
+                               $qId = (int)$question['id'];
+                               $this->qIds[] = $qId;
+                       }
+                       $this->entityInfo[$qId] = array(
+                               'id' => $qId,
+                               'type' => 'question',
+                               'election' => $eId,
+                               'options' => array(),
+                       );
+                       $this->properties[$qId] = array();
+                       $this->messages[$this->lang][$qId] = array(
+                               'text' => $question['text'],
+                       );
+
+                       $this->processFormData( $qId, $question, 
SecurePoll_Ballot::$ballotTypes[$ballot], 'question' );
+                       $this->processFormData( $qId, $question, 
SecurePoll_Tallier::$tallierTypes[$tally], 'question' );
+                       $this->processFormData( $qId, $question, 
SecurePoll_Crypt::$cryptTypes[$crypt], 'question' );
+
+                       // Process options for this question
+                       foreach ( $question['options'] as $option ) {
+                               if ( (int)$option['id'] <= 0 ) {
+                                       $oId = --$curId;
+                               } else {
+                                       $oId = (int)$option['id'];
+                                       $this->oIds[] = $oId;
+                               }
+                               $this->entityInfo[$oId] = array(
+                                       'id' => $oId,
+                                       'type' => 'option',
+                                       'election' => $eId,
+                                       'question' => $qId,
+                               );
+                               $this->properties[$oId] = array();
+                               $this->messages[$this->lang][$oId] = array(
+                                       'text' => $option['text'],
+                               );
+
+                               $this->processFormData( $oId, $option, 
SecurePoll_Ballot::$ballotTypes[$ballot], 'option' );
+                               $this->processFormData( $oId, $option, 
SecurePoll_Tallier::$tallierTypes[$tally], 'option' );
+                               $this->processFormData( $oId, $option, 
SecurePoll_Crypt::$cryptTypes[$crypt], 'option' );
+
+                               $this->entityInfo[$qId]['options'][] = 
&$this->entityInfo[$oId];
+                       }
+
+                       $this->entityInfo[$eId]['questions'][] = 
&$this->entityInfo[$qId];
+               }
+       }
+
+       /**
+        * Extract the values for the class's properties and messages
+        *
+        * @param int $id
+        * @param array $formData Form data array
+        * @param string|false $class Class with the ::getCreateDescriptors 
static method
+        * @param string|null $category If given, ::getCreateDescriptors is
+        *    expected to return an array with subarrays for different 
categories
+        *    of descriptors, and this selects which subarray to process.
+        */
+       private function processFormData( $id, $formData, $class, $category ) {
+               if ( $class === false ) {
+                       return;
+               }
+
+               $items = call_user_func_array( array( $class, 
'getCreateDescriptors' ), array() );
+
+               if ( $category ) {
+                       if ( !isset( $items[$category] ) ) {
+                               return;
+                       }
+                       $items = $items[$category];
+               }
+
+               foreach ( $items as $key => $item ) {
+                       if ( !isset( $item['SecurePoll_type'] ) ) {
+                               continue;
+                       }
+                       $value = $formData[$key];
+                       switch ( $item['SecurePoll_type'] ) {
+                               case 'property':
+                                       $this->properties[$id][$key] = $value;
+                                       break;
+                               case 'properties':
+                                       foreach ( $value as $k => $v ) {
+                                               $this->properties[$id][$k] = $v;
+                                       }
+                                       break;
+                               case 'message':
+                                       $this->messages[$this->lang][$id][$key] 
= $value;
+                                       break;
+                               case 'messages':
+                                       foreach ( $value as $k => $v ) {
+                                               
$this->messages[$this->lang][$id][$k] = $v;
+                                       }
+                                       break;
+                       }
+               }
+       }
+
+       /**
+        * Get the name of a wiki
+        *
+        * @param string $dbname
+        * @return string
+        */
+       public static function getWikiName( $dbname ) {
+               $name = WikiMap::getWikiName( $dbname );
+               return $name ?: $dbname;
+       }
+
+       /**
+        * Get the list of wiki names
+        *
+        * @return array
+        */
+       public static function getWikiList() {
+               global $wgConf;
+
+               $wikiNames = array();
+               foreach ( $wgConf->getLocalDatabases() as $dbname ) {
+                       $host = self::getWikiName( $dbname );
+                       if ( strpos( $host, '.' ) ) {
+                               // e.g. "en.wikipedia.org"
+                               $wikiNames[$host] = $dbname;
+                       }
+               }
+
+               // Make sure the local wiki is represented
+               $dbname = wfWikiID();
+               $wikiNames[self::getWikiName( $dbname )] = $dbname;
+
+               ksort( $wikiNames );
+
+               return $wikiNames;
+       }
 
        /**
         * Convert the submitted array of admin usernames into a string for
@@ -680,41 +1084,13 @@
                }
                return join( '|', $admins );
        }
+}
 
-       /**
-        * Get the name of a wiki
-        *
-        * @param string $dbname
-        * @return string
-        */
-       private function getWikiName( $dbname ) {
-               $name = WikiMap::getWikiName( $dbname );
-               return $name ?: $dbname;
-       }
+class SecurePoll_StatusException extends Exception {
+       public $status;
 
-       /**
-        * Get the list of wiki names
-        *
-        * @return array
-        */
-       private function getWikiList() {
-               global $wgConf;
-
-               $wikiNames = array();
-               foreach ( $wgConf->getLocalDatabases() as $dbname ) {
-                       $host = $this->getWikiName( $dbname );
-                       if ( strpos( $host, '.' ) ) {
-                               // e.g. "en.wikipedia.org"
-                               $wikiNames[$host] = $dbname;
-                       }
-               }
-
-               // Make sure the local wiki is represented
-               $dbname = wfWikiID();
-               $wikiNames[$this->getWikiName( $dbname )] = $dbname;
-
-               ksort( $wikiNames );
-
-               return $wikiNames;
+       function __construct( $message /* ... */ ) {
+               $args = func_get_args();
+               $this->status = call_user_func_array( 'Status::newFatal', $args 
);
        }
 }
diff --git a/includes/pages/EntryPage.php b/includes/pages/EntryPage.php
index ef2f504..b6f9cba 100644
--- a/includes/pages/EntryPage.php
+++ b/includes/pages/EntryPage.php
@@ -44,26 +44,37 @@
        public $subpages = array(
                'vote' => array(
                        'public' => true,
+                       'visible-after-start' => true,
                        'visible-after-close' => false,
                ),
                'translate' => array(
                        'public' => true,
+                       'visible-after-start' => true,
                        'visible-after-close' => true,
                ),
                'list' => array(
                        'public' => true,
+                       'visible-after-start' => true,
                        'visible-after-close' => true,
+               ),
+               'edit' => array(
+                       'public' => false,
+                       'visible-after-start' => false,
+                       'visible-after-close' => false,
                ),
                'votereligibility' => array(
                        'public' => false,
+                       'visible-after-start' => true,
                        'visible-after-close' => true,
                ),
                'dump' => array(
                        'public' => false,
+                       'visible-after-start' => true,
                        'visible-after-close' => true,
                ),
                'tally' => array(
                        'public' => false,
+                       'visible-after-start' => true,
                        'visible-after-close' => true,
                ),
        );
@@ -147,8 +158,9 @@
                                $s .= $sep;
                        }
                        if( ( $this->isAdmin || $props['public'] )
-                               && ( !$this->election->isFinished() || 
$props['visible-after-close'] ) )
-                       {
+                               && ( !$this->election->isStarted() || 
$props['visible-after-start'] )
+                               && ( !$this->election->isFinished() || 
$props['visible-after-close'] )
+                       ) {
                                $title = $this->entryPage->parent->getTitle( 
"$subpage/$id" );
                                $s .= Linker::makeKnownLinkObj( $title, 
$linkText );
                        } else {
diff --git a/includes/pages/TranslatePage.php b/includes/pages/TranslatePage.php
index 7dc473d..1b13238 100644
--- a/includes/pages/TranslatePage.php
+++ b/includes/pages/TranslatePage.php
@@ -9,7 +9,7 @@
         * @param $params array Array of subpage parameters.
         */
        function execute( $params ) {
-               global $wgOut, $wgUser, $wgLang, $wgRequest;
+               global $wgOut, $wgUser, $wgLang, $wgRequest, 
$wgSecurePollUseNamespace;
 
                if ( !count( $params ) ) {
                        $wgOut->addWikiMsg( 'securepoll-too-few-params' );
@@ -95,6 +95,14 @@
                }
                $s .= '</table>';
                if ( $this->isAdmin ) {
+                       if ( $wgSecurePollUseNamespace ) {
+                               $s .=
+                                       '<p style="text-align: center;">' .
+                                       wfMsgHtml( 
'securepoll-translate-label-comment' ) .
+                                       Xml::input( 'comment' ) .
+                                       "</p>";
+                       }
+
                        $s .=
                        '<p style="text-align: center;">' .
                        Xml::submitButton( wfMsg( 'securepoll-submit-translate' 
) ) .
@@ -147,7 +155,7 @@
         * Submit message text changes.
         */
        function doSubmit( $secondary ) {
-               global $wgRequest, $wgOut;
+               global $wgRequest, $wgOut, $wgSecurePollUseNamespace;
 
                if ( !$this->isAdmin ) {
                        $wgOut->addWikiMsg( 'securepoll-need-admin' );
@@ -178,6 +186,13 @@
                                $replaceBatch,
                                __METHOD__
                        );
+
+                       if ( $wgSecurePollUseNamespace ) {
+                               list( $title, $content ) = 
SecurePollContentHandler::makeContentFromElection(
+                                       $this->election, "msg/$secondary" );
+                               $wp = WikiPage::factory( $title );
+                               $wp->doEditContent( $content, 
$wgRequest->getText( 'comment' ) );
+                       }
                }
                $wgOut->redirect( $this->getTitle( $secondary )->getFullUrl() );
        }
diff --git a/includes/pages/VoterEligibilityPage.php 
b/includes/pages/VoterEligibilityPage.php
index 3212147..31ecb71 100644
--- a/includes/pages/VoterEligibilityPage.php
+++ b/includes/pages/VoterEligibilityPage.php
@@ -77,7 +77,9 @@
                }
        }
 
-       private function saveProperties( $properties, $delete ) {
+       private function saveProperties( $properties, $delete, $comment ) {
+               global $wgSecurePollUseNamespace;
+
                $wikis = $this->election->getProperty( 'wikis' );
                if ( $wikis ) {
                        $wikis = explode( "\n", $wikis );
@@ -134,9 +136,20 @@
                        }
                        throw $ex;
                }
+
+               // Record this election to the SecurePoll namespace, if so 
configured.
+               if ( $wgSecurePollUseNamespace ) {
+                       // Create a new context to bypass caching
+                       $context = new SecurePoll_Context;
+                       $election = $context->getElection( 
$this->election->getID() );
+
+                       list( $title, $content ) = 
SecurePollContentHandler::makeContentFromElection( $election );
+                       $wp = WikiPage::factory( $title );
+                       $wp->doEditContent( $content, $comment );
+               }
        }
 
-       private function fetchList( $property ) {
+       private function fetchList( $property, $db = DB_SLAVE ) {
                $wikis = $this->election->getProperty( 'wikis' );
                if ( $wikis ) {
                        $wikis = explode( "\n", $wikis );
@@ -149,7 +162,7 @@
 
                $names = array();
                foreach ( $wikis as $dbname ) {
-                       $dbr = wfGetDB( DB_SLAVE, array(), $dbname );
+                       $dbr = wfGetDB( $db, array(), $dbname );
 
                        $id = $dbr->selectField( 'securepoll_elections', 
'el_entity', array(
                                'el_title' => $this->election->title
@@ -180,10 +193,12 @@
                }
                sort( $names );
 
-               return join( "\n", $names );
+               return $names;
        }
 
-       private function saveList( $property, $names ) {
+       private function saveList( $property, $names, $comment ) {
+               global $wgSecurePollUseNamespace;
+
                $wikis = $this->election->getProperty( 'wikis' );
                if ( $wikis ) {
                        $wikis = explode( "\n", $wikis );
@@ -209,6 +224,8 @@
                        }
                }
 
+               $list = "{$this->election->getId()}/list/$property";
+
                $dbws = array();
                try {
                        foreach ( $wikis as $dbname ) {
@@ -224,7 +241,6 @@
                                        continue;
                                }
 
-                               $list = "$property-" . substr( 
$this->election->title, 0, 200 );
                                $dbw->replace( 'securepoll_properties',
                                        array( 'pr_entity', 'pr_key' ),
                                        array(
@@ -266,9 +282,30 @@
                        }
                        throw $ex;
                }
+
+               // Record this election to the SecurePoll namespace, if so 
configured.
+               if ( $wgSecurePollUseNamespace ) {
+                       // Create a new context to bypass caching
+                       $context = new SecurePoll_Context;
+                       $election = $context->getElection( 
$this->election->getID() );
+
+                       list( $title, $content ) = 
SecurePollContentHandler::makeContentFromElection( $election );
+                       $wp = WikiPage::factory( $title );
+                       $wp->doEditContent( $content, $comment );
+
+                       $json = FormatJson::encode( $this->fetchList( 
$property, DB_MASTER ),
+                               false, FormatJson::ALL_OK );
+                       $title = Title::makeTitle( NS_SECUREPOLL, $list );
+                       $wp = WikiPage::factory( $title );
+                       $wp->doEditContent(
+                               $x=ContentHandler::makeContent( $json, $title, 
'SecurePoll' ), $comment
+                       );
+               }
        }
 
        private function executeConfig() {
+               global $wgSecurePollUseNamespace;
+
                /** @todo These should be migrated to core, once the jquery.ui
                 * objectors write their own date picker. */
                if ( !isset( HTMLForm::$typeMappings['date'] ) || !isset( 
HTMLForm::$typeMappings['daterange'] ) ) {
@@ -559,6 +596,13 @@
                        'default' => Html::closeElement( 'dl' ),
                );
 
+               if ( $wgSecurePollUseNamespace ) {
+                       $formItems['comment'] = array(
+                               'type' => 'text',
+                               'label-message' => 
'securepoll-votereligibility-label-comment',
+                       );
+               }
+
                $form = new HTMLForm( $formItems, $this->parent->getContext(), 
'securepoll-votereligibility' );
                $form->addHeaderText(
                        $this->msg( 'securepoll-votereligibility-basic-info' 
)->parseAsBlock(), 'basic'
@@ -638,10 +682,10 @@
 
                $populate = !empty( $properties['list_populate'] );
                if ( $populate ) {
-                       $properties['need-list'] = 'need-list-' . substr( 
$this->election->title, 0, 200 );
+                       $properties['need-list'] = 'need-list-' . 
$this->election->getId();
                }
 
-               $this->saveProperties( $properties, $deleteProperties );
+               $this->saveProperties( $properties, $deleteProperties, 
$formData['comment'] );
 
                if ( $populate ) {
                        SecurePoll_PopulateVoterListJob::pushJobsForElection( 
$this->election );
@@ -651,6 +695,8 @@
        }
 
        private function executeEdit( $which ) {
+               global $wgSecurePollUseNamespace;
+
                $out = $this->parent->getOutput();
 
                if ( !isset( self::$lists[$which] ) ) {
@@ -681,8 +727,15 @@
                        'label-message' => 
'securepoll-votereligibility-label-names',
                        'type' => 'textarea',
                        'rows' => 20,
-                       'default' => $this->fetchList( $property ),
+                       'default' => join( "\n", $this->fetchList( $property ) 
),
                );
+
+               if ( $wgSecurePollUseNamespace ) {
+                       $formItems['comment'] = array(
+                               'type' => 'text',
+                               'label-message' => 
'securepoll-votereligibility-label-comment',
+                       );
+               }
 
                $form = new HTMLForm( $formItems, $this->parent->getContext(), 
'securepoll-votereligibility' );
                $form->addHeaderText(
@@ -691,7 +744,7 @@
                $form->setDisplayFormat( 'div' );
                $form->setSubmitTextMsg( 
'securepoll-votereligibility-edit-action' );
                $form->setSubmitCallback( function ( $formData, $form ) use ( 
$property ) {
-                       $this->saveList( $property, $formData['names'] );
+                       $this->saveList( $property, $formData['names'], 
$formData['comment'] );
                        return Status::newGood();
                } );
                $result = $form->show();
@@ -706,6 +759,8 @@
        }
 
        private function executeClear( $which ) {
+               global $wgSecurePollUseNamespace;
+
                $out = $this->parent->getOutput();
 
                if ( !isset( self::$lists[$which] ) ) {
@@ -776,6 +831,25 @@
                        throw $ex;
                }
 
+               // Record this election to the SecurePoll namespace, if so 
configured.
+               if ( $wgSecurePollUseNamespace ) {
+                       // Create a new context to bypass caching
+                       $context = new SecurePoll_Context;
+                       $election = $context->getElection( 
$this->election->getID() );
+
+                       list( $title, $content ) = 
SecurePollContentHandler::makeContentFromElection( $election );
+                       $wp = WikiPage::factory( $title );
+                       $wp->doEditContent( $content,
+                               $this->msg( 
'securepoll-votereligibility-cleared-comment', $name ) );
+
+                       $title = Title::makeTitle( NS_SECUREPOLL, 
"{$election->getId()}/list/$property" );
+                       $wp = WikiPage::factory( $title );
+                       $wp->doEditContent(
+                               ContentHandler::makeContent( '[]', $title, 
'SecurePoll' ),
+                               $this->msg( 
'securepoll-votereligibility-cleared-comment', $name )
+                       );
+               }
+
                $out->setPageTitle( $this->msg( 
'securepoll-votereligibility-cleared' ) );
                $out->addWikiMsg( 'securepoll-votereligibility-cleared-text', 
$name );
                $out->returnToMain( false,

-- 
To view, visit https://gerrit.wikimedia.org/r/166056
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ic975dc9a2fba2bf0cc9c4fb8ad4ed906458f76b4
Gerrit-PatchSet: 2
Gerrit-Project: mediawiki/extensions/SecurePoll
Gerrit-Branch: master
Gerrit-Owner: Anomie <bjor...@wikimedia.org>
Gerrit-Reviewer: Anomie <bjor...@wikimedia.org>
Gerrit-Reviewer: Deskana <dga...@wikimedia.org>
Gerrit-Reviewer: Siebrand <siebr...@kitano.nl>
Gerrit-Reviewer: Tim Starling <tstarl...@wikimedia.org>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to