diff --git a/adminstuds.php b/adminstuds.php index 871812b..0c74ed4 100644 --- a/adminstuds.php +++ b/adminstuds.php @@ -17,6 +17,8 @@ * Auteurs de Framadate/OpenSondage : Framasoft (https://github.com/framasoft) */ use Framadate\Editable; +use Framadate\Exception\AlreadyExistsException; +use Framadate\Exception\ConcurrentEditionException; use Framadate\Exception\MomentAlreadyExistsException; use Framadate\Message; use Framadate\Services\AdminPollService; @@ -201,6 +203,7 @@ if (!empty($_POST['save'])) { // Save edition of an old vote $name = $inputService->filterName($_POST['name']); $editedVote = filter_input(INPUT_POST, 'save', FILTER_VALIDATE_INT); $choices = $inputService->filterArray($_POST['choices'], FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => CHOICE_REGEX]]); + $slots_hash = $inputService->filterMD5($_POST['control']); if (empty($editedVote)) { $message = new Message('danger', __('Error', 'Something is going wrong...')); @@ -211,16 +214,21 @@ if (!empty($_POST['save'])) { // Save edition of an old vote if ($message == null) { // Update vote - $result = $pollService->updateVote($poll_id, $editedVote, $name, $choices); - if ($result) { - $message = new Message('success', __('adminstuds', 'Vote updated')); - } else { - $message = new Message('danger', __('Error', 'Update vote failed')); + try { + $result = $pollService->updateVote($poll_id, $editedVote, $name, $choices, $slots_hash); + if ($result) { + $message = new Message('success', __('adminstuds', 'Vote updated')); + } else { + $message = new Message('danger', __('Error', 'Update vote failed')); + } + } catch (ConcurrentEditionException $cee) { + $message = new Message('danger', __('Error', 'Poll has been updated before you vote')); } } } elseif (isset($_POST['save'])) { // Add a new vote $name = $inputService->filterName($_POST['name']); $choices = $inputService->filterArray($_POST['choices'], FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => CHOICE_REGEX]]); + $slots_hash = $inputService->filterMD5($_POST['control']); if ($name == null) { $message = new Message('danger', __('Error', 'The name is invalid.')); @@ -231,11 +239,17 @@ if (!empty($_POST['save'])) { // Save edition of an old vote if ($message == null) { // Add vote - $result = $pollService->addVote($poll_id, $name, $choices); - if ($result) { - $message = new Message('success', __('adminstuds', 'Vote added')); - } else { - $message = new Message('danger', __('Error', 'Adding vote failed')); + try { + $result = $pollService->addVote($poll_id, $name, $choices, $slots_hash); + if ($result) { + $message = new Message('success', __('adminstuds', 'Vote added')); + } else { + $message = new Message('danger', __('Error', 'Adding vote failed')); + } + } catch (AlreadyExistsException $aee) { + $message = new Message('danger', __('Error', 'You already voted')); + } catch (ConcurrentEditionException $cee) { + $message = new Message('danger', __('Error', 'Poll has been updated before you vote')); } } } @@ -428,6 +442,7 @@ $smarty->assign('title', __('Generic', 'Poll') . ' - ' . $poll->title); $smarty->assign('expired', strtotime($poll->end_date) < time()); $smarty->assign('deletion_date', strtotime($poll->end_date) + PURGE_DELAY * 86400); $smarty->assign('slots', $poll->format === 'D' ? $pollService->splitSlots($slots) : $slots); +$smarty->assign('slots_hash', $pollService->hashSlots($slots)); $smarty->assign('votes', $pollService->splitVotes($votes)); $smarty->assign('best_choices', $pollService->computeBestChoices($votes)); $smarty->assign('comments', $comments); diff --git a/app/classes/Framadate/Exception/AlreadyExistsException.php b/app/classes/Framadate/Exception/AlreadyExistsException.php new file mode 100644 index 0000000..510b798 --- /dev/null +++ b/app/classes/Framadate/Exception/AlreadyExistsException.php @@ -0,0 +1,9 @@ + ['regexp' => MD5_REGEX]]); + } + public function filterBoolean($boolean) { return !!filter_var($boolean, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => BOOLEAN_TRUE_REGEX]]); } diff --git a/app/classes/Framadate/Services/PollService.php b/app/classes/Framadate/Services/PollService.php index a666041..fa2ab46 100644 --- a/app/classes/Framadate/Services/PollService.php +++ b/app/classes/Framadate/Services/PollService.php @@ -18,10 +18,13 @@ */ namespace Framadate\Services; +use Framadate\Exception\AlreadyExistsException; +use Framadate\Exception\ConcurrentEditionException; use Framadate\Form; use Framadate\FramaDB; use Framadate\Repositories\RepositoryFactory; use Framadate\Security\Token; +use Framadate\Utils; class PollService { @@ -82,17 +85,25 @@ class PollService { return $slots; } - public function updateVote($poll_id, $vote_id, $name, $choices) { - $choices = implode($choices); + public function updateVote($poll_id, $vote_id, $name, $choices, $slots_hash) { + // Check if slots are still the same + $this->checkThatSlotsDidntChanged($poll_id, $slots_hash); + // Update vote + $choices = implode($choices); return $this->voteRepository->update($poll_id, $vote_id, $name, $choices); } - function addVote($poll_id, $name, $choices) { + function addVote($poll_id, $name, $choices, $slots_hash) { + // Check if slots are still the same + $this->checkThatSlotsDidntChanged($poll_id, $slots_hash); + + // Check if vote already exists if ($this->voteRepository->existsByPollIdAndName($poll_id, $name)) { - return false; + throw new AlreadyExistsException(); } + // Insert new vote $choices = implode($choices); $token = $this->random(16); return $this->voteRepository->insert($poll_id, $name, $choices, $token); @@ -167,6 +178,16 @@ class PollService { return $splitted; } + /** + * @param $slots array The slots to hash + * @return string The hash + */ + public function hashSlots($slots) { + return md5(array_reduce($slots, function($carry, $item) { + return $carry . $item->id . '@' . $item->moments . ';'; + })); + } + function splitVotes($votes) { $splitted = array(); foreach ($votes as $vote) { @@ -201,4 +222,18 @@ class PollService { return time() + 86400; } + /** + * This method checks if the hash send by the user is the same as the computed hash. + * + * @param $poll_id int The id of the poll + * @param $slots_hash string The hash sent by the user + * @throws ConcurrentEditionException Thrown when hashes are differents + */ + private function checkThatSlotsDidntChanged($poll_id, $slots_hash) { + $slots = $this->slotRepository->listByPollId($poll_id); + if ($slots_hash !== $this->hashSlots($slots)) { + throw new ConcurrentEditionException(); + } + } + } diff --git a/app/inc/constants.php b/app/inc/constants.php index 227b129..2aaecaa 100644 --- a/app/inc/constants.php +++ b/app/inc/constants.php @@ -27,6 +27,7 @@ const BOOLEAN_REGEX = '/^(on|off|true|false|1|0)$/i'; const BOOLEAN_TRUE_REGEX = '/^(on|true|1)$/i'; const EDITABLE_CHOICE_REGEX = '/^[0-2]$/'; const BASE64_REGEX = '/^[A-Za-z0-9]+$/'; +const MD5_REGEX = '/^[A-Fa-f0-9]+$/'; // CSRF (300s = 5min) const TOKEN_TIME = 300; diff --git a/locale/de.json b/locale/de.json index e3f0b07..9dd9a09 100644 --- a/locale/de.json +++ b/locale/de.json @@ -333,6 +333,8 @@ "Failed to save poll": "Speichern der Umfrage fehlgeschlagen", "Update vote failed": "Aktualisierung der Wertung fehlgeschlagen", "Adding vote failed": "Stimmabgabe fehlgeschlagen", + "You already voted": "DE_Vous avez déjà voté", + "Poll has been updated before you vote": "DE_Le sondage a été mis à jour avant votre vote", "Comment failed": "Abgabe des Kommentars gescheitert", "You can't create a poll with hidden results with the following edition option:": "Sie können mit der folgenden Editier-Option keine Umfrage mit versteckten Ergebnissen erzeugen:", "Failed to delete column": "Löschen der Spalte fehlgeschlagen", diff --git a/locale/en.json b/locale/en.json index d70604d..02b1781 100644 --- a/locale/en.json +++ b/locale/en.json @@ -333,6 +333,8 @@ "Failed to save poll": "Failed to save poll", "Update vote failed": "Update vote failed", "Adding vote failed": "Adding vote failed", + "You already voted": "You already voted", + "Poll has been updated before you vote": "Poll has been updated before you vote", "Comment failed": "Comment failed", "You can't create a poll with hidden results with the following edition option:": "You can't create a poll with hidden results with the following option: ", "Failed to delete column": "Failed to delete column", diff --git a/locale/es.json b/locale/es.json index e6fed3f..4264b92 100644 --- a/locale/es.json +++ b/locale/es.json @@ -333,6 +333,8 @@ "Failed to save poll": "ES_Echec de la sauvegarde du sondage", "Update vote failed": "ES_Mise à jour du vote échoué", "Adding vote failed": "ES_Ajout d'un vote échoué", + "You already voted": "ES_Vous avez déjà voté", + "Poll has been updated before you vote": "ES_Le sondage a été mis à jour avant votre vote", "Comment failed": "ES_Commentaire échoué", "You can't create a poll with hidden results with the following edition option:": "ES_Vous ne pouvez pas créer de sondage avec résulats cachés avec les options d'éditions suivantes : ", "Failed to delete column": "Error al eliminar la columna", diff --git a/locale/fr.json b/locale/fr.json index 4a5ce6c..1c3381a 100644 --- a/locale/fr.json +++ b/locale/fr.json @@ -348,6 +348,8 @@ "Failed to save poll": "Echec de la sauvegarde du sondage", "Update vote failed": "Mise à jour du vote échoué", "Adding vote failed": "Ajout d'un vote échoué", + "You already voted": "Vous avez déjà voté", + "Poll has been updated before you vote": "Le sondage a été mis à jour avant votre vote", "Comment failed": "Commentaire échoué", "You can't create a poll with hidden results with the following edition option:": "Vous ne pouvez pas créer de sondage avec résulats cachés avec les options d'éditions suivantes : ", "Failed to delete column": "Échec de la suppression de colonne", diff --git a/locale/it.json b/locale/it.json index a34bc4e..3942846 100644 --- a/locale/it.json +++ b/locale/it.json @@ -333,6 +333,8 @@ "Failed to save poll": "Errore nel salvataggio del sondaggio", "Update vote failed": "Aggiornamento del voto fallito", "Adding vote failed": "Aggiunta del voto fallito", + "You already voted": "IT_Vous avez déjà voté", + "Poll has been updated before you vote": "IT_Le sondage a été mis à jour avant votre vote", "Comment failed": "Commento fallito", "You can't create a poll with hidden results with the following edition option:": "Non potete creare un sondaggio con i risultati nascosti con queste opzioni: ", "Failed to delete column": "Impossibile eliminare la colonna", diff --git a/studs.php b/studs.php index 6a57efb..8816394 100644 --- a/studs.php +++ b/studs.php @@ -16,6 +16,8 @@ * Auteurs de STUdS (projet initial) : Guilhem BORGHESI (borghesi@unistra.fr) et Raphaël DROZ * Auteurs de Framadate/OpenSondage : Framasoft (https://github.com/framasoft) */ +use Framadate\Exception\AlreadyExistsException; +use Framadate\Exception\ConcurrentEditionException; use Framadate\Services\LogService; use Framadate\Services\PollService; use Framadate\Services\InputService; @@ -121,6 +123,7 @@ if (!empty($_POST['save'])) { // Save edition of an old vote $name = $inputService->filterName($_POST['name']); $editedVote = filter_input(INPUT_POST, 'save', FILTER_VALIDATE_INT); $choices = $inputService->filterArray($_POST['choices'], FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => CHOICE_REGEX]]); + $slots_hash = $inputService->filterMD5($_POST['control']); if (empty($editedVote)) { $message = new Message('danger', __('Error', 'Something is going wrong...')); @@ -131,23 +134,28 @@ if (!empty($_POST['save'])) { // Save edition of an old vote if ($message == null) { // Update vote - $result = $pollService->updateVote($poll_id, $editedVote, $name, $choices); - if ($result) { - if ($poll->editable == Editable::EDITABLE_BY_OWN) { - $editedVoteUniqueId = filter_input(INPUT_POST, 'edited_vote', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => POLL_REGEX]]); - $urlEditVote = Utils::getUrlSondage($poll_id, false, $editedVoteUniqueId); - $message = new Message('success', __('studs', 'Your vote has been registered successfully, but be careful: regarding this poll options, you need to keep this personal link to edit your own vote:'), $urlEditVote); + try { + $result = $pollService->updateVote($poll_id, $editedVote, $name, $choices, $slots_hash); + if ($result) { + if ($poll->editable == Editable::EDITABLE_BY_OWN) { + $editedVoteUniqueId = filter_input(INPUT_POST, 'edited_vote', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => POLL_REGEX]]); + $urlEditVote = Utils::getUrlSondage($poll_id, false, $editedVoteUniqueId); + $message = new Message('success', __('studs', 'Your vote has been registered successfully, but be careful: regarding this poll options, you need to keep this personal link to edit your own vote:'), $urlEditVote); + } else { + $message = new Message('success', __('studs', 'Update vote succeeded')); + } + sendUpdateNotification($poll, $mailService, $name, UPDATE_VOTE); } else { - $message = new Message('success', __('studs', 'Update vote succeeded')); + $message = new Message('danger', __('Error', 'Update vote failed')); } - sendUpdateNotification($poll, $mailService, $name, UPDATE_VOTE); - } else { - $message = new Message('danger', __('Error', 'Update vote failed')); + } catch (ConcurrentEditionException $cee) { + $message = new Message('danger', __('Error', 'Poll has been updated before you vote')); } } } elseif (isset($_POST['save'])) { // Add a new vote $name = $inputService->filterName($_POST['name']); $choices = $inputService->filterArray($_POST['choices'], FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => CHOICE_REGEX]]); + $slots_hash = $inputService->filterMD5($_POST['control']); if ($name == null) { $message = new Message('danger', __('Error', 'The name is invalid.')); @@ -158,18 +166,24 @@ if (!empty($_POST['save'])) { // Save edition of an old vote if ($message == null) { // Add vote - $result = $pollService->addVote($poll_id, $name, $choices); - if ($result) { - if ($poll->editable == Editable::EDITABLE_BY_OWN) { - $urlEditVote = Utils::getUrlSondage($poll_id, false, $result->uniqId); - $editedVoteUniqueId = $result->uniqId; - $message = new Message('success', __('studs', 'Your vote has been registered successfully, but be careful: regarding this poll options, you need to keep this personal link to edit your own vote:'), $urlEditVote); + try { + $result = $pollService->addVote($poll_id, $name, $choices, $slots_hash); + if ($result) { + if ($poll->editable == Editable::EDITABLE_BY_OWN) { + $urlEditVote = Utils::getUrlSondage($poll_id, false, $result->uniqId); + $editedVoteUniqueId = $result->uniqId; + $message = new Message('success', __('studs', 'Your vote has been registered successfully, but be careful: regarding this poll options, you need to keep this personal link to edit your own vote:'), $urlEditVote); + } else { + $message = new Message('success', __('studs', 'Adding the vote succeeded')); + } + sendUpdateNotification($poll, $mailService, $name, ADD_VOTE); } else { - $message = new Message('success', __('studs', 'Adding the vote succeeded')); + $message = new Message('danger', __('Error', 'Adding vote failed')); } - sendUpdateNotification($poll, $mailService, $name, ADD_VOTE); - } else { - $message = new Message('danger', __('Error', 'Adding vote failed')); + } catch (AlreadyExistsException $aee) { + $message = new Message('danger', __('Error', 'You already voted')); + } catch (ConcurrentEditionException $cee) { + $message = new Message('danger', __('Error', 'Poll has been updated before you vote')); } } } @@ -211,6 +225,7 @@ $smarty->assign('title', __('Generic', 'Poll') . ' - ' . $poll->title); $smarty->assign('expired', strtotime($poll->end_date) < time()); $smarty->assign('deletion_date', strtotime($poll->end_date) + PURGE_DELAY * 86400); $smarty->assign('slots', $poll->format === 'D' ? $pollService->splitSlots($slots) : $slots); +$smarty->assign('slots_hash', $pollService->hashSlots($slots)); $smarty->assign('votes', $pollService->splitVotes($votes)); $smarty->assign('best_choices', $pollService->computeBestChoices($votes)); $smarty->assign('comments', $comments); diff --git a/tpl/part/vote_table_classic.tpl b/tpl/part/vote_table_classic.tpl index c321cc0..b297982 100644 --- a/tpl/part/vote_table_classic.tpl +++ b/tpl/part/vote_table_classic.tpl @@ -6,6 +6,7 @@
+ diff --git a/tpl/part/vote_table_date.tpl b/tpl/part/vote_table_date.tpl index 03ad351..2f5145f 100644 --- a/tpl/part/vote_table_date.tpl +++ b/tpl/part/vote_table_date.tpl @@ -7,6 +7,7 @@
+
{__('Poll results', 'Votes of the poll')} {$poll->title|html}
{__('Poll results', 'Votes of the poll')} {$poll->title|html}