BanEntry: work around stupid bug in ext/date

https://bugs.php.net/bug.php?id=75992

When plugins do time-limited bans and users enter stupid time values, a shitty bug in ext/date gets triggered, but only when reading the ban entries from disk. DateTime->format() is able to produce formatted strings which have more than 4 digits in the year, which are then considered invalid. This works around it by trying to parse a formatted version on the fly to ensure that it is valid.

This also cleans up and improves ban list loading and handling.
This commit is contained in:
Dylan K. Taylor 2018-02-22 14:48:53 +00:00
parent aa11dbb928
commit 37e8c8d324
2 changed files with 66 additions and 22 deletions

View File

@ -56,6 +56,7 @@ class BanEntry{
} }
public function setCreated(\DateTime $date){ public function setCreated(\DateTime $date){
self::validateDate($date);
$this->creationDate = $date; $this->creationDate = $date;
} }
@ -78,6 +79,9 @@ class BanEntry{
* @param \DateTime|null $date * @param \DateTime|null $date
*/ */
public function setExpires(\DateTime $date = null){ public function setExpires(\DateTime $date = null){
if($date !== null){
self::validateDate($date);
}
$this->expirationDate = $date; $this->expirationDate = $date;
} }
@ -110,37 +114,71 @@ class BanEntry{
return $str; return $str;
} }
/**
* Hacky function to validate \DateTime objects due to a bug in PHP. format() with "Y" can emit years with more than
* 4 digits, but createFromFormat() with "Y" doesn't accept them if they have more than 4 digits on the year.
*
* @link https://bugs.php.net/bug.php?id=75992
*
* @param \DateTime $dateTime
* @throws \RuntimeException if the argument can't be parsed from a formatted date string
*/
private static function validateDate(\DateTime $dateTime) : void{
self::parseDate($dateTime->format(self::$format));
}
/**
* @param string $date
*
* @return \DateTime|null
* @throws \RuntimeException
*/
private static function parseDate(string $date) : ?\DateTime{
$datetime = \DateTime::createFromFormat(self::$format, $date);
if(!($datetime instanceof \DateTime)){
throw new \RuntimeException("Error parsing date for BanEntry: " . implode(", ", \DateTime::getLastErrors()["errors"]));
}
return $datetime;
}
/** /**
* @param string $str * @param string $str
* *
* @return BanEntry|null * @return BanEntry|null
* @throws \RuntimeException
*/ */
public static function fromString(string $str){ public static function fromString(string $str) : ?BanEntry{
if(strlen($str) < 2){ if(strlen($str) < 2){
return null; return null;
}else{ }else{
$str = explode("|", trim($str)); $str = explode("|", trim($str));
$entry = new BanEntry(trim(array_shift($str))); $entry = new BanEntry(trim(array_shift($str)));
if(count($str) > 0){ do{
$datetime = \DateTime::createFromFormat(self::$format, array_shift($str)); if(empty($str)){
if(!($datetime instanceof \DateTime)){ break;
MainLogger::getLogger()->alert("Error parsing date for BanEntry for player \"" . $entry->getName() . "\", the format may be invalid!");
return $entry;
} }
$entry->setCreated($datetime);
if(count($str) > 0){ $entry->setCreated(self::parseDate(array_shift($str)));
if(empty($str)){
break;
}
$entry->setSource(trim(array_shift($str))); $entry->setSource(trim(array_shift($str)));
if(count($str) > 0){ if(empty($str)){
break;
}
$expire = trim(array_shift($str)); $expire = trim(array_shift($str));
if(strtolower($expire) !== "forever" and strlen($expire) > 0){ if(strtolower($expire) !== "forever" and strlen($expire) > 0){
$entry->setExpires(\DateTime::createFromFormat(self::$format, $expire)); $entry->setExpires(self::parseDate($expire));
} }
if(count($str) > 0){ if(empty($str)){
break;
}
$entry->setReason(trim(array_shift($str))); $entry->setReason(trim(array_shift($str)));
} }while(false);
}
}
}
return $entry; return $entry;
} }

View File

@ -147,10 +147,16 @@ class BanList{
if(is_resource($fp)){ if(is_resource($fp)){
while(($line = fgets($fp)) !== false){ while(($line = fgets($fp)) !== false){
if($line{0} !== "#"){ if($line{0} !== "#"){
try{
$entry = BanEntry::fromString($line); $entry = BanEntry::fromString($line);
if($entry instanceof BanEntry){ if($entry instanceof BanEntry){
$this->list[$entry->getName()] = $entry; $this->list[$entry->getName()] = $entry;
} }
}catch(\Throwable $e){
$logger = MainLogger::getLogger();
$logger->critical("Failed to parse ban entry from string \"$line\": " . $e->getMessage());
$logger->logException($e);
}
} }
} }
fclose($fp); fclose($fp);