Fix overly lenient date validation (#4574)

* Fix overly lenient date validation

- In the past date validation would roll over dates that didn't exist into the next month.  Now they return an error.

Signed-off-by: objec <objecttothis@gmail.com>

* Refactor naming

- Refactor parameter
- Refactor function name.

Signed-off-by: objec <objecttothis@gmail.com>

* Add unit tests for LocaleHelper

Signed-off-by: objec <objecttothis@gmail.com>

* Remove files from being tracked.

Signed-off-by: objec <objecttothis@gmail.com>

---------

Signed-off-by: objec <objecttothis@gmail.com>
This commit is contained in:
objecttothis
2026-06-10 23:16:25 +04:00
committed by GitHub
parent 84aeeb52fe
commit e6388deed8
9 changed files with 66 additions and 162 deletions

1
.gitignore vendored
View File

@@ -56,6 +56,7 @@ $RECYCLE.BIN/
.com.apple.timemachine.donotpresent
# Other
build/
generate_languages.php
dist
docs

View File

@@ -1239,7 +1239,7 @@ class Items extends Secure_Controller
}
break;
case DATE:
if (!valid_date($attributeValue) && !empty($attributeValue)) {
if (!isValidDate($attributeValue) && !empty($attributeValue)) {
log_message('error', "'$attributeValue' is not an acceptable DATE value. The value must match the set locale.");
return true;
}

View File

@@ -639,13 +639,14 @@ function dateformat_bootstrap(string $php_format): string
}
/**
* @param string $date
* @param string $candidate
* @return bool
*/
function valid_date(string $date): bool // TODO: need a better name for $date. Perhaps $candidate. Also the function name would be better as is_valid_date()
function isValidDate(string $candidate): bool
{
$config = config(OSPOS::class)->settings;
return (DateTime::createFromFormat($config['dateformat'], $date));
$parsed = DateTime::createFromFormat($config['dateformat'], $candidate);
return $parsed !== false && $parsed->format($config['dateformat']) === $candidate;
}
/**

View File

@@ -388,7 +388,7 @@ class Attribute extends Model
foreach ($builder->get()->getResult() as $attribute) {
switch ($to) {
case DATE:
$success = valid_date($attribute->attribute_value);
$success = isValidDate($attribute->attribute_value);
break;
case DECIMAL:
$success = valid_decimal($attribute->attribute_value);

View File

@@ -1 +0,0 @@
{"version":2,"defects":{"Token_libTest::testRenderHandlesSpecialCharacters":8,"Token_libTest::testRenderHandlesUnicode":8,"Token_libTest::testRenderHandlesNewLines":8,"Token_libTest::testRenderHandlesTabs":8,"Token_libTest::testRenderHandlesDateAtStart":8,"Token_libTest::testRenderHandlesSqlInjectionAttempt":8,"Token_libTest::testRenderHandlesVeryLongStringWithDate":8,"Token_libTest::testRenderHandlesMultipleDates":8,"Token_libTest::testRenderDoesNotReplaceInvalidFormatSpecifiers":7},"times":{"Token_libTest::testRenderReturnsInputStringWhenNoTokens":0.002,"Token_libTest::testRenderHandlesStringWithPercentNotInDateFormat":0.004,"Token_libTest::testRenderHandlesInvalidDateFormatPercentDashPercent":0.001,"Token_libTest::testRenderHandlesInvalidDateFormatPercentYPercentQPercentBad":0,"Token_libTest::testRenderHandlesStringWithPercentAPercent":0,"Token_libTest::testRenderHandlesExtremelyLongString":0,"Token_libTest::testRenderHandlesStringWithMultiplePercentSymbols":0,"Token_libTest::testRenderHandlesStringWithOnlyPercentSymbol":0,"Token_libTest::testRenderPreservesTextWithValidDateTokensAndNoOtherTokens":0,"Token_libTest::testRenderHandlesEmptyString":0,"Token_libTest::testScanExtractsTokens":0,"Token_libTest::testScanExtractsTokensWithLength":0,"Token_libTest::testScanReturnsEmptyArrayForNoTokens":0,"Token_libTest::testRenderHandlesConsecutivePercentSigns":0,"Token_libTest::testRenderHandlesEscapedPercentSigns":0,"Token_libTest::testRenderHandlesSpecialCharacters":0.005,"Token_libTest::testRenderHandlesUnicode":0,"Token_libTest::testRenderHandlesNewLines":0,"Token_libTest::testRenderHandlesTabs":0,"Token_libTest::testRenderHandlesUnclosedBraces":0,"Token_libTest::testRenderHandlesUnopenedBraces":0,"Token_libTest::testRenderHandlesDateAtStart":0,"Token_libTest::testRenderHandlesSqlInjectionAttempt":0,"Token_libTest::testRenderHandlesVeryLongStringWithDate":0,"Token_libTest::testRenderHandlesMultipleDates":0,"Token_libTest::testRenderHandlesValidYearFormat":0,"Token_libTest::testRenderHandlesValidMonthFormat":0,"Token_libTest::testRenderHandlesValidDayFormat":0,"Token_libTest::testRenderHandlesFullDateFormat":0,"Token_libTest::testRenderHandlesPercentB":0,"Token_libTest::testRenderHandlesPercentA":0,"Token_libTest::testRenderHandlesComplexPercentFormat":0,"Token_libTest::testRenderDoesNotReplaceInvalidFormatSpecifiers":0,"Token_libTest::testScanWorksWithMixedContent":0,"Token_libTest::testRenderReplacesTimezoneFormat":0,"Tests\\Libraries\\Token_libTest::testRenderReturnsInputStringWhenNoTokens":0.001,"Tests\\Libraries\\Token_libTest::testRenderHandlesStringWithPercentNotInDateFormat":0.004,"Tests\\Libraries\\Token_libTest::testRenderHandlesInvalidDateFormatPercentDashPercent":0.001,"Tests\\Libraries\\Token_libTest::testRenderHandlesInvalidDateFormatPercentYPercentQPercentBad":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesStringWithPercentAPercent":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesExtremelyLongString":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesStringWithMultiplePercentSymbols":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesStringWithOnlyPercentSymbol":0,"Tests\\Libraries\\Token_libTest::testRenderPreservesTextWithValidDateTokensAndNoOtherTokens":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesEmptyString":0,"Tests\\Libraries\\Token_libTest::testScanExtractsTokens":0,"Tests\\Libraries\\Token_libTest::testScanExtractsTokensWithLength":0,"Tests\\Libraries\\Token_libTest::testScanReturnsEmptyArrayForNoTokens":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesConsecutivePercentSigns":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesEscapedPercentSigns":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesUnclosedBraces":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesUnopenedBraces":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesVeryLongStringWithDate":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesMultipleDates":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesValidYearFormat":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesValidMonthFormat":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesValidDayFormat":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesFullDateFormat":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesPercentB":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesPercentA":0,"Tests\\Libraries\\Token_libTest::testRenderHandlesComplexPercentFormat":0,"Tests\\Libraries\\Token_libTest::testRenderDoesNotReplaceInvalidFormatSpecifiers":0,"Tests\\Libraries\\Token_libTest::testRenderReplacesTimezoneFormat":0,"Tests\\Libraries\\Token_libTest::testScanWorksWithMixedContent":0}}

View File

@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="/app/phpunit.xml.dist" tests="29" assertions="44" errors="0" failures="0" skipped="0" time="0.029097">
<testsuite name="App" tests="29" assertions="44" errors="0" failures="0" skipped="0" time="0.029097">
<testsuite name="Tests\Libraries\Token_libTest" file="/app/tests/Libraries/Token_libTest.php" tests="29" assertions="44" errors="0" failures="0" skipped="0" time="0.029097">
<testcase name="testRenderReturnsInputStringWhenNoTokens" file="/app/tests/Libraries/Token_libTest.php" line="18" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.007667"/>
<testcase name="testRenderHandlesStringWithPercentNotInDateFormat" file="/app/tests/Libraries/Token_libTest.php" line="25" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="2" time="0.004001"/>
<testcase name="testRenderHandlesInvalidDateFormatPercentDashPercent" file="/app/tests/Libraries/Token_libTest.php" line="33" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="2" time="0.001162"/>
<testcase name="testRenderHandlesInvalidDateFormatPercentYPercentQPercentBad" file="/app/tests/Libraries/Token_libTest.php" line="41" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000766"/>
<testcase name="testRenderHandlesStringWithPercentAPercent" file="/app/tests/Libraries/Token_libTest.php" line="48" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000613"/>
<testcase name="testRenderHandlesExtremelyLongString" file="/app/tests/Libraries/Token_libTest.php" line="55" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000513"/>
<testcase name="testRenderHandlesStringWithMultiplePercentSymbols" file="/app/tests/Libraries/Token_libTest.php" line="62" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="2" time="0.000589"/>
<testcase name="testRenderHandlesStringWithOnlyPercentSymbol" file="/app/tests/Libraries/Token_libTest.php" line="70" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000578"/>
<testcase name="testRenderPreservesTextWithValidDateTokensAndNoOtherTokens" file="/app/tests/Libraries/Token_libTest.php" line="77" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000612"/>
<testcase name="testRenderHandlesEmptyString" file="/app/tests/Libraries/Token_libTest.php" line="84" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000503"/>
<testcase name="testScanExtractsTokens" file="/app/tests/Libraries/Token_libTest.php" line="91" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="2" time="0.000578"/>
<testcase name="testScanExtractsTokensWithLength" file="/app/tests/Libraries/Token_libTest.php" line="98" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="2" time="0.000501"/>
<testcase name="testScanReturnsEmptyArrayForNoTokens" file="/app/tests/Libraries/Token_libTest.php" line="105" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000513"/>
<testcase name="testRenderHandlesConsecutivePercentSigns" file="/app/tests/Libraries/Token_libTest.php" line="111" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="2" time="0.000628"/>
<testcase name="testRenderHandlesEscapedPercentSigns" file="/app/tests/Libraries/Token_libTest.php" line="119" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000626"/>
<testcase name="testRenderHandlesUnclosedBraces" file="/app/tests/Libraries/Token_libTest.php" line="126" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000617"/>
<testcase name="testRenderHandlesUnopenedBraces" file="/app/tests/Libraries/Token_libTest.php" line="133" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000620"/>
<testcase name="testRenderHandlesVeryLongStringWithDate" file="/app/tests/Libraries/Token_libTest.php" line="140" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="2" time="0.000599"/>
<testcase name="testRenderHandlesMultipleDates" file="/app/tests/Libraries/Token_libTest.php" line="148" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000593"/>
<testcase name="testRenderHandlesValidYearFormat" file="/app/tests/Libraries/Token_libTest.php" line="155" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000688"/>
<testcase name="testRenderHandlesValidMonthFormat" file="/app/tests/Libraries/Token_libTest.php" line="162" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000611"/>
<testcase name="testRenderHandlesValidDayFormat" file="/app/tests/Libraries/Token_libTest.php" line="169" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000627"/>
<testcase name="testRenderHandlesFullDateFormat" file="/app/tests/Libraries/Token_libTest.php" line="176" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="1" time="0.000757"/>
<testcase name="testRenderHandlesPercentB" file="/app/tests/Libraries/Token_libTest.php" line="183" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="3" time="0.000766"/>
<testcase name="testRenderHandlesPercentA" file="/app/tests/Libraries/Token_libTest.php" line="192" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="3" time="0.000703"/>
<testcase name="testRenderHandlesComplexPercentFormat" file="/app/tests/Libraries/Token_libTest.php" line="201" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="2" time="0.000669"/>
<testcase name="testRenderDoesNotReplaceInvalidFormatSpecifiers" file="/app/tests/Libraries/Token_libTest.php" line="209" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="2" time="0.000646"/>
<testcase name="testRenderReplacesTimezoneFormat" file="/app/tests/Libraries/Token_libTest.php" line="217" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="2" time="0.000811"/>
<testcase name="testScanWorksWithMixedContent" file="/app/tests/Libraries/Token_libTest.php" line="225" class="Tests\Libraries\Token_libTest" classname="Tests.Libraries.Token_libTest" assertions="2" time="0.000540"/>
</testsuite>
</testsuite>
</testsuite>
</testsuites>

View File

@@ -1,87 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Test Documentation</title>
<style>
body {
text-rendering: optimizeLegibility;
font-family: Source SansSerif Pro, Arial, sans-serif;
font-variant-ligatures: common-ligatures;
font-kerning: normal;
margin-left: 2rem;
background-color: #fff;
color: #000;
}
body > ul > li {
font-size: larger;
}
h2 {
font-size: larger;
text-decoration-line: underline;
text-decoration-thickness: 2px;
margin: 0;
padding: 0.5rem 0;
}
ul {
list-style: none;
margin: 0 0 2rem;
padding: 0 0 0 1rem;
text-indent: -1rem;
}
.success:before {
color: #4e9a06;
content: '✓';
padding-right: 0.5rem;
}
.defect {
color: #a40000;
}
.defect:before {
color: #a40000;
content: '✗';
padding-right: 0.5rem;
}
</style>
</head>
<body>
<h2>Token_lib (Tests\Libraries\Token_lib)</h2>
<ul>
<li class="success">Render returns input string when no tokens</li>
<li class="success">Render handles string with percent not in date format</li>
<li class="success">Render handles invalid date format percent dash percent</li>
<li class="success">Render handles invalid date format percent y percent q percent bad</li>
<li class="success">Render handles string with percent a percent</li>
<li class="success">Render handles extremely long string</li>
<li class="success">Render handles string with multiple percent symbols</li>
<li class="success">Render handles string with only percent symbol</li>
<li class="success">Render preserves text with valid date tokens and no other tokens</li>
<li class="success">Render handles empty string</li>
<li class="success">Scan extracts tokens</li>
<li class="success">Scan extracts tokens with length</li>
<li class="success">Scan returns empty array for no tokens</li>
<li class="success">Render handles consecutive percent signs</li>
<li class="success">Render handles escaped percent signs</li>
<li class="success">Render handles unclosed braces</li>
<li class="success">Render handles unopened braces</li>
<li class="success">Render handles very long string with date</li>
<li class="success">Render handles multiple dates</li>
<li class="success">Render handles valid year format</li>
<li class="success">Render handles valid month format</li>
<li class="success">Render handles valid day format</li>
<li class="success">Render handles full date format</li>
<li class="success">Render handles percent b</li>
<li class="success">Render handles percent a</li>
<li class="success">Render handles complex percent format</li>
<li class="success">Render does not replace invalid format specifiers</li>
<li class="success">Render replaces timezone format</li>
<li class="success">Scan works with mixed content</li>
</ul>
</body>
</html>

View File

@@ -1,31 +0,0 @@
Token_lib (Tests\Libraries\Token_lib)
[x] Render returns input string when no tokens
[x] Render handles string with percent not in date format
[x] Render handles invalid date format percent dash percent
[x] Render handles invalid date format percent y percent q percent bad
[x] Render handles string with percent a percent
[x] Render handles extremely long string
[x] Render handles string with multiple percent symbols
[x] Render handles string with only percent symbol
[x] Render preserves text with valid date tokens and no other tokens
[x] Render handles empty string
[x] Scan extracts tokens
[x] Scan extracts tokens with length
[x] Scan returns empty array for no tokens
[x] Render handles consecutive percent signs
[x] Render handles escaped percent signs
[x] Render handles unclosed braces
[x] Render handles unopened braces
[x] Render handles very long string with date
[x] Render handles multiple dates
[x] Render handles valid year format
[x] Render handles valid month format
[x] Render handles valid day format
[x] Render handles full date format
[x] Render handles percent b
[x] Render handles percent a
[x] Render handles complex percent format
[x] Render does not replace invalid format specifiers
[x] Render replaces timezone format
[x] Scan works with mixed content

View File

@@ -0,0 +1,59 @@
<?php
use CodeIgniter\Config\Factories;
use CodeIgniter\Test\CIUnitTestCase;
use Config\OSPOS;
class LocaleHelperTest extends CIUnitTestCase
{
protected function setUp(): void
{
parent::setUp();
require_once __DIR__ . '/../../app/Helpers/locale_helper.php';
$config = new OSPOS();
$config->settings = ['dateformat' => 'Y-m-d'];
Factories::injectMock('config', OSPOS::class, $config);
}
public function testValidDateReturnsTrue(): void
{
$this->assertTrue(isValidDate('2024-06-10'));
}
public function testInvalidDateFormatReturnsFalse(): void
{
$this->assertFalse(isValidDate('10/06/2024'));
}
public function testImpossibleDateReturnsFalse(): void
{
$this->assertFalse(isValidDate('2024-13-01'));
}
public function testPhpDateOverflowReturnsFalse(): void
{
// PHP silently overflows Feb 30 → Mar 1; the format()===candidate check catches this
$this->assertFalse(isValidDate('2024-02-30'));
}
public function testEmptyStringReturnsFalse(): void
{
$this->assertFalse(isValidDate(''));
}
public function testLeapDayValidReturnsTrue(): void
{
$this->assertTrue(isValidDate('2024-02-29'));
}
public function testLeapDayInvalidYearReturnsFalse(): void
{
$this->assertFalse(isValidDate('2023-02-29'));
}
public function testPartialDateReturnsFalse(): void
{
$this->assertFalse(isValidDate('2024-06'));
}
}