From bc964e87a7b41f12180ae9d9840d8cc233d8c4ff Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Fri, 19 Apr 2019 20:20:55 +0200 Subject: [PATCH] [Big] Move displaying dates from libc to PHP-intl / ICU * Make sure we always work only with DateTimes * Support slots again under SQLite * Small fixes Signed-off-by: Thomas Citharel --- adminstuds.php | 17 +++- app/classes/Framadate/Choice.php | 18 ++-- .../MomentAlreadyExistsException.php | 17 +++- .../Exception/SlotAlreadyExistsException.php | 20 ++++ app/classes/Framadate/Form.php | 12 +++ .../Repositories/AbstractRepository.php | 25 +++-- .../Repositories/CommentRepository.php | 27 +++-- .../Framadate/Repositories/PollRepository.php | 41 +++++--- .../Framadate/Repositories/VoteRepository.php | 82 +++++++++++---- .../Framadate/Security/PasswordHasher.php | 1 - .../Framadate/Services/AdminPollService.php | 36 ++++--- .../Framadate/Services/InputService.php | 12 ++- .../Services/NotificationService.php | 1 - .../Framadate/Services/PollService.php | 65 +++++++----- .../Framadate/Services/SessionService.php | 1 - app/inc/i18n.php | 99 ++++++++++++++++--- app/inc/init.php | 2 +- app/inc/smarty.php | 39 +++++++- composer.json | 5 +- create_classic_poll.php | 4 +- create_date_poll.php | 5 +- exportcsv.php | 2 +- js/app/studs.js | 2 +- studs.php | 7 +- tpl/admin/polls.tpl | 6 +- tpl/create_date_poll_step_2.tpl | 2 +- tpl/mail/find_polls.tpl | 2 +- tpl/part/comments_list.tpl | 4 +- tpl/part/poll_info.tpl | 30 +++++- tpl/part/vote_table_date.tpl | 24 ++--- tpl/studs.tpl | 2 +- 31 files changed, 456 insertions(+), 154 deletions(-) create mode 100644 app/classes/Framadate/Exception/SlotAlreadyExistsException.php diff --git a/adminstuds.php b/adminstuds.php index 02bd208..ff81b08 100644 --- a/adminstuds.php +++ b/adminstuds.php @@ -16,11 +16,14 @@ * Auteurs de STUdS (projet initial) : Guilhem BORGHESI (borghesi@unistra.fr) et Raphaƫl DROZ * Auteurs de Framadate/OpenSondage : Framasoft (https://github.com/framasoft) */ + +use Doctrine\DBAL\DBALException; use Framadate\Editable; use Framadate\Exception\AlreadyExistsException; use Framadate\Exception\ConcurrentEditionException; use Framadate\Exception\ConcurrentVoteException; use Framadate\Exception\MomentAlreadyExistsException; +use Framadate\Exception\SlotAlreadyExistsException; use Framadate\Message; use Framadate\Security\PasswordHasher; use Framadate\Services\AdminPollService; @@ -465,7 +468,7 @@ if (isset($_POST['confirm_add_column'])) { exit_displaying_add_column(new Message('danger', __('Error', "Can't create an empty column."))); } if ($poll->format === 'D') { - $date = DateTime::createFromFormat(__('Date', 'Y-m-d'), $_POST['newdate'])->setTime(0, 0, 0); + $date = $inputService->filterDate($_POST['newdate']); $time = $date->getTimestamp(); $newmoment = strip_tags($_POST['newmoment']); $adminPollService->addDateSlot($poll_id, $time, $newmoment); @@ -475,8 +478,12 @@ if (isset($_POST['confirm_add_column'])) { } $message = new Message('success', __('adminstuds', 'Choice added')); + } catch (SlotAlreadyExistsException $e) { + exit_displaying_add_column(new Message('danger', __f('Error', 'The column %s already exists', $e->getSlot()))); } catch (MomentAlreadyExistsException $e) { - exit_displaying_add_column(new Message('danger', __('Error', 'The column already exists'))); + exit_displaying_add_column(new Message('danger', __f('Error', 'The column %s already exists with %s', $e->getSlot(), $e->getMoment()))); + } catch (DBALException $e) { + exit_displaying_add_column(new Message('danger', __('Error', 'Error while adding a column'))); } } @@ -484,14 +491,16 @@ if (isset($_POST['confirm_add_column'])) { $slots = $pollService->allSlotsByPoll($poll); $votes = $pollService->allVotesByPollId($poll_id); $comments = $pollService->allCommentsByPollId($poll_id); +$deletion_date = clone $poll->end_date; +$deletion_date->add(new DateInterval('P' . PURGE_DELAY . 'D')); // Assign data to template $smarty->assign('poll_id', $poll_id); $smarty->assign('admin_poll_id', $admin_poll_id); $smarty->assign('poll', $poll); $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('expired', $poll->end_date < new DateTime()); +$smarty->assign('deletion_date', $deletion_date); $smarty->assign('slots', $poll->format === 'D' ? $pollService->splitSlots($slots) : $slots); $smarty->assign('slots_hash', $pollService->hashSlots($slots)); $smarty->assign('votes', $pollService->splitVotes($votes)); diff --git a/app/classes/Framadate/Choice.php b/app/classes/Framadate/Choice.php index e583e26..3a2d1be 100644 --- a/app/classes/Framadate/Choice.php +++ b/app/classes/Framadate/Choice.php @@ -24,34 +24,34 @@ class Choice * Name of the Choice */ private $name; - + /** - * All availables slots for this Choice. + * All available slots for this Choice. */ private $slots; - - public function __construct($name='') + + public function __construct($name = '') { $this->name = $name; $this->slots = []; } - + public function addSlot($slot) { $this->slots[] = $slot; } - + public function getName() { return $this->name; } - + public function getSlots() { return $this->slots; } - - static function compare(Choice $a, Choice $b) + + public static function compare(Choice $a, Choice $b) { return strcmp($a->name, $b->name); } diff --git a/app/classes/Framadate/Exception/MomentAlreadyExistsException.php b/app/classes/Framadate/Exception/MomentAlreadyExistsException.php index 287afd6..c6d7f52 100644 --- a/app/classes/Framadate/Exception/MomentAlreadyExistsException.php +++ b/app/classes/Framadate/Exception/MomentAlreadyExistsException.php @@ -1,7 +1,20 @@ moment = $moment; + } + + /** + * @return mixed + */ + public function getMoment() + { + return $this->moment; } } diff --git a/app/classes/Framadate/Exception/SlotAlreadyExistsException.php b/app/classes/Framadate/Exception/SlotAlreadyExistsException.php new file mode 100644 index 0000000..af54755 --- /dev/null +++ b/app/classes/Framadate/Exception/SlotAlreadyExistsException.php @@ -0,0 +1,20 @@ +slot = $slot; + } + + /** + * @return mixed + */ + public function getSlot() + { + return $this->slot; + } +} diff --git a/app/classes/Framadate/Form.php b/app/classes/Framadate/Form.php index 2f62012..c0e1b40 100644 --- a/app/classes/Framadate/Form.php +++ b/app/classes/Framadate/Form.php @@ -18,6 +18,8 @@ */ namespace Framadate; +use DateTime; + class Form { public $title; @@ -26,7 +28,17 @@ class Form public $admin_name; public $admin_mail; public $format; + + /** + * @var DateTime + */ public $end_date; + + /** + * @var DateTime + */ + public $creation_date; + public $choix_sondage; public $ValueMax; diff --git a/app/classes/Framadate/Repositories/AbstractRepository.php b/app/classes/Framadate/Repositories/AbstractRepository.php index fa7b752..3608bef 100644 --- a/app/classes/Framadate/Repositories/AbstractRepository.php +++ b/app/classes/Framadate/Repositories/AbstractRepository.php @@ -2,6 +2,11 @@ namespace Framadate\Repositories; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\ConnectionException; +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Driver\Statement; +use Doctrine\DBAL\Query\QueryBuilder; +use PDOStatement; abstract class AbstractRepository { /** @@ -24,7 +29,7 @@ abstract class AbstractRepository { } /** - * @throws \Doctrine\DBAL\ConnectionException + * @throws ConnectionException */ public function commit() { @@ -32,17 +37,25 @@ abstract class AbstractRepository { } /** - * @throws \Doctrine\DBAL\ConnectionException + * @throws ConnectionException */ public function rollback() { $this->connect->rollback(); } + /** + * @return QueryBuilder + */ + public function createQueryBuilder() + { + return $this->connect->createQueryBuilder(); + } + /** * @param string $sql - * @throws \Doctrine\DBAL\DBALException - * @return bool|\Doctrine\DBAL\Driver\Statement|\PDOStatement + *@throws DBALException + * @return bool|Statement|PDOStatement */ public function prepare($sql) { @@ -51,8 +64,8 @@ abstract class AbstractRepository { /** * @param string $sql - * @throws \Doctrine\DBAL\DBALException - * @return bool|\Doctrine\DBAL\Driver\Statement|\PDOStatement + *@throws DBALException + * @return bool|Statement|PDOStatement */ public function query($sql) { diff --git a/app/classes/Framadate/Repositories/CommentRepository.php b/app/classes/Framadate/Repositories/CommentRepository.php index c122361..89f847d 100644 --- a/app/classes/Framadate/Repositories/CommentRepository.php +++ b/app/classes/Framadate/Repositories/CommentRepository.php @@ -1,19 +1,30 @@ prepare('SELECT * FROM ' . Utils::table('comment') . ' WHERE poll_id = ? ORDER BY id'); $prepared->execute([$poll_id]); - return $prepared->fetchAll(); + $comments = $prepared->fetchAll(); + + /** + * Hack to make date a proper DateTime + */ + return array_map(function($comment) { + $comment->date = Type::getType(Type::DATETIME)->convertToPhpValue($comment->date, $this->connect->getDatabasePlatform()); + return $comment; + }, $comments); } /** @@ -24,7 +35,7 @@ class CommentRepository extends AbstractRepository { * @param $comment * @return bool */ - function insert($poll_id, $name, $comment) + public function insert($poll_id, $name, $comment) { return $this->connect->insert(Utils::table('comment'), ['poll_id' => $poll_id, 'name' => $name, 'comment' => $comment]) > 0; } @@ -32,10 +43,10 @@ class CommentRepository extends AbstractRepository { /** * @param $poll_id * @param $comment_id - * @throws \Doctrine\DBAL\Exception\InvalidArgumentException + * @throws InvalidArgumentException * @return bool */ - function deleteById($poll_id, $comment_id) + public function deleteById($poll_id, $comment_id) { return $this->connect->delete(Utils::table('comment'), ['poll_id' => $poll_id, 'id' => $comment_id]) > 0; } @@ -44,10 +55,10 @@ class CommentRepository extends AbstractRepository { * Delete all comments of a given poll. * * @param $poll_id int The ID of the given poll. - * @throws \Doctrine\DBAL\Exception\InvalidArgumentException + * @throws InvalidArgumentException * @return bool true if action succeeded. */ - function deleteByPollId($poll_id) + public function deleteByPollId($poll_id) { return $this->connect->delete(Utils::table('comment'), ['poll_id' => $poll_id]) > 0; } @@ -56,7 +67,7 @@ class CommentRepository extends AbstractRepository { * @param $poll_id * @param $name * @param $comment - * @throws \Doctrine\DBAL\DBALException + * @throws DBALException * @return bool */ public function exists($poll_id, $name, $comment) diff --git a/app/classes/Framadate/Repositories/PollRepository.php b/app/classes/Framadate/Repositories/PollRepository.php index f44a928..78d0860 100644 --- a/app/classes/Framadate/Repositories/PollRepository.php +++ b/app/classes/Framadate/Repositories/PollRepository.php @@ -1,6 +1,11 @@ connect->insert(Utils::table('poll'), [ 'id' => $poll_id, @@ -19,7 +25,7 @@ class PollRepository extends AbstractRepository { 'description' => $form->description, 'admin_name' => $form->admin_name, 'admin_mail' => $form->admin_mail, - 'end_date' => (new \DateTime)->setTimestamp($form->end_date)->format('Y-m-d H:i:s'), + 'end_date' => $form->end_date->format('Y-m-d H:i:s'), 'format' => $form->format, 'editable' => ($form->editable>=0 && $form->editable<=2) ? $form->editable : 0, 'receiveNewVotes' => $form->receiveNewVotes ? 1 : 0, @@ -34,7 +40,7 @@ class PollRepository extends AbstractRepository { /** * @param $poll_id - * @throws \Doctrine\DBAL\DBALException + * @throws DBALException * @return mixed */ public function findById($poll_id) @@ -44,12 +50,18 @@ class PollRepository extends AbstractRepository { $poll = $prepared->fetch(); $prepared->closeCursor(); + /** + * Hack to make date a proper DateTime + */ + $poll->creation_date = Type::getType(Type::DATETIME)->convertToPhpValue($poll->creation_date, $this->connect->getDatabasePlatform()); + $poll->end_date = Type::getType(Type::DATETIME)->convertToPhpValue($poll->end_date, $this->connect->getDatabasePlatform()); + return $poll; } /** * @param $admin_poll_id - * @throws \Doctrine\DBAL\DBALException + * @throws DBALException * @return mixed */ public function findByAdminId($admin_poll_id) { @@ -58,12 +70,15 @@ class PollRepository extends AbstractRepository { $poll = $prepared->fetch(); $prepared->closeCursor(); + $poll->creation_date = Type::getType(Type::DATETIME)->convertToPhpValue($poll->creation_date, $this->connect->getDatabasePlatform()); + $poll->end_date = Type::getType(Type::DATETIME)->convertToPhpValue($poll->end_date, $this->connect->getDatabasePlatform()); + return $poll; } /** * @param $poll_id - * @throws \Doctrine\DBAL\DBALException + * @throws DBALException * @return bool */ public function existsById($poll_id) { @@ -76,7 +91,7 @@ class PollRepository extends AbstractRepository { /** * @param $admin_poll_id - * @throws \Doctrine\DBAL\DBALException + * @throws DBALException * @return bool */ public function existsByAdminId($admin_poll_id) { @@ -98,7 +113,7 @@ class PollRepository extends AbstractRepository { 'admin_name' => $poll->admin_name, 'admin_mail' => $poll->admin_mail, 'description' => $poll->description, - 'end_date' => $poll->end_date, # TODO : Harmonize dates between here and insert + 'end_date' => $poll->end_date->format('Y-m-d H:i:s'), # TODO : Harmonize dates between here and insert 'active' => $poll->active, 'editable' => $poll->editable >= 0 && $poll->editable <= 2 ? $poll->editable : 0, 'hidden' => $poll->hidden ? 1 : 0, @@ -111,7 +126,7 @@ class PollRepository extends AbstractRepository { /** * @param $poll_id - * @throws \Doctrine\DBAL\Exception\InvalidArgumentException + * @throws InvalidArgumentException * @return bool */ public function deleteById($poll_id) @@ -122,7 +137,7 @@ class PollRepository extends AbstractRepository { /** * Find old polls. Limit: 20. * - * @throws \Doctrine\DBAL\DBALException + * @throws DBALException * @return array Array of old polls */ public function findOldPolls() @@ -138,7 +153,7 @@ class PollRepository extends AbstractRepository { * @param array $search Array of search : ['id'=>..., 'title'=>..., 'name'=>..., 'mail'=>...] * @param int $start The number of first entry to select * @param int $limit The number of entries to find - * @throws \Doctrine\DBAL\DBALException + * @throws DBALException * @return array The found polls */ public function findAll($search, $start, $limit) { @@ -194,7 +209,7 @@ class PollRepository extends AbstractRepository { * Find all polls that are created with the given admin mail. * * @param string $mail Email address of the poll admin - * @throws \Doctrine\DBAL\DBALException + * @throws DBALException * @return array The list of matching polls */ public function findAllByAdminMail($mail) { @@ -208,7 +223,7 @@ class PollRepository extends AbstractRepository { * Get the total number of polls in database. * * @param array $search Array of search : ['id'=>..., 'title'=>..., 'name'=>...] - * @throws \Doctrine\DBAL\DBALException + * @throws DBALException * @return int The number of polls */ public function count($search = null) { diff --git a/app/classes/Framadate/Repositories/VoteRepository.php b/app/classes/Framadate/Repositories/VoteRepository.php index 0c2aa02..c0d247b 100644 --- a/app/classes/Framadate/Repositories/VoteRepository.php +++ b/app/classes/Framadate/Repositories/VoteRepository.php @@ -1,12 +1,15 @@ prepare('UPDATE ' . Utils::table('vote') . ' SET choices = CONCAT(SUBSTRING(choices, 1, ?), " ", SUBSTRING(choices, ?)) WHERE poll_id = ?'); //#51 : default value for unselected vote - - return $prepared->execute([$insert_position, $insert_position + 1, $poll_id]); + $qb = $this->createQueryBuilder(); + $sql = $this->connect->getDatabasePlatform()->getName() === 'sqlite' ? + $this->generateSQLForInsertDefaultSQLite($qb, $insert_position) : + $this->generateSQLForInsertDefault($qb, $insert_position) + ; + $query = $qb->update(Utils::table('vote')) + ->set('choices', $sql) + ->where($qb->expr()->eq('poll_id', $qb->createNamedParameter($poll_id))) + ; + return $query->execute(); } function insert($poll_id, $name, $choices, $token, $mail) { @@ -48,7 +58,7 @@ class VoteRepository extends AbstractRepository { /** * @param $poll_id * @param $vote_id - * @throws \Doctrine\DBAL\DBALException + * @throws DBALException * @return bool */ public function deleteById($poll_id, $vote_id) @@ -66,7 +76,7 @@ class VoteRepository extends AbstractRepository { * Delete all votes of a given poll. * * @param $poll_id int The ID of the given poll. - * @throws \Doctrine\DBAL\DBALException + * @throws DBALException * @return bool|null true if action succeeded. */ public function deleteByPollId($poll_id) @@ -79,14 +89,20 @@ class VoteRepository extends AbstractRepository { * * @param $poll_id int The ID of the poll * @param $index int The index of the vote into the poll - * @throws \Doctrine\DBAL\DBALException * @return bool|null true if action succeeded. */ public function deleteByIndex($poll_id, $index) { - $prepared = $this->prepare('UPDATE ' . Utils::table('vote') . ' SET choices = CONCAT(SUBSTR(choices, 1, ?), SUBSTR(choices, ?)) WHERE poll_id = ?'); - - return $prepared->execute([$index, $index + 2, $poll_id]); + $qb = $this->createQueryBuilder(); + $sql = $this->connect->getDatabasePlatform()->getName() === 'sqlite' ? + $this->generateSQLForInsertDefaultSQLite($qb, $index, true) : + $this->generateSQLForInsertDefault($qb, $index, true) + ; + $query = $qb->update(Utils::table('vote')) + ->set('choices', $sql) + ->where($qb->expr()->eq('poll_id', $qb->createNamedParameter($poll_id))) + ; + return $query->execute(); } /** @@ -113,7 +129,7 @@ class VoteRepository extends AbstractRepository { * * @param int $poll_id ID of the poll * @param string $name Name of the vote - * @throws \Doctrine\DBAL\DBALException + * @throws DBALException * @return bool true if vote already exists */ public function existsByPollIdAndName($poll_id, $name) { @@ -128,7 +144,7 @@ class VoteRepository extends AbstractRepository { * @param int $poll_id ID of the poll * @param string $name Name of the vote * @param int $vote_id ID of the current vote - * @throws \Doctrine\DBAL\DBALException + * @throws DBALException * @return bool true if vote already exists */ public function existsByPollIdAndNameAndVoteId($poll_id, $name, $vote_id) { @@ -136,5 +152,37 @@ class VoteRepository extends AbstractRepository { $prepared->execute([$poll_id, $name, $vote_id]); return $prepared->rowCount() > 0; } + + /** + * @param QueryBuilder $qb + * @param $insert_position + * @param bool $delete + * @return string + */ + private function generateSQLForInsertDefaultSQLite($qb, $insert_position, $delete = false) + { + $position = $insert_position + ($delete ? 2 : 1); + return 'SUBSTR(choices, 1, ' + . $qb->createNamedParameter($insert_position) + . ') || " " || SUBSTR(choices, ' + . $qb->createNamedParameter($position) + . ')'; + } + + /** + * @param QueryBuilder $qb + * @param int $insert_position + * @param bool $delete + * @return string + */ + private function generateSQLForInsertDefault($qb, $insert_position, $delete = false) + { + $position = $insert_position + ($delete ? 2 : 1); + return 'CONCAT(SUBSTR(choices, 1, ' + . $qb->createNamedParameter($insert_position) + . '), " ", SUBSTR(choices, ' + . $qb->createNamedParameter($position) + . '))'; + } } diff --git a/app/classes/Framadate/Security/PasswordHasher.php b/app/classes/Framadate/Security/PasswordHasher.php index ba46a24..0df489d 100644 --- a/app/classes/Framadate/Security/PasswordHasher.php +++ b/app/classes/Framadate/Security/PasswordHasher.php @@ -1,6 +1,5 @@ commentRepository = RepositoryFactory::commentRepository(); } - function updatePoll($poll) { - global $config; - - $end_date = strtotime($poll->end_date); - - if ($end_date < strtotime($poll->creation_date)) { + /** + * @param Form $poll + * @return bool + */ + public function updatePoll($poll) { + if ($poll->end_date < $poll->creation_date) { $poll->end_date = $poll->creation_date; - } elseif ($end_date > $this->pollService->maxExpiryDate()) { - $poll->end_date = utf8_encode(strftime('%Y-%m-%d', $this->pollService->maxExpiryDate())); + } elseif ($poll->end_date > $this->pollService->maxExpiryDate()) { + $poll->end_date = $this->pollService->maxExpiryDate(); } return $this->pollRepository->update($poll); @@ -216,7 +219,7 @@ class AdminPollService { * @param $datetime int The datetime * @param $new_moment string The moment's name * @throws MomentAlreadyExistsException When the moment to add already exists in database - * @throws \Doctrine\DBAL\ConnectionException + * @throws ConnectionException */ public function addDateSlot($poll_id, $datetime, $new_moment) { $this->logService->logEntries( @@ -242,7 +245,7 @@ class AdminPollService { // Check if moment already exists (maybe not necessary) if (in_array($new_moment, $moments, true)) { - throw new MomentAlreadyExistsException(); + throw new MomentAlreadyExistsException($slot, $new_moment); } // Update found slot @@ -257,6 +260,7 @@ class AdminPollService { // Commit transaction $this->connect->commit(); } catch (DBALException $e) { + $this->logService->log('ERROR', "Database error, couldn't insert date slot" . $e->getMessage()); $this->connect->rollBack(); } } @@ -270,7 +274,7 @@ class AdminPollService { * @param $poll_id int The ID of the poll * @param $title int The title * @throws MomentAlreadyExistsException When the moment to add already exists in database - * @throws \Doctrine\DBAL\ConnectionException + * @throws ConnectionException * @throws \Doctrine\DBAL\DBALException */ public function addClassicSlot($poll_id, $title) { @@ -309,10 +313,10 @@ class AdminPollService { * * @param $slots array All the slots of the poll * @param $datetime int The datetime of the new slot - * @return \stdClass An object like this one: {insert:X, slot:Y} where Y can be null. + * @return stdClass An object like this one: {insert:X, slot:Y} where Y can be null. */ private function findInsertPosition($slots, $datetime) { - $result = new \stdClass(); + $result = new stdClass(); $result->slot = null; $result->insert = 0; @@ -329,11 +333,13 @@ class AdminPollService { $result->insert += count($moments); $result->slot = $slot; break; - } elseif ($datetime < $rowDatetime) { + } + + if ($datetime < $rowDatetime) { // We have to insert before this slot break; } - $result->insert += count($moments); + $result->insert += count($moments); } return $result; diff --git a/app/classes/Framadate/Services/InputService.php b/app/classes/Framadate/Services/InputService.php index 6c07a1b..83e3221 100644 --- a/app/classes/Framadate/Services/InputService.php +++ b/app/classes/Framadate/Services/InputService.php @@ -20,6 +20,7 @@ namespace Framadate\Services; use DateTime; use Egulias\EmailValidator\EmailValidator; use Egulias\EmailValidator\Validation\RFCValidation; +use InvalidArgumentException; /** * This class helps to clean all inputs from the users or external services. @@ -124,9 +125,16 @@ class InputService { return $this->returnIfNotBlank($comment); } + /** + * @param string $date + * @return DateTime + */ public function filterDate($date) { - $dDate = DateTime::createFromFormat(__('Date', 'Y-m-d'), $date)->setTime(0, 0, 0); - return $dDate->format('Y-m-d H:i:s'); + $dDate = parse_translation_date($date); + if ($dDate) { + return $dDate; + } + throw new InvalidArgumentException('Invalid date'); } /** diff --git a/app/classes/Framadate/Services/NotificationService.php b/app/classes/Framadate/Services/NotificationService.php index d0941a3..38d51d3 100644 --- a/app/classes/Framadate/Services/NotificationService.php +++ b/app/classes/Framadate/Services/NotificationService.php @@ -1,6 +1,5 @@ connect = $connect; + public function __construct(Connection $connect, LogService $logService, NotificationService $notificationService) { $this->logService = $logService; $this->notificationService = $notificationService; $this->sessionService = new SessionService(); @@ -57,7 +68,7 @@ class PollService { * Find a poll from its ID. * * @param $poll_id int The ID of the poll - * @return \stdClass|null The found poll, or null + * @return stdClass|null The found poll, or null */ function findById($poll_id) { try { @@ -142,7 +153,7 @@ class PollService { * @throws AlreadyExistsException * @throws ConcurrentEditionException * @throws ConcurrentVoteException - * @return \stdClass + * @return stdClass */ function addVote($poll_id, $name, $choices, $slots_hash, $mail) { $this->checkVoteConstraints($choices, $poll_id, $slots_hash, $name); @@ -218,9 +229,10 @@ class PollService { /** * @param Form $form + * @throws ConnectionException * @return array */ - function createPoll(Form $form) { + public function createPoll(Form $form) { // Generate poll IDs, loop while poll ID already exists $this->pollRepository->beginTransaction(); @@ -230,7 +242,7 @@ class PollService { $poll_id = $this->random(16); } while ($this->pollRepository->existsById($poll_id)); $admin_poll_id = $poll_id . $this->random(8); - } else { // User have choosen the poll id + } else { // User have chosen the poll id $poll_id = $form->id; do { $admin_poll_id = $this->random(24); @@ -261,7 +273,7 @@ class PollService { /** * @param array $votes - * @param \stdClass $poll + * @param stdClass $poll * @return array */ public function computeBestChoices($votes, $poll) { @@ -293,8 +305,8 @@ class PollService { function splitSlots($slots) { $splitted = []; foreach ($slots as $slot) { - $obj = new \stdClass(); - $obj->day = $slot->title; + $obj = new stdClass(); + $obj->day = (new DateTime())->setTimestamp((int) $slot->title); $obj->moments = explode(',', $slot->moments); $splitted[] = $obj; @@ -316,7 +328,7 @@ class PollService { function splitVotes($votes) { $splitted = []; foreach ($votes as $vote) { - $obj = new \stdClass(); + $obj = new stdClass(); $obj->id = $vote->id; $obj->name = $vote->name; $obj->uniqId = $vote->uniqId; @@ -330,18 +342,25 @@ class PollService { } /** - * @return int The max timestamp allowed for expiry date + * @throws Exception + * @return DateTime The max timestamp allowed for expiry date */ public function maxExpiryDate() { - global $config; - return time() + (86400 * $config['default_poll_duration']); + try { + global $config; + $default_poll_duration = isset($config['default_poll_duration']) ? $config['default_poll_duration'] : 60; + return (new DateTime())->add(new DateInterval('P' . $default_poll_duration . 'D')); + } catch (Exception $e) { + throw new RuntimeException('Configuration Exception'); + } } /** - * @return int The min timestamp allowed for expiry date + * @throws Exception + * @return DateTime The min timestamp allowed for expiry date */ public function minExpiryDate() { - return time() + 86400; + return (new DateTime())->add(new DateInterval('P1D')); } /** @@ -355,7 +374,7 @@ class PollService { } /** - * @param \stdClass $poll + * @param stdClass $poll * @return array */ private function computeEmptyBestChoices($poll) @@ -440,7 +459,7 @@ class PollService { * This method checks if the votes doesn't conflicts the maximum votes constraint * * @param $user_choice - * @param \stdClass $poll + * @param stdClass $poll * @param string $poll_id * @throws ConcurrentVoteException */ diff --git a/app/classes/Framadate/Services/SessionService.php b/app/classes/Framadate/Services/SessionService.php index ca045e1..29e27e1 100644 --- a/app/classes/Framadate/Services/SessionService.php +++ b/app/classes/Framadate/Services/SessionService.php @@ -1,6 +1,5 @@ format($date); +} + +/** + * Formats a DateTime according to a translated format + * + * @param DateTime $date + * @param string $pattern + * @return string + */ +function date_format_translation(DateTime $date, $pattern = 'Y-m-d') { + return $date->format(__('Date', $pattern)); +} + +/** + * Converts a string into a DateTime according to the IntlDateFormatter + * + * @param $dateString + * @param string $pattern + * @param string|null $forceLocale + * @return DateTime|null + */ +function parse_intl_date($dateString, $pattern = DATE_FORMAT_DATE, $forceLocale = null) { + global $locale; + $local_locale = $forceLocale || $locale; + + $dateFormatter = IntlDateFormatter::create( + $local_locale, + IntlDateFormatter::FULL, + IntlDateFormatter::FULL, + date_default_timezone_get(), + IntlDateFormatter::GREGORIAN, + $pattern + ); + $timestamp = $dateFormatter->parse($dateString); + try { + return (new DateTime())->setTimestamp($timestamp); + } catch (Exception $e) { + return null; + } +} + +/** + * Converts a string into a DateTime according to a translated format + * + * @param string $dateString + * @param string $pattern + * @return DateTime + */ +function parse_translation_date($dateString, $pattern = 'Y-m-d') { + return DateTime::createFromFormat(__('Date', $pattern), $dateString); +} + /* i18n helper functions */ use Symfony\Component\Translation\Loader\PoFileLoader; use Symfony\Component\Translation\Translator; @@ -39,7 +119,7 @@ use Symfony\Component\Translation\Translator; class __i18n { private static $translator; private static $fallbacktranslator; - + public static function init($locale) { self::$translator = new Translator($locale); self::$translator->addLoader('pofile', new PoFileLoader()); @@ -51,7 +131,7 @@ class __i18n { self::$fallbacktranslator->addLoader('pofile', new PoFileLoader()); self::$fallbacktranslator->addResource('pofile', ROOT_DIR . "po/" . DEFAULT_LANGUAGE . ".po", DEFAULT_LANGUAGE); } - + public static function translate($key) { return self::$translator->trans($key) ?: self::$fallbacktranslator->trans($key); @@ -68,16 +148,3 @@ function __f($section, $key, $args) { $args = array_slice(func_get_args(), 2); return vsprintf($msg, $args); } - -/* Date Format */ -$date_format['txt_full'] = __('Date', '%A, %B %e, %Y'); //summary in create_date_poll.php and removal date in choix_(date|autre).php -$date_format['txt_short'] = __('Date', '%A %e %B %Y'); // radio title -$date_format['txt_day'] = __('Date', '%a %e'); -$date_format['txt_date'] = __('Date', '%Y-%m-%d'); -$date_format['txt_month_year'] = __('Date', '%B %Y'); -$date_format['txt_datetime_short'] = __('Date', '%m/%d/%Y %H:%M'); -if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { //%e can't be used on Windows platform, use %#d instead - foreach ($date_format as $k => $v) { - $date_format[$k] = preg_replace('#(?assign('use_nav_js', strstr($serverName, 'framadate.org')); $smarty->assign('provide_fork_awesome', !isset($config['provide_fork_awesome']) || $config['provide_fork_awesome']); $smarty->assign('locale', $locale); $smarty->assign('langs', $ALLOWED_LANGUAGES); -$smarty->assign('date_format', $date_format); if (isset($config['tracking_code'])) { $smarty->assign('tracking_code', $config['tracking_code']); } @@ -46,7 +45,8 @@ if (defined('FAVICON')) { } // Dev Mode -if (isset($_SERVER['FRAMADATE_DEVMODE']) && $_SERVER['FRAMADATE_DEVMODE'] || php_sapi_name() === 'cli-server') { +if (php_sapi_name() === 'cli-server') { + $smarty->caching = 0; $smarty->force_compile = true; $smarty->compile_check = true; } else { @@ -125,3 +125,38 @@ function smarty_modifier_locale_2_lang($locale) { function path_for_datepicker_locale($lang) { return __DIR__ . '/../../js/locales/bootstrap-datepicker.' . $lang . '.js'; } + +/** + * @param $date + * @param string $pattern + * @return string + */ +function smarty_modifier_date_format_intl(DateTime $date, $pattern) { + return date_format_intl($date, $pattern); +} + +/** + * @param DateTime $date + * @return int + */ +function smarty_modifier_date_to_timestamp(DateTime $date) { + return $date->getTimestamp(); +} + +/** + * @param integer $timestamp + * @throws Exception + * @return DateTime + */ +function smarty_modifier_timestamp_to_date($timestamp) { + return (new DateTime())->setTimestamp((int) $timestamp); +} + +/** + * @param DateTime $date + * @param string $pattern + * @return bool|DateTime + */ +function smarty_modifier_date_format_translation(DateTime $date, $pattern = 'Y-m-d') { + return date_format_translation($date, $pattern); +} diff --git a/composer.json b/composer.json index e29ca9c..e4899d7 100644 --- a/composer.json +++ b/composer.json @@ -56,10 +56,10 @@ "require": { "php": ">=5.6.0", "ext-pdo": "*", + "ext-intl": "*", "smarty/smarty": "^3.1", "phpmailer/phpmailer": "~6.0", "ircmaxell/password-compat": "dev-master", - "roave/security-advisories": "dev-master", "erusev/parsedown": "^1.7", "egulias/email-validator": "~2.1", "doctrine/dbal": "^2.5", @@ -70,7 +70,8 @@ "require-dev": { "phpunit/phpunit": "^5.7", - "friendsofphp/php-cs-fixer": "~2.0" + "friendsofphp/php-cs-fixer": "~2.0", + "roave/security-advisories": "dev-master" }, "autoload": { diff --git a/create_classic_poll.php b/create_classic_poll.php index 00c8646..7ec210f 100644 --- a/create_classic_poll.php +++ b/create_classic_poll.php @@ -41,7 +41,7 @@ $sessionService = new SessionService(); if (is_file('bandeaux_local.php')) { include_once('bandeaux_local.php'); } else { - include_once('bandeaux.php'); + include_once 'bandeaux.php'; } $max_expiry_time = $pollService->maxExpiryDate(); @@ -142,7 +142,7 @@ switch ($step) { } $summary .= ''; - $end_date_str = utf8_encode(strftime($date_format['txt_date'], $max_expiry_time)); //textual date + $end_date_str = date_format_intl($max_expiry_time); //textual date $_SESSION['form'] = serialize($form); diff --git a/create_date_poll.php b/create_date_poll.php index 8ec2731..ae03ee7 100644 --- a/create_date_poll.php +++ b/create_date_poll.php @@ -166,7 +166,8 @@ switch ($step) { $choices = $form->getChoices(); foreach ($choices as $choice) { /** @var Choice $choice */ - $summary .= '
  • ' . strftime($date_format['txt_full'], $choice->getName()); + $date = (new DateTime())->setTimestamp((int) $choice->getName()); + $summary .= '
  • ' . $end_date_str = date_format_intl($date); //textual date $first = true; foreach ($choice->getSlots() as $slots) { $summary .= $first ? ': ' : ', '; @@ -177,7 +178,7 @@ switch ($step) { } $summary .= ''; - $end_date_str = utf8_encode(strftime($date_format['txt_date'], $max_expiry_time)); // textual date + $end_date_str = date_format_intl($max_expiry_time); //textual date $_SESSION['form'] = serialize($form); diff --git a/exportcsv.php b/exportcsv.php index f2d08e5..df0f112 100644 --- a/exportcsv.php +++ b/exportcsv.php @@ -80,7 +80,7 @@ if ($poll->format === 'D') { $titles_line = ','; $moments_line = ','; foreach ($slots as $slot) { - $title = Utils::csvEscape(strftime($date_format['txt_date'], $slot->title)); + $title = Utils::csvEscape($dateFormatter->format($slot->title)); $moments = explode(',', $slot->moments); $titles_line .= str_repeat($title . ',', count($moments)); diff --git a/js/app/studs.js b/js/app/studs.js index 45ef0a6..198830f 100644 --- a/js/app/studs.js +++ b/js/app/studs.js @@ -218,7 +218,7 @@ function checkCommentSending() { button.data("textSend", button.text()); } - if (!form.get(0).checkValidity()) { + if (form.get(0) && !form.get(0).checkValidity()) { button.prop("disabled", true); button.text(button.data("textWait")); } else { diff --git a/studs.php b/studs.php index fb9f970..f391ed6 100644 --- a/studs.php +++ b/studs.php @@ -232,12 +232,15 @@ if ($resultPubliclyVisible || $accessGranted) { $comments = $pollService->allCommentsByPollId($poll_id); } +$deletion_date = clone $poll->end_date; +$deletion_date->add(new DateInterval('P' . PURGE_DELAY . 'D')); + // Assign data to template $smarty->assign('poll_id', $poll_id); $smarty->assign('poll', $poll); $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('expired', $poll->end_date < new DateTime()); +$smarty->assign('deletion_date', $deletion_date); $smarty->assign('slots', $poll->format === 'D' ? $pollService->splitSlots($slots) : $slots); $smarty->assign('slots_hash', $pollService->hashSlots($slots)); $smarty->assign('votes', $pollService->splitVotes($votes)); diff --git a/tpl/admin/polls.tpl b/tpl/admin/polls.tpl index 81355aa..f2a0067 100644 --- a/tpl/admin/polls.tpl +++ b/tpl/admin/polls.tpl @@ -93,10 +93,10 @@ {$poll->admin_name|html} {$poll->admin_mail|html} - {if strtotime($poll->end_date) > time()} - {date('d/m/y', strtotime($poll->end_date))} + {if $poll->end_date > date_create()} + {$poll->end_date|date_format_intl:'d/m/Y'} {else} - {strtotime($poll->end_date)|date_format:'d/m/Y'} + {$poll->end_date|date_format_intl:'d/m/Y'} {/if} {$poll->votes|html} {$poll->id|html} diff --git a/tpl/create_date_poll_step_2.tpl b/tpl/create_date_poll_step_2.tpl index e53b4b2..f4b4f0d 100644 --- a/tpl/create_date_poll_step_2.tpl +++ b/tpl/create_date_poll_step_2.tpl @@ -39,7 +39,7 @@
    {foreach $choices as $i=>$choice} {if $choice->getName()} - {$day_value = $choice->getName()|date_format:$date_format['txt_date']} + {$day_value = $choice->getName()|timestamp_to_date|date_format_translation} {else} {$day_value = ''} {/if} diff --git a/tpl/mail/find_polls.tpl b/tpl/mail/find_polls.tpl index d3b55d5..eee41ea 100644 --- a/tpl/mail/find_polls.tpl +++ b/tpl/mail/find_polls.tpl @@ -3,7 +3,7 @@ {foreach $polls as $poll}
  • {$poll->title|html} - ({__('Generic', 'Creation date:')} {$poll->creation_date|date_format:$date_format['txt_full']}) + ({__('Generic', 'Creation date:')} {$poll->creation_date|date_format_intl:DATE_FORMAT_FULL})
  • {/foreach} diff --git a/tpl/part/comments_list.tpl b/tpl/part/comments_list.tpl index ac88fb1..3818e9e 100644 --- a/tpl/part/comments_list.tpl +++ b/tpl/part/comments_list.tpl @@ -7,7 +7,9 @@ {if $admin && !$expired} {/if} - {$comment->date|date_format:$date_format['txt_datetime_short']} +{* {$comment->date}*} +{* {$comment->date|date_format_intl}*} + {$comment->date|date_format_intl:DATE_FORMAT_SHORT} {$comment->name|html}  {$comment->comment|escape|nl2br} diff --git a/tpl/part/poll_info.tpl b/tpl/part/poll_info.tpl index 3123786..051c08a 100644 --- a/tpl/part/poll_info.tpl +++ b/tpl/part/poll_info.tpl @@ -116,15 +116,37 @@
    -

    {$poll->end_date|date_format:$date_format['txt_date']|html}

    +

    {$poll->end_date|date_format_intl:DATE_FORMAT_FULL|html} + +

    diff --git a/tpl/part/vote_table_date.tpl b/tpl/part/vote_table_date.tpl index 6ad8c51..a5dccca 100644 --- a/tpl/part/vote_table_date.tpl +++ b/tpl/part/vote_table_date.tpl @@ -26,16 +26,16 @@ {foreach $slots as $slot} {foreach $slot->moments as $id=>$moment} - + title="{__('adminstuds', 'Remove column')} {$slot->day|date_format_intl:DATE_FORMAT_SHORT|html} - {$moment|html}"> {__('Generic', 'Remove')} {if $poll->collect_users_mail != constant("Framadate\CollectMail::NO_COLLECT")} + title="{__('adminstuds', 'Collect the emails of the polled users for the choice')} {$slot->day|date_format_intl:DATE_FORMAT_SHORT|html} - {$moment|html}"> {__('Generic', 'Collect emails')} {/if} @@ -56,7 +56,7 @@ {$count_same = 0} {$previous = 0} {foreach $slots as $id=>$slot} - {$display = $slot->day|date_format:$date_format.txt_month_year|html} + {$display = $slot->day|date_format_intl:DATE_FORMAT_MONTH_YEAR|html} {if $previous !== 0 && $previous != $display} {$previous} {$count_same = 0} @@ -79,7 +79,7 @@ {foreach $slots as $id=>$slot} - {$slot->day|date_format:$date_format.txt_day|html} + {$slot->day|date_format_intl:DATE_FORMAT_DAY|html} {for $foo=0 to ($slot->moments|count)-1} {append var='headersD' value=$id} {/for} @@ -95,7 +95,7 @@ {$moment|html} {append var='headersH' value=$headersDCount} {$headersDCount = $headersDCount+1} - {$slots_raw[] = $slot->day|date_format:$date_format.txt_full|cat:' - '|cat:$moment} + {$slots_raw[] = $slot->day|date_format_intl:DATE_FORMAT_FULL|cat:' - '|cat:$moment} {/foreach} {/foreach} @@ -258,12 +258,12 @@
      - {if $poll->valuemax eq NULL || $best_choices['y'][$i] lt $poll->valuemax} + {if $poll->ValueMax eq NULL || $best_choices['y'][$i] lt $poll->ValueMax}
    • -
    • @@ -271,7 +271,7 @@ -