diff --git a/lib/Database.php b/lib/Database.php index fdede55..ea39f21 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -827,7 +827,7 @@ class Database { $limit = Date::normalize("now"); if (Arsse::$conf->purgeFeeds) { // if there is a retention period specified, compute it; otherwise feed are deleted immediatelty - $limit->sub(new \DateInterval(Arsse::$conf->purgeFeeds)); + $limit = Date::sub(Arsse::$conf->purgeFeeds, $limit); } $out = (bool) $this->db->prepare("DELETE from arsse_feeds where orphaned <= ?", "datetime")->run($limit); // commit changes and return diff --git a/lib/Feed.php b/lib/Feed.php index ce1a88d..54b32a2 100644 --- a/lib/Feed.php +++ b/lib/Feed.php @@ -328,12 +328,12 @@ class Feed { return [$new, $edited]; } - protected function computeNextFetch(): \DateTime { + protected function computeNextFetch(): \DateTimeImmutable { $now = Date::normalize(time()); if (!$this->modified) { $diff = $now->getTimestamp() - $this->lastModified->getTimestamp(); $offset = $this->normalizeDateDiff($diff); - $now->modify("+".$offset); + return $now->modify("+".$offset); } else { // the algorithm for updated feeds (returning 200 rather than 304) uses the same parameters as for 304, // save that the last three intervals between item dates are computed, and if any two fall within @@ -347,20 +347,19 @@ class Feed { $offsets[] = $this->normalizeDateDiff($diff); } if ($offsets[0]==$offsets[1] || $offsets[0]==$offsets[2]) { - $now->modify("+".$offsets[0]); + return $now->modify("+".$offsets[0]); } elseif ($offsets[1]==$offsets[2]) { - $now->modify("+".$offsets[1]); + return $now->modify("+".$offsets[1]); } else { - $now->modify("+ 1 hour"); + return $now->modify("+ 1 hour"); } } else { - $now->modify("+ 1 hour"); + return $now->modify("+ 1 hour"); } } - return $now; } - public static function nextFetchOnError($errCount): \DateTime { + public static function nextFetchOnError($errCount): \DateTimeImmutable { if ($errCount < 3) { $offset = "5 minutes"; } elseif ($errCount < 15) { diff --git a/lib/Misc/ValueInfo.php b/lib/Misc/ValueInfo.php index 870dad0..3f226f5 100644 --- a/lib/Misc/ValueInfo.php +++ b/lib/Misc/ValueInfo.php @@ -19,7 +19,7 @@ class ValueInfo { // strings const EMPTY = 1 << 2; const WHITE = 1 << 3; - //normalization types + // normalization types const T_MIXED = 0; // pass through unchanged const T_NULL = 1; // convert to null const T_BOOL = 2; // convert to boolean @@ -28,11 +28,23 @@ class ValueInfo { const T_DATE = 5; // convert to DateTimeInterface instance const T_STRING = 6; // convert to string const T_ARRAY = 7; // convert to array - //normalization modes + // normalization modes const M_NULL = 1 << 28; // pass nulls through regardless of target type const M_DROP = 1 << 29; // drop the value (return null) if the type doesn't match const M_STRICT = 1 << 30; // throw an exception if the type doesn't match const M_ARRAY = 1 << 31; // the value should be a flat array of values of the specified type; indexed and associative are both acceptable + // symbolic date and time formats + const DATE_FORMATS = [ // in out + 'iso8601' => ["!Y-m-d\TH:i:s", "Y-m-d\TH:i:s\Z" ], // NOTE: ISO 8601 dates require special input processing because of varying formats for timezone offsets + 'iso8601m' => ["!Y-m-d\TH:i:s.u", "Y-m-d\TH:i:s.u\Z" ], // NOTE: ISO 8601 dates require special input processing because of varying formats for timezone offsets + 'microtime' => ["U.u", "0.u00 U" ], // NOTE: the actual input format at the user level matches the output format; pre-processing is required for PHP not to fail + 'http' => ["!D, d M Y H:i:s \G\M\T", "D, d M Y H:i:s \G\M\T"], + 'sql' => ["!Y-m-d H:i:s", "Y-m-d H:i:s" ], + 'date' => ["!Y-m-d", "Y-m-d" ], + 'time' => ["!H:i:s", "H:i:s" ], + 'unix' => ["U", "U" ], + 'float' => ["U.u", "U.u" ], + ]; public static function normalize($value, int $type, string $dateInFormat = null, $dateOutFormat = null) { $allowNull = ($type & self::M_NULL); @@ -131,14 +143,14 @@ class ValueInfo { if (is_string($value)) { return $value; } - $dateOutFormat = $dateOutFormat ?? "iso8601"; - $dateOutFormat = isset(Date::FORMAT[$dateOutFormat]) ? Date::FORMAT[$dateOutFormat][1] : $dateOutFormat; - if ($value instanceof \DateTimeImmutable) { - return $value->setTimezone(new \DateTimeZone("UTC"))->format($dateOutFormat); - } elseif ($value instanceof \DateTime) { - $out = clone $value; - $out->setTimezone(new \DateTimeZone("UTC")); - return $out->format($dateOutFormat); + if ($value instanceof \DateTimeInterface) { + $dateOutFormat = $dateOutFormat ?? "iso8601"; + $dateOutFormat = isset(self::DATE_FORMATS[$dateOutFormat]) ? self::DATE_FORMATS[$dateOutFormat][1] : $dateOutFormat; + if ($value instanceof \DateTimeImmutable) { + return $value->setTimezone(new \DateTimeZone("UTC"))->format($dateOutFormat); + } elseif ($value instanceof \DateTime) { + return \DateTimeImmutable::createFromMutable($value)->setTimezone(new \DateTimeZone("UTC"))->format($dateOutFormat); + } } elseif (is_float($value) && is_finite($value)) { $out = (string) $value; if (!strpos($out, "E")) { @@ -168,13 +180,11 @@ class ValueInfo { if ($value instanceof \DateTimeImmutable) { return $value->setTimezone(new \DateTimeZone("UTC")); } elseif ($value instanceof \DateTime) { - $out = clone $value; - $out->setTimezone(new \DateTimeZone("UTC")); - return $out; + return \DateTimeImmutable::createFromMutable($value)->setTimezone(new \DateTimeZone("UTC")); } elseif (is_int($value)) { - return \DateTime::createFromFormat("U", (string) $value, new \DateTimeZone("UTC")); + return \DateTimeImmutable::createFromFormat("U", (string) $value, new \DateTimeZone("UTC")); } elseif (is_float($value)) { - return \DateTime::createFromFormat("U.u", sprintf("%F", $value), new \DateTimeZone("UTC")); + return \DateTimeImmutable::createFromFormat("U.u", sprintf("%F", $value), new \DateTimeZone("UTC")); } elseif (is_string($value)) { try { if (!is_null($dateInFormat)) { @@ -187,28 +197,28 @@ class ValueInfo { throw new \Exception; } } - $f = isset(Date::FORMAT[$dateInFormat]) ? Date::FORMAT[$dateInFormat][0] : $dateInFormat; + $f = isset(self::DATE_FORMATS[$dateInFormat]) ? self::DATE_FORMATS[$dateInFormat][0] : $dateInFormat; if ($dateInFormat=="iso8601" || $dateInFormat=="iso8601m") { - // DateTime::createFromFormat() doesn't provide one catch-all for ISO 8601 timezone specifiers, so we try all of them till one works + // DateTimeImmutable::createFromFormat() doesn't provide one catch-all for ISO 8601 timezone specifiers, so we try all of them till one works if ($dateInFormat=="iso8601m") { - $f2 = Date::FORMAT["iso8601"][0]; + $f2 = self::DATE_FORMATS["iso8601"][0]; $zones = [$f."", $f."\Z", $f."P", $f."O", $f2."", $f2."\Z", $f2."P", $f2."O"]; } else { $zones = [$f."", $f."\Z", $f."P", $f."O"]; } do { $ftz = array_shift($zones); - $out = \DateTime::createFromFormat($ftz, $value, new \DateTimeZone("UTC")); + $out = \DateTimeImmutable::createFromFormat($ftz, $value, new \DateTimeZone("UTC")); } while (!$out && $zones); } else { - $out = \DateTime::createFromFormat($f, $value, new \DateTimeZone("UTC")); + $out = \DateTimeImmutable::createFromFormat($f, $value, new \DateTimeZone("UTC")); } if (!$out) { throw new \Exception; } return $out; } else { - return new \DateTime($value, new \DateTimeZone("UTC")); + return new \DateTimeImmutable($value, new \DateTimeZone("UTC")); } } catch (\Exception $e) { if ($strict && !$drop) { diff --git a/tests/cases/Misc/TestValueInfo.php b/tests/cases/Misc/TestValueInfo.php index 87c6f01..8c2f12d 100644 --- a/tests/cases/Misc/TestValueInfo.php +++ b/tests/cases/Misc/TestValueInfo.php @@ -506,6 +506,18 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest { list($type, $value, $exp) = $test; $this->assertEquals($exp, I::normalize($value, $type | I::M_ARRAY, "iso8601"), "Failed test #$index"); } + // Date-to-string format tests + $test = new \DateTimeImmutable("now", new \DateTimezone("UTC")); + $exp = $test->format(I::DATE_FORMATS['iso8601'][1]); + $this->assertSame($exp, I::normalize($test, I::T_STRING, null), "Failed test for null output date format"); + foreach (I::DATE_FORMATS as $name => $formats) { + $exp = $test->format($formats[1]); + $this->assertSame($exp, I::normalize($test, I::T_STRING, null, $name), "Failed test for output date format '$name'"); + } + foreach (["U", "M j, Y (D)", "r", "c"] as $format) { + $exp = $test->format($format); + $this->assertSame($exp, I::normalize($test, I::T_STRING, null, $format), "Failed test for output date format '$format'"); + } } protected function d($spec, $local, $immutable): \DateTimeInterface { @@ -517,7 +529,7 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest { } } - protected function t(float $spec): \DateTime { - return \DateTime::createFromFormat("U.u", sprintf("%F", $spec), new \DateTimeZone("UTC")); + protected function t(float $spec): \DateTimeImmutable { + return \DateTimeImmutable::createFromFormat("U.u", sprintf("%F", $spec), new \DateTimeZone("UTC")); } }