diff --git a/app/code/Magento/Security/Model/ResourceModel/UserExpiration.php b/app/code/Magento/Security/Model/ResourceModel/UserExpiration.php index 71a331a178006..240dda2f0dfb9 100644 --- a/app/code/Magento/Security/Model/ResourceModel/UserExpiration.php +++ b/app/code/Magento/Security/Model/ResourceModel/UserExpiration.php @@ -7,10 +7,19 @@ namespace Magento\Security\Model\ResourceModel; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\Stdlib\DateTime\Timezone\LocalizedDateToUtcConverterInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Security\Model\UserExpiration as UserExpirationModel; + /** * Admin User Expiration resource model */ -class UserExpiration extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +class UserExpiration extends AbstractDb { /** @@ -21,24 +30,31 @@ class UserExpiration extends \Magento\Framework\Model\ResourceModel\Db\AbstractD protected $_isPkAutoIncrement = false; /** - * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface + * @var TimezoneInterface */ private $timezone; /** - * UserExpiration constructor. - * - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timezone - * @param string $connectionName + * @var LocalizedDateToUtcConverterInterface + */ + private $localizedDateToUtcConverter; + + /** + * @param Context $context + * @param TimezoneInterface $timezone + * @param string|null $connectionName + * @param LocalizedDateToUtcConverterInterface|null $localizedDateToUtcConverter */ public function __construct( - \Magento\Framework\Model\ResourceModel\Db\Context $context, - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timezone, - ?string $connectionName = null + Context $context, + TimezoneInterface $timezone, + ?string $connectionName = null, + ?LocalizedDateToUtcConverterInterface $localizedDateToUtcConverter = null ) { parent::__construct($context, $connectionName); $this->timezone = $timezone; + $this->localizedDateToUtcConverter = $localizedDateToUtcConverter ?: ObjectManager::getInstance() + ->get(LocalizedDateToUtcConverterInterface::class); } /** @@ -54,15 +70,17 @@ protected function _construct() /** * Convert to UTC time. * - * @param \Magento\Framework\Model\AbstractModel $userExpiration + * @param AbstractModel $userExpiration * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ - protected function _beforeSave(\Magento\Framework\Model\AbstractModel $userExpiration) + protected function _beforeSave(AbstractModel $userExpiration) { - /** @var $userExpiration \Magento\Security\Model\UserExpiration */ + /** @var $userExpiration UserExpirationModel */ $expiresAt = $userExpiration->getExpiresAt(); - $utcValue = $this->timezone->convertConfigTimeToUtc($expiresAt); + $utcValue = strtotime($expiresAt) + ? $this->timezone->convertConfigTimeToUtc($expiresAt) + : $this->localizedDateToUtcConverter->convertLocalizedDateToUtc($expiresAt); $userExpiration->setExpiresAt($utcValue); return $this; @@ -71,15 +89,16 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $userExpir /** * Convert to store time. * - * @param \Magento\Framework\Model\AbstractModel $userExpiration - * @return $this|\Magento\Framework\Model\ResourceModel\Db\AbstractDb + * @param AbstractModel $userExpiration + * @return $this|AbstractDb * @throws \Exception */ - protected function _afterLoad(\Magento\Framework\Model\AbstractModel $userExpiration) + protected function _afterLoad(AbstractModel $userExpiration) { - /** @var $userExpiration \Magento\Security\Model\UserExpiration */ + /** @var $userExpiration UserExpirationModel */ if ($userExpiration->getExpiresAt()) { - $storeValue = $this->timezone->date($userExpiration->getExpiresAt()); + $date = new \DateTime($userExpiration->getExpiresAt()); + $storeValue = $this->timezone->date($date); $userExpiration->setExpiresAt($storeValue->format('Y-m-d H:i:s')); } diff --git a/app/code/Magento/Security/Model/UserExpiration/Validator.php b/app/code/Magento/Security/Model/UserExpiration/Validator.php index 62dbd7852ff33..ae46e2c3cf2f6 100644 --- a/app/code/Magento/Security/Model/UserExpiration/Validator.php +++ b/app/code/Magento/Security/Model/UserExpiration/Validator.php @@ -7,7 +7,9 @@ namespace Magento\Security\Model\UserExpiration; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Framework\Stdlib\DateTime\Timezone\LocalizedDateToUtcConverterInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\Validator\AbstractValidator; @@ -16,25 +18,35 @@ */ class Validator extends AbstractValidator { - - /**@var TimezoneInterface */ + /** + * @var TimezoneInterface + */ private $timezone; - /**@var DateTime */ + /** + * @var DateTime + */ private $dateTime; /** - * Validator constructor. - * + * @var LocalizedDateToUtcConverterInterface + */ + private $localizedDateToUtcConverter; + + /** * @param TimezoneInterface $timezone * @param DateTime $dateTime + * @param LocalizedDateToUtcConverterInterface|null $localizedDateToUtcConverter */ public function __construct( TimezoneInterface $timezone, - DateTime $dateTime + DateTime $dateTime, + ?LocalizedDateToUtcConverterInterface $localizedDateToUtcConverter = null ) { $this->timezone = $timezone; $this->dateTime = $dateTime; + $this->localizedDateToUtcConverter = $localizedDateToUtcConverter ?: ObjectManager::getInstance() + ->get(LocalizedDateToUtcConverterInterface::class); } /** @@ -48,18 +60,13 @@ public function isValid($value) { $this->_clearMessages(); $messages = []; - $expiresAt = $value; $label = 'Expiration date'; - if (\Zend_Validate::is($expiresAt, 'NotEmpty')) { - if (strtotime($expiresAt)) { - $currentTime = $this->dateTime->gmtTimestamp(); - $utcExpiresAt = $this->timezone->convertConfigTimeToUtc($expiresAt); - $expiresAt = $this->timezone->date($utcExpiresAt)->getTimestamp(); - if ($expiresAt < $currentTime) { - $messages['expires_at'] = __('"%1" must be later than the current date.', $label); - } - } else { - $messages['expires_at'] = __('"%1" is not a valid date.', $label); + if (\Zend_Validate::is($value, 'NotEmpty')) { + $utcExpiresAt = $this->localizedDateToUtcConverter->convertLocalizedDateToUtc($value); + $currentTime = $this->dateTime->gmtTimestamp(); + $expiresAt = $this->timezone->date($utcExpiresAt)->getTimestamp(); + if ($expiresAt < $currentTime) { + $messages['expires_at'] = __('"%1" must be later than the current date.', $label); } } $this->_addMessages($messages); diff --git a/app/code/Magento/Security/Test/Unit/Model/UserExpiration/ValidatorTest.php b/app/code/Magento/Security/Test/Unit/Model/UserExpiration/ValidatorTest.php deleted file mode 100644 index a3ffed69ca9b0..0000000000000 --- a/app/code/Magento/Security/Test/Unit/Model/UserExpiration/ValidatorTest.php +++ /dev/null @@ -1,102 +0,0 @@ -dateTimeMock = - $this->createPartialMock(\Magento\Framework\Stdlib\DateTime\DateTime::class, ['gmtTimestamp']); - $this->timezoneMock = - $this->createPartialMock( - Timezone::class, - ['date', 'convertConfigTimeToUtc'] - ); - $this->validator = $objectManager->getObject( - Validator::class, - ['dateTime' => $this->dateTimeMock, 'timezone' => $this->timezoneMock] - ); - } - - public function testWithInvalidDate() - { - $expireDate = 'invalid_date'; - $this->assertFalse($this->validator->isValid($expireDate)); - $this->assertStringContainsString( - '"Expiration date" is not a valid date.', - (string)current($this->validator->getMessages()) - ); - } - - public function testWithPastDate() - { - /** @var \DateTime|MockObject $dateObject */ - $dateObject = $this->createMock(\DateTime::class); - $this->timezoneMock->expects(static::once()) - ->method('date') - ->willReturn($dateObject); - - $currentDate = new \DateTime(); - $currentDate = $currentDate->getTimestamp(); - $expireDate = new \DateTime(); - $expireDate->modify('-10 days'); - - $this->dateTimeMock->expects(static::once())->method('gmtTimestamp')->willReturn($currentDate); - $this->timezoneMock->expects(static::once())->method('date')->willReturn($expireDate); - $dateObject->expects(static::once())->method('getTimestamp')->willReturn($expireDate->getTimestamp()); - $this->assertFalse($this->validator->isValid($expireDate->format('Y-m-d H:i:s'))); - $this->assertStringContainsString( - '"Expiration date" must be later than the current date.', - (string)current($this->validator->getMessages()) - ); - } - - public function testWithFutureDate() - { - /** @var \DateTime|MockObject $dateObject */ - $dateObject = $this->createMock(\DateTime::class); - $this->timezoneMock->expects(static::once()) - ->method('date') - ->willReturn($dateObject); - $currentDate = new \DateTime(); - $currentDate = $currentDate->getTimestamp(); - $expireDate = new \DateTime(); - $expireDate->modify('+10 days'); - - $this->dateTimeMock->expects(static::once())->method('gmtTimestamp')->willReturn($currentDate); - $this->timezoneMock->expects(static::once())->method('date')->willReturn($expireDate); - $dateObject->expects(static::once())->method('getTimestamp')->willReturn($expireDate->getTimestamp()); - static::assertTrue($this->validator->isValid($expireDate->format('Y-m-d H:i:s'))); - static::assertEquals([], $this->validator->getMessages()); - } -} diff --git a/dev/tests/integration/testsuite/Magento/Security/Model/ResourceModel/UserExpirationTest.php b/dev/tests/integration/testsuite/Magento/Security/Model/ResourceModel/UserExpirationTest.php new file mode 100644 index 0000000000000..72e76535a3153 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/Model/ResourceModel/UserExpirationTest.php @@ -0,0 +1,97 @@ +userExpirationResource = Bootstrap::getObjectManager()->get(UserExpiration::class); + } + + /** + * Verify user expiration saved with correct date. + * + * @magentoDataFixture Magento/User/_files/dummy_user.php + * @dataProvider userExpirationSaveDataProvider + * @magentoAppArea adminhtml + * @return void + */ + public function testUserExpirationSave(string $locale): void + { + $localeResolver = Bootstrap::getObjectManager()->get(ResolverInterface::class); + $timeZone = Bootstrap::getObjectManager()->get(TimezoneInterface::class); + $localeResolver->setLocale($locale); + $initialExpirationDate = $timeZone->date()->modify('+10 day'); + $expireDate = $timeZone->formatDateTime( + $initialExpirationDate, + \IntlDateFormatter::MEDIUM, + \IntlDateFormatter::MEDIUM + ); + $userExpirationFactory = Bootstrap::getObjectManager()->get(UserExpirationFactory::class); + $userExpiration = $userExpirationFactory->create(); + $userExpiration->setExpiresAt($expireDate); + $userExpiration->setUserId($this->getUserId()); + $this->userExpirationResource->save($userExpiration); + $loadedUserExpiration = $userExpirationFactory->create(); + $this->userExpirationResource->load($loadedUserExpiration, $userExpiration->getId()); + + self::assertEquals($initialExpirationDate->format('Y-m-d H:i:s'), $loadedUserExpiration->getExpiresAt()); + } + + /** + * Provides locale codes for validation test. + * + * @return array + */ + public function userExpirationSaveDataProvider(): array + { + return [ + 'default' => [ + 'locale_code' => 'en_US', + ], + 'non_default_english_textual' => [ + 'locale_code' => 'de_DE', + ], + 'non_default_non_english_textual' => [ + 'locale_code' => 'uk_UA', + ], + ]; + } + + /** + * Retrieve user id from db. + * + * @return int + */ + private function getUserId(): int + { + $userResource = Bootstrap::getObjectManager()->get(UserResource::class); + $data = $userResource->loadByUsername('dummy_username'); + + return (int)$data['user_id']; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Security/Model/UserExpiration/ValidatorTest.php b/dev/tests/integration/testsuite/Magento/Security/Model/UserExpiration/ValidatorTest.php new file mode 100644 index 0000000000000..a96e2ae641616 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/Model/UserExpiration/ValidatorTest.php @@ -0,0 +1,87 @@ +validator = Bootstrap::getObjectManager()->get(Validator::class); + $this->localeResolver = Bootstrap::getObjectManager()->get(ResolverInterface::class); + $this->timeZone = Bootstrap::getObjectManager()->get(TimezoneInterface::class); + } + + /** + * Verify validation for date expiration with different locales. + * + * @magentoAppArea adminhtml + * @dataProvider validateUserExpiresAtDataProvider + * @return void + */ + public function testValidateUserExpiresAt(string $locale): void + { + $this->localeResolver->setLocale($locale); + $date = $this->timeZone->date()->modify('+10 day'); + $expireDate = $this->timeZone->formatDateTime($date, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM); + self::assertTrue($this->validator->isValid($expireDate)); + + $date = $this->timeZone->date()->modify('-10 day'); + $expireDate = $this->timeZone->formatDateTime($date, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM); + self::assertFalse($this->validator->isValid($expireDate)); + self::assertEquals( + $this->validator->getMessages(), + ['expires_at' => __('"%1" must be later than the current date.', 'Expiration date')] + ); + } + + /** + * Provides locale codes for validation test. + * + * @return array + */ + public function validateUserExpiresAtDataProvider(): array + { + return [ + 'default' => [ + 'locale_code' => 'en_US', + ], + 'non_default_english_textual' => [ + 'locale_code' => 'de_DE', + ], + 'non_default_non_english_textual' => [ + 'locale_code' => 'uk_UA', + ], + ]; + } +} diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone/LocalizedDateToUtcConverter.php b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone/LocalizedDateToUtcConverter.php index 420fd6e543e07..22a37902ebcd4 100644 --- a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone/LocalizedDateToUtcConverter.php +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone/LocalizedDateToUtcConverter.php @@ -7,12 +7,11 @@ namespace Magento\Framework\Stdlib\DateTime\Timezone; -use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\Locale\ResolverInterface; -use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** - * Class LocalizedDateToUtcConverter + * Localized date to UTC converter. */ class LocalizedDateToUtcConverter implements LocalizedDateToUtcConverterInterface { @@ -34,8 +33,6 @@ class LocalizedDateToUtcConverter implements LocalizedDateToUtcConverterInterfac private $localeResolver; /** - * LocalizedDateToUtcConverter constructor. - * * @param TimezoneInterface $timezone * @param ResolverInterface $localeResolver */ @@ -65,9 +62,7 @@ public function convertLocalizedDateToUtc($date) $localTimestamp = $formatter->parse($date); $gmtTimestamp = $this->timezone->date($localTimestamp)->getTimestamp(); $formattedUniversalTime = date($this->defaultFormat, $gmtTimestamp); - - $date = new \DateTime($formattedUniversalTime, new \DateTimeZone($configTimezone)); - $date->setTimezone(new \DateTimeZone('UTC')); + $date = new \DateTime($formattedUniversalTime); return $date->format($this->defaultFormat); }