diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php index 31d0df783..b311b2005 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php @@ -1000,8 +1000,9 @@ END; } } - public static function RecTimeDirRemove(string $sTempPath, int $iTime2Kill, int $iNow) : bool + public static function RecTimeDirRemove(string $sTempPath, int $iTime2Kill, int $iNow = 0) : bool { + $iNow = $iNow ?: \time(); $iFileCount = 0; $sTempPath = rtrim($sTempPath, '\\/'); diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/File.php b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/File.php index c880c2ef1..8dd7a17e6 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/File.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/File.php @@ -76,7 +76,7 @@ class File implements \MailSo\Cache\DriverInterface { if (0 < $iTimeToClearInHours) { - \MailSo\Base\Utils::RecTimeDirRemove($this->sCacheFolder, 60 * 60 * $iTimeToClearInHours, \time()); + \MailSo\Base\Utils::RecTimeDirRemove($this->sCacheFolder, 3600 * $iTimeToClearInHours); return true; } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php index 27d0a3cda..616ce6f1e 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -191,7 +191,7 @@ class Actions switch ($sName) { case 'files': // RainLoop\Providers\Files\IFiles - $mResult = new Providers\Files\FileStorage(APP_PRIVATE_DATA . 'storage/files'); + $mResult = new Providers\Files\FileStorage(APP_PRIVATE_DATA . 'storage'); break; case 'storage': case 'storage-local': diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php index 7f373d39d..e9781414b 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php @@ -55,6 +55,16 @@ class FileStorage implements \RainLoop\Providers\Files\IFiles $bCreate = !!\preg_match('/[wac]/', $sOpenMode); $sFileName = $this->generateFullFileName($oAccount, $sKey, $bCreate); + if (!\file_exists($sFileName)) { + $sOldFileName = $this->generateFullFileNameOld($oAccount, $sKey); + if (\file_exists($sOldFileName) + && ( + (!\is_dir(\dirname($sOldFileName)) && !\mkdir(\dirname($sOldFileName), 0700, true)) + || !\rename($sOldFileName, $sFileName) + )) { + $sFileName = $sOldFileName; + } + } if ($bCreate || \file_exists($sFileName)) { $mResult = \fopen($sFileName, $sOpenMode); @@ -68,57 +78,57 @@ class FileStorage implements \RainLoop\Providers\Files\IFiles return $mResult; } - public function GetFileName(\RainLoop\Model\Account $oAccount, string $sKey) : string + public function GetFileName(\RainLoop\Model\Account $oAccount, string $sKey) /*: string|false*/ { - $mResult = false; $sFileName = $this->generateFullFileName($oAccount, $sKey); - if (\file_exists($sFileName)) - { - $mResult = $sFileName; + if (!\file_exists($sFileName)) { + $sFileName = $this->generateFullFileNameOld($oAccount, $sKey); } - - return $mResult; + return \file_exists($sFileName) ? $sFileName : false; } public function Clear(\RainLoop\Model\Account $oAccount, string $sKey) : bool { - $mResult = true; $sFileName = $this->generateFullFileName($oAccount, $sKey); - if (\file_exists($sFileName)) - { - if (isset($this->aResources[$sFileName]) && \is_resource($this->aResources[$sFileName])) - { + if (!\file_exists($sFileName)) { + $sFileName = $this->generateFullFileNameOld($oAccount, $sKey); + } + if (\file_exists($sFileName)) { + if (isset($this->aResources[$sFileName]) && \is_resource($this->aResources[$sFileName])) { \fclose($this->aResources[$sFileName]); } - - $mResult = \unlink($sFileName); + return \unlink($sFileName); } - - return $mResult; + return false; } - public function FileSize(\RainLoop\Model\Account $oAccount, string $sKey) : int + public function FileSize(\RainLoop\Model\Account $oAccount, string $sKey) /*: int|false*/ { - $mResult = false; $sFileName = $this->generateFullFileName($oAccount, $sKey); - if (\file_exists($sFileName)) - { - $mResult = \filesize($sFileName); + if (!\file_exists($sFileName)) { + $sFileName = $this->generateFullFileNameOld($oAccount, $sKey); } - - return $mResult; + return \file_exists($sFileName) ? \filesize($sFileName) : false; } public function FileExists(\RainLoop\Model\Account $oAccount, string $sKey) : bool { - return \file_exists($this->generateFullFileName($oAccount, $sKey)); + return \file_exists($this->generateFullFileName($oAccount, $sKey)) + || \file_exists($this->generateFullFileNameOld($oAccount, $sKey)); } public function GC(int $iTimeToClearInHours = 24) : bool { - if (0 < $iTimeToClearInHours) - { - \MailSo\Base\Utils::RecTimeDirRemove($this->sDataPath, 60 * 60 * $iTimeToClearInHours, \time()); + if (0 < $iTimeToClearInHours) { + $iTimeToClear = 3600 * $iTimeToClearInHours; + foreach (\glob("{$this->sDataPath}/*", GLOB_ONLYDIR) as $sDomain) { + foreach (\glob("{$sDomain}/*", GLOB_ONLYDIR) as $sLocal) { + \MailSo\Base\Utils::RecTimeDirRemove("{$sLocal}/.files", $iTimeToClear); + } + } + // Old + \MailSo\Base\Utils::RecTimeDirRemove("{$this->sDataPath}/files", $iTimeToClear); + return true; } @@ -141,7 +151,36 @@ class FileStorage implements \RainLoop\Providers\Files\IFiles return true; } + /** + * Replace control characters, ampersand, spaces and reserved characters (based on Win95 VFAT) + * en.wikipedia.org/wiki/Filename#Reserved_characters_and_words + */ + private static function fixName($filename) + { + return \preg_replace('#[|\\\\?*<":>+\\[\\]/&\\s\\pC]#su', '-', $filename); + } + private function generateFullFileName(\RainLoop\Model\Account $oAccount, string $sKey, bool $bMkDir = false) : string + { + $sEmail = $oAccount->ParentEmailHelper() ?: 'nobody@unknown.tld'; + $sSubEmail = $oAccount->IsAdditionalAccount() ? $oAccount->Email() : ''; + + $aEmail = \explode('@', $sEmail ?: 'nobody@unknown.tld'); + $sDomain = \trim(1 < \count($aEmail) ? \array_pop($aEmail) : ''); + $sFilePath = $this->sDataPath + .'/'.static::fixName($sDomain ?: 'unknown.tld') + .'/'.static::fixName(\implode('@', $aEmail) ?: '.unknown') + .($sSubEmail ? '/'.static::fixName($sSubEmail) : '') + .'/.files/'.\sha1($sKey); + + if ($bMkDir && !\is_dir(\dirname($sFilePath)) && !\mkdir(\dirname($sFilePath), 0700, true)) { + throw new \RainLoop\Exceptions\Exception('Can\'t make storage directory "'.$sFilePath.'"'); + } + + return $sFilePath; + } + + private function generateFullFileNameOld(\RainLoop\Model\Account $oAccount, string $sKey, bool $bMkDir = false) : string { $sEmail = $sSubEmail = ''; if ($oAccount instanceof \RainLoop\Model\Account) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/FileStorage.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/FileStorage.php index 01bfa80a2..5ab03d8f3 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/FileStorage.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/FileStorage.php @@ -2,6 +2,8 @@ namespace RainLoop\Providers\Storage; +use \RainLoop\Providers\Storage\Enumerations\StorageType; + class FileStorage implements \RainLoop\Providers\Storage\IStorage { /** @@ -31,8 +33,8 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage */ public function Put($oAccount, int $iStorageType, string $sKey, string $sValue) : bool { - return false !== \file_put_contents( - $this->generateFileName($oAccount, $iStorageType, $sKey, true), $sValue); + $sFileName = $this->generateFileName($oAccount, $iStorageType, $sKey, true); + return $sFileName && false !== \file_put_contents($sFileName, $sValue); } /** @@ -45,9 +47,15 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage { $mValue = false; $sFileName = $this->generateFileName($oAccount, $iStorageType, $sKey); - if (\file_exists($sFileName)) - { + if ($sFileName && \file_exists($sFileName)) { $mValue = \file_get_contents($sFileName); + } else { + $sFileName = $this->generateFileNameOld($oAccount, $iStorageType, $sKey); + if ($sFileName && \file_exists($sFileName)) { + $mValue = \file_get_contents($sFileName); + $this->Put($oAccount, $iStorageType, $sKey, $mValue); + \unlink($sFileName); + } } return false === $mValue ? $mDefault : $mValue; @@ -60,9 +68,13 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage { $mResult = true; $sFileName = $this->generateFileName($oAccount, $iStorageType, $sKey); - if (\file_exists($sFileName)) - { + if ($sFileName && \file_exists($sFileName)) { $mResult = \unlink($sFileName); + } else { + $sFileName = $this->generateFileNameOld($oAccount, $iStorageType, $sKey); + if ($sFileName && \file_exists($sFileName)) { + $mResult = \unlink($sFileName); + } } return $mResult; @@ -73,19 +85,18 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage */ public function DeleteStorage($oAccount) : bool { - $sPath = $this->generateFileName($oAccount, - \RainLoop\Providers\Storage\Enumerations\StorageType::USER, 'xxx', false, true); - - if (!empty($sPath) && \is_dir($sPath)) - { + $sPath = $this->generateFileName($oAccount, StorageType::USER, '', false, true); + if ($sPath && \is_dir($sPath)) { \MailSo\Base\Utils::RecRmDir($sPath); } - $sPath = $this->generateFileName($oAccount, - \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, 'xxx', false, true); + $sPath = $this->generateFileNameOld($oAccount, StorageType::USER, '', false, true); + if ($sPath && \is_dir($sPath)) { + \MailSo\Base\Utils::RecRmDir($sPath); + } - if (!empty($sPath) && \is_dir($sPath)) - { + $sPath = $this->generateFileNameOld($oAccount, StorageType::CONFIG, '', false, true); + if ($sPath && \is_dir($sPath)) { \MailSo\Base\Utils::RecRmDir($sPath); } @@ -97,14 +108,74 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage return $this->bLocal; } + /** + * Replace control characters, ampersand, spaces and reserved characters (based on Win95 VFAT) + * en.wikipedia.org/wiki/Filename#Reserved_characters_and_words + */ + private static function fixName($filename) + { + return \preg_replace('#[|\\\\?*<":>+\\[\\]/&\\s\\pC]#su', '-', $filename); + } + /** * @param \RainLoop\Model\Account|string|null $mAccount */ - public function generateFileName($mAccount, int $iStorageType, string $sKey, bool $bMkDir = false, bool $bForDeleteAction = false) : string + private function generateFileName($mAccount, int $iStorageType, string $sKey, bool $bMkDir = false, bool $bForDeleteAction = false) : string + { + $sEmail = $sSubEmail = ''; + if (null === $mAccount) { + $iStorageType = StorageType::NOBODY; + } else if ($mAccount instanceof \RainLoop\Model\Account) { + $sEmail = $mAccount->ParentEmailHelper(); + if ($this->bLocal && $mAccount->IsAdditionalAccount() && !$bForDeleteAction) + { + $sSubEmail = $mAccount->Email(); + } + } else if (\is_string($mAccount) && empty($sEmail)) { + $sEmail = $mAccount; + } + + $sFilePath = ''; + switch ($iStorageType) + { + case StorageType::NOBODY: + $sFilePath = $this->sDataPath.'/__nobody__/'.\sha1($sKey ?: \time()); + break; + case StorageType::CONFIG: + if (empty($sEmail)) { + return ''; + } + $aEmail = \explode('@', $sEmail ?: 'nobody@unknown.tld'); + $sDomain = \trim(1 < \count($aEmail) ? \array_pop($aEmail) : ''); + $sFilePath = $this->sDataPath + .'/'.static::fixName($sDomain ?: 'unknown.tld') + .'/'.static::fixName(\implode('@', $aEmail) ?: '.unknown') + .'/'.($sSubEmail ? static::fixName($sSubEmail).'/' : '') + .($sKey ? static::fixName($sKey) : ''); + break; + default: + throw new \Exception("Invalid storage type {$iStorageType}"); + } + + if ($bMkDir && !empty($sFilePath) && !\is_dir(\dirname($sFilePath))) + { + if (!\mkdir(\dirname($sFilePath), 0700, true)) + { + throw new \RainLoop\Exceptions\Exception('Can\'t make storage directory "'.$sFilePath.'"'); + } + } + + return $sFilePath; + } + + /** + * Old RainLoop structure + */ + private function generateFileNameOld($mAccount, int $iStorageType, string $sKey, bool $bMkDir = false, bool $bForDeleteAction = false) : string { if (null === $mAccount) { - $iStorageType = \RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY; + $iStorageType = StorageType::NOBODY; } $sEmail = $sSubEmail = ''; @@ -129,20 +200,20 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage switch ($iStorageType) { default: - case \RainLoop\Providers\Storage\Enumerations\StorageType::USER: - case \RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY: + case StorageType::USER: + case StorageType::NOBODY: $sTypePath = 'data'; $sKeyPath = \md5($sKey); $sKeyPath = \substr($sKeyPath, 0, 2).'/'.$sKeyPath; break; - case \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG: + case StorageType::CONFIG: $sTypePath = 'cfg'; $sKeyPath = \preg_replace('/[_]+/', '_', \preg_replace('/[^a-zA-Z0-9\/]/', '_', $sKey)); break; } $sFilePath = ''; - if (\RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY === $iStorageType) + if (StorageType::NOBODY === $iStorageType) { $sFilePath = $this->sDataPath.'/'.$sTypePath.'/__nobody__/'.$sKeyPath; } @@ -150,7 +221,7 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage { $sFilePath = $this->sDataPath.'/'.$sTypePath.'/'. \str_pad(\rtrim(\substr($sEmail, 0, 2), '@'), 2, '_').'/'.$sEmail.'/'. - (0 < \strlen($sSubEmail) ? $sSubEmail.'/' : ''). + (\strlen($sSubEmail) ? $sSubEmail.'/' : ''). ($bForDeleteAction ? '' : $sKeyPath); }