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