diff --git a/libraries/src/Form/Field/CalendarField.php b/libraries/src/Form/Field/CalendarField.php index 428ca5a236f63..900ee2851c2f5 100644 --- a/libraries/src/Form/Field/CalendarField.php +++ b/libraries/src/Form/Field/CalendarField.php @@ -11,8 +11,10 @@ \defined('JPATH_PLATFORM') or die; use DateTime; +use DateTimeImmutable; use Joomla\CMS\Factory; use Joomla\CMS\Form\FormField; +use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\Registry\Registry; @@ -274,7 +276,8 @@ protected function getInput() { $tz = date_default_timezone_get(); date_default_timezone_set('UTC'); - $this->value = strftime($this->format, strtotime($this->value)); + $date = DateTimeImmutable::createFromFormat('U', strtotime($this->value)); + $this->value = $date->format(HTMLHelper::strftimeFormatToDateFormat($this->format)); date_default_timezone_set($tz); } else diff --git a/libraries/src/HTML/HTMLHelper.php b/libraries/src/HTML/HTMLHelper.php index 92893b35facb0..d2984d811a456 100644 --- a/libraries/src/HTML/HTMLHelper.php +++ b/libraries/src/HTML/HTMLHelper.php @@ -1072,7 +1072,7 @@ public static function tooltipText($title = '', $content = '', $translate = true * @param string $value The date value * @param string $name The name of the text field * @param string $id The id of the text field - * @param string $format The date format + * @param string $format The date format using the @deprecated strftime format parameters * @param mixed $attribs Additional HTML attributes * The array can have the following keys: * readonly Sets the readonly parameter for the input tag @@ -1085,6 +1085,7 @@ public static function tooltipText($title = '', $content = '', $translate = true * * @since 1.5 * + * @note This method uses deprecated strftime format parameters for backward compatibility. */ public static function calendar($value, $name, $id, $format = '%Y-%m-%d', $attribs = array()) { @@ -1131,7 +1132,8 @@ public static function calendar($value, $name, $id, $format = '%Y-%m-%d', $attri { $tz = date_default_timezone_get(); date_default_timezone_set('UTC'); - $inputvalue = strftime($format, strtotime($value)); + $date = \DateTimeImmutable::createFromFormat('U', strtotime($value)); + $inputvalue = $date->format(self::strftimeFormatToDateFormat($format)); date_default_timezone_set($tz); } else @@ -1284,4 +1286,63 @@ private static function checkFileOrder($first, $second) return ''; } + + /** + * Convert strftime format to php date format as strftime is deprecated and we have + * to be able to provide same backward compatibility with existing format strings. + * + * @param $strftimeformat string The format compatible with strftime. + * + * @return string The format compatible with PHP's Date functions. + * + * @since __DEPLOY_VERSION__ + * + * @throws \Exception + * + * @note Thanks to @relipse for https://stackoverflow.com/questions/22665959/using-php-strftime-using-date-format-string/62781773#62781773 + */ + public static function strftimeFormatToDateFormat(string $strftimeformat): string + { + $unsupported = ['%U', '%V', '%C', '%g', '%G']; + $foundunsupported = []; + + foreach ($unsupported as $unsup) + { + if (strpos($strftimeformat, $unsup) !== false) + { + $foundunsupported[] = $unsup; + } + } + + if (!empty($foundunsupported)) + { + throw new \Exception("Found these unsupported chars: " . implode(",", $foundunsupported) . ' in ' . $strftimeformat); + } + + /** + * It is important to note that some do not translate accurately + * ie. lowercase L is supposed to convert to number with a preceding space if it is under 10, + * there is no accurate conversion, so we just use 'g' + */ + $phpdateformat = str_replace( + ['%a','%A','%d','%e','%u','%w','%W','%b','%h','%B','%m','%y','%Y', '%D', '%F', '%x', '%n', '%t', '%H', '%k', '%I', '%l', '%M', '%p', '%P', + // %I:%M:%S %p + '%r', + // %H:%M + '%R', + '%S', + // %H:%M:%S + '%T', + '%X', '%z', '%Z', '%c', '%s', '%%' + ], + ['D','l', 'd', 'j', 'N', 'w', 'W', 'M', 'M', 'F', 'm', 'y', 'Y', 'm/d/y', 'Y-m-d', 'm/d/y', "\n", "\t", 'H', 'G', 'h', 'g', 'i', 'A', 'a', 'h:i:s A', 'H:i', 's', 'H:i:s', 'H:i:s', 'O', 'T', + // Tue Feb 5 00:45:10 2009 + 'D M j H:i:s Y' , + 'U', '%' + ], + $strftimeformat + ); + + return $phpdateformat; + } } diff --git a/tests/Unit/Libraries/Cms/Html/HtmlHelperTest.php b/tests/Unit/Libraries/Cms/Html/HtmlHelperTest.php new file mode 100644 index 0000000000000..2f547d7b84763 --- /dev/null +++ b/tests/Unit/Libraries/Cms/Html/HtmlHelperTest.php @@ -0,0 +1,61 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Tests\Unit\Libraries\Cms\Html; + +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\Tests\Unit\UnitTestCase; + +/** + * Test class for HtmlHelperTest. + * + * @since __DEPLOY_VERSION__ + */ +class HtmlHelperTest extends UnitTestCase +{ + /** + * @var string Base HTML Output with place holders + * + * @since __DEPLOY_VERSION__ + */ + private $template = '
+
+ + +
+
+'; + /** + * Test the replacement of using deprecated strftime with Date formats + * + * @since __DEPLOY_VERSION__ + */ + public function testCalendar() + { + $this->assertEquals( + sprintf($this->template, 'testId', 'testName', 'Mar', 'Mar', 'testId', '%b'), + HTMLHelper::calendar('1978-03-08 06:12:12', 'testName', 'testId', '%b', []) + ); + + $this->assertEquals( + sprintf($this->template, 'testId', 'testName', '1978-03-08', '1978-03-08', 'testId', '%Y-%m-%d'), + HTMLHelper::calendar('1978-03-08 06:12:12', 'testName', 'testId', '%Y-%m-%d', []) + ); + } +} diff --git a/tests/Unit/UnitTestCase.php b/tests/Unit/UnitTestCase.php index 5a40be2017dd7..ec64efecda803 100644 --- a/tests/Unit/UnitTestCase.php +++ b/tests/Unit/UnitTestCase.php @@ -8,6 +8,17 @@ */ namespace Joomla\Tests\Unit; +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Document\Document; +use Joomla\CMS\Document\FactoryInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Language; +use Joomla\CMS\WebAsset\WebAssetManager; +use Joomla\Input\Input; +use Joomla\Registry\Registry; +use ReflectionClass; +use stdClass; + /** * Base Unit Test case for common behaviour across unit tests * @@ -15,4 +26,71 @@ */ abstract class UnitTestCase extends \PHPUnit\Framework\TestCase { + /** + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function setUp(): void + { + $this->initJoomla(); + } + + /** + * Sets up the minimal amount of Joomla, mocked where possible, to run the + * CMS Unit Test Cases + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function initJoomla(): void + { + // Start with the easy mocks. + $input = $this->getMockBuilder(Input::class) + ->getMockForAbstractClass(); + $factory = $this->createMock(Factory::class); + $lang = $this->getMockBuilder(Language::class) + ->getMockForAbstractClass(); + + // Mock the Document object. + $doc = new Document( + [ + 'factory' => $this->createMock(FactoryInterface::class), + ] + ); + + // Mock WA and some calls used that we are not directly testing. + $wa = $this->createMock(WebAssetManager::class); + $wa->expects($this->any())->method('__call')->will($this->returnValue($wa)); + $doc->setWebAssetManager($wa); + + // Inject the mocked document in the mocked factory. + $factory::$document = $doc; + + // Mock a template that the app will return from getTemplate(). + $template = new stdClass; + $template->template = 'system'; + $template->params = new Registry; + $template->inheritable = 0; + $template->parent = ''; + + // Ensure the application can return all our mocked items. + $app = $this->createMock(CMSApplication::class); + $app->method('__get') + ->with('input') + ->willReturn($input); + $app->method('getLanguage') + ->will($this->returnValue($lang)); + $app->method('getDocument') + ->will($this->returnValue($factory::$document)); + $app->method('getTemplate') + ->will($this->returnValue($template)); + + // Finally, set the application into the factory. + $reflection = new ReflectionClass($factory); + $reflection_property = $reflection->getProperty('application'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($factory, $app); + } }