diff --git a/plugins/avatars/avatars.js b/plugins/avatars/avatars.js index 43a9cec42..6a1ccd56f 100644 --- a/plugins/avatars/avatars.js +++ b/plugins/avatars/avatars.js @@ -64,7 +64,7 @@ if (messageItemHeader) { messageItemHeader.prepend(Element.fromHTML( - `` + `` )); } @@ -98,7 +98,7 @@ if ('MailMessageList' === e.detail.viewModelTemplateID) { document.getElementById('MailMessageList').content.querySelector('.messageCheckbox') - .append(Element.fromHTML(``)); + .append(Element.fromHTML(``)); } }); diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php index 8c82541e9..fcf762763 100644 --- a/plugins/avatars/index.php +++ b/plugins/avatars/index.php @@ -7,8 +7,8 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin AUTHOR = 'SnappyMail', URL = 'https://snappymail.eu/', VERSION = '1.1', - RELEASE = '2022-11-23', - REQUIRED = '2.22.0', + RELEASE = '2022-11-27', + REQUIRED = '2.22.4', CATEGORY = 'Contacts', LICENSE = 'MIT', DESCRIPTION = 'Show photo of sender in message and messages list (supports BIMI and Gravatar, Contacts is still TODO)'; @@ -19,8 +19,9 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin $this->addJs('avatars.js'); $this->addJsonHook('Avatar', 'DoAvatar'); $this->addPartHook('Avatar', 'ServiceAvatar'); - // TODO: https://github.com/the-djmaze/snappymail/issues/714 -// $this->addHook('filter.json-response', 'FilterJsonResponse'); + // https://github.com/the-djmaze/snappymail/issues/714 + $this->Config()->Get('plugin', 'delay', true) + || $this->addHook('filter.json-response', 'FilterJsonResponse'); } public function FilterJsonResponse(string $sAction, array &$aResponseItem) @@ -69,10 +70,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin public function ServiceAvatar(string $sServiceName, string $sBimi, string $sEmail) { $sEmail = \SnappyMail\Crypt::DecryptUrlSafe($sEmail); - $oActions = \RainLoop\Api::Actions(); - $oActions->verifyCacheByKey($sEmail, true); if ($sEmail && ($aResult = $this->getAvatar($sEmail, !empty($sBimi)))) { - $oActions->Http()->ServerUseCache($oActions->etag($sEmail), \time(), \time() + 86400); \header('Content-Type: '.$aResult[0]); echo $aResult[1]; return true; @@ -83,6 +81,9 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin protected function configMapping() : array { return array( + \RainLoop\Plugins\Property::NewInstance('delay')->SetLabel('Delay loading') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(true), \RainLoop\Plugins\Property::NewInstance('bimi')->SetLabel('Use BIMI (https://bimigroup.org/)') ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) ->SetDefaultValue(false), @@ -98,28 +99,30 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin return null; } - $oActions = \RainLoop\Api::Actions(); -// $oActions->verifyCacheByKey($sEmail, true); + $sAsciiEmail = \mb_strtolower(\MailSo\Base\Utils::IdnToAscii($sEmail, true)); + $sEmailId = \sha1($sAsciiEmail); + + \MailSo\Base\Http::setETag($sEmailId); + \header('Cache-Control: private'); +// \header('Expires: '.\gmdate('D, j M Y H:i:s', \time() + 86400).' UTC'); $aResult = null; - $sAsciiEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true); - $sEmailId = \sha1(\strtolower($sAsciiEmail)); - $sFile = \APP_PRIVATE_DATA . 'avatars/' . $sEmailId; $aFiles = \glob("{$sFile}.*"); if ($aFiles) { + \MailSo\Base\Http::setLastModified(\filemtime($aFiles[0])); $aResult = [ \mime_content_type($aFiles[0]), \file_get_contents($aFiles[0]) ]; -// $oActions->Http()->ServerUseCache($oActions->etag($sEmail), \time(), \time() + 86400); return $aResult; } // TODO: lookup contacts vCard and return PHOTO value /* if (!$aResult) { + $oActions = \RainLoop\Api::Actions(); $oAccount = $oActions->getAccountFromToken(); if ($oAccount) { $oAddressBookProvider = $oActions->AddressBookProvider($oAccount); @@ -172,6 +175,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin $sFile . \SnappyMail\File\MimeType::toExtension($aResult[0]), $aResult[1] ); + \MailSo\Base\Http::setLastModified(\time()); } if (!$aResult) { @@ -182,18 +186,18 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin 'empty-contact' // DATA_IMAGE_USER_DOT_PIC ]; foreach ($aServices as $service) { - if (\file_exists(__DIR__ . "/images/{$service}.png")) { + $file = __DIR__ . "/images/{$service}.png"; + if (\file_exists($file)) { + \MailSo\Base\Http::setLastModified(\filemtime($file)); $aResult = [ 'image/png', - \file_get_contents(__DIR__ . "/images/{$service}.png") + \file_get_contents($file) ]; break; } } } -// $oActions->Http()->ServerUseCache($oActions->etag($sEmail), \time(), \time() + 86400); - return $aResult; } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php index 16ee13cfe..5ac3fa106 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php @@ -36,14 +36,14 @@ class Http * * @return mixed */ - public function GetServer(string $sKey, $mDefault = null) + public static function GetServer(string $sKey, $mDefault = null) { return isset($_SERVER[$sKey]) ? $_SERVER[$sKey] : $mDefault; } public function GetMethod() : string { - return $this->GetServer('REQUEST_METHOD', ''); + return static::GetServer('REQUEST_METHOD', ''); } public function IsPost() : bool @@ -67,7 +67,7 @@ class Http { if (empty($sValueToCheck)) { - $sValueToCheck = $this->GetServer('REMOTE_ADDR', ''); + $sValueToCheck = static::GetServer('REMOTE_ADDR', ''); } return $this->CheckLocalhost($sValueToCheck); @@ -84,21 +84,16 @@ class Http return $sRawBody; } - public function GetHeader(string $sHeader) : string + public static function GetHeader(string $sHeader) : string { $sServerKey = 'HTTP_'.\strtoupper(\str_replace('-', '_', $sHeader)); - $sResultHeader = $this->GetServer($sServerKey, ''); - - if (0 === \strlen($sResultHeader) && - \MailSo\Base\Utils::FunctionCallable('apache_request_headers')) - { + $sResultHeader = static::GetServer($sServerKey, ''); + if (0 === \strlen($sResultHeader) && \MailSo\Base\Utils::FunctionCallable('apache_request_headers')) { $sHeaders = \apache_request_headers(); - if (isset($sHeaders[$sHeader])) - { + if (isset($sHeaders[$sHeader])) { $sResultHeader = $sHeaders[$sHeader]; } } - return $sResultHeader; } @@ -109,15 +104,15 @@ class Http public function IsSecure(bool $bCheckProxy = true) : bool { - $sHttps = \strtolower($this->GetServer('HTTPS', '')); - if ('on' === $sHttps || ('' === $sHttps && '443' === (string) $this->GetServer('SERVER_PORT', ''))) + $sHttps = \strtolower(static::GetServer('HTTPS', '')); + if ('on' === $sHttps || ('' === $sHttps && '443' === (string) static::GetServer('SERVER_PORT', ''))) { return true; } if ($bCheckProxy && ( - ('https' === \strtolower($this->GetServer('HTTP_X_FORWARDED_PROTO', ''))) || - ('on' === \strtolower($this->GetServer('HTTP_X_FORWARDED_SSL', ''))) + ('https' === \strtolower(static::GetServer('HTTP_X_FORWARDED_PROTO', ''))) || + ('on' === \strtolower(static::GetServer('HTTP_X_FORWARDED_SSL', ''))) )) { return true; @@ -128,11 +123,11 @@ class Http public function GetHost(bool $bWithRemoteUserData = false, bool $bWithoutWWW = true, bool $bWithoutPort = false) : string { - $sHost = $this->GetServer('HTTP_HOST', ''); + $sHost = static::GetServer('HTTP_HOST', ''); if (!\strlen($sHost)) { - $sName = $this->GetServer('SERVER_NAME'); - $iPort = (int) $this->GetServer('SERVER_PORT', 80); + $sName = static::GetServer('SERVER_NAME'); + $iPort = (int) static::GetServer('SERVER_PORT', 80); $sHost = (\in_array($iPort, array(80, 433))) ? $sName : $sName.':'.$iPort; } @@ -144,7 +139,7 @@ class Http if ($bWithRemoteUserData) { - $sUser = \trim($this->GetServer('REMOTE_USER', '')); + $sUser = \trim(static::GetServer('REMOTE_USER', '')); $sHost = (\strlen($sUser) ? $sUser.'@' : '').$sHost; } @@ -159,63 +154,68 @@ class Http public function GetClientIp(bool $bCheckProxy = false) : string { $sIp = ''; - if ($bCheckProxy && null !== $this->GetServer('HTTP_CLIENT_IP', null)) + if ($bCheckProxy && null !== static::GetServer('HTTP_CLIENT_IP', null)) { - $sIp = $this->GetServer('HTTP_CLIENT_IP', ''); + $sIp = static::GetServer('HTTP_CLIENT_IP', ''); } - else if ($bCheckProxy && null !== $this->GetServer('HTTP_X_FORWARDED_FOR', null)) + else if ($bCheckProxy && null !== static::GetServer('HTTP_X_FORWARDED_FOR', null)) { - $sIp = $this->GetServer('HTTP_X_FORWARDED_FOR', ''); + $sIp = static::GetServer('HTTP_X_FORWARDED_FOR', ''); } else { - $sIp = $this->GetServer('REMOTE_ADDR', ''); + $sIp = static::GetServer('REMOTE_ADDR', ''); } return $sIp; } - public function ServerNotModifiedCache(int $iExpireTime, bool $bSetCacheHeader = true, string $sEtag = '') : bool + public static function checkETag(string $ETag) : void { - $bResult = false; - if (0 < $iExpireTime) - { - $iUtcTimeStamp = \time(); - $sIfModifiedSince = $this->GetHeader('If-Modified-Since', ''); - if (0 === \strlen($sIfModifiedSince)) - { - if ($bSetCacheHeader) - { - \header('Cache-Control: public'); - \header('Pragma: public'); - \header('Last-Modified: '.\gmdate('D, d M Y H:i:s', $iUtcTimeStamp - $iExpireTime).' UTC'); - \header('Expires: '.\gmdate('D, j M Y H:i:s', $iUtcTimeStamp + $iExpireTime).' UTC'); - - if (\strlen($sEtag)) - { - \header('Etag: '.$sEtag); - } - } - } - else - { - static::StatusHeader(304); - $bResult = true; - } + $sIfNoneMatch = static::GetHeader('If-None-Match'); + if ($sIfNoneMatch && false !== \strpos($sIfNoneMatch, $ETag)) { + static::StatusHeader(304); + exit; + } + $sIfMatch = static::GetHeader('If-Match'); + if ($sIfMatch && false === \strpos($sIfMatch, $ETag)) { + static::StatusHeader(412); + exit; } - - return $bResult; } - /** - * @staticvar boolean $bCache - */ + public static function setETag(string $ETag) : void + { + static::checkETag($ETag); + \header("ETag: \"{$ETag}\""); + } + + public static function checkLastModified(int $mtime) : void + { + $sIfModifiedSince = static::GetHeader('If-Modified-Since'); + if ($sIfModifiedSince && $mtime <= \strtotime($sIfModifiedSince)) { + static::StatusHeader(304); + exit; + } + $sIfUnmodifiedSince = static::GetHeader('If-Unmodified-Since'); + if ($sIfUnmodifiedSince && $mtime > \strtotime($sIfUnmodifiedSince)) { + static::StatusHeader(412); + exit; + } + } + + public static function setLastModified(int $mtime) : void + { + static::checkLastModified($mtime); + \header('Last-Modified: '.\gmdate('D, d M Y H:i:s \G\M\T', $mtime)); # DATE_RFC1123 + } + + private static $bCache = false; + public function ServerNoCache() { - static $bCache = false; - if (false === $bCache) - { - $bCache = true; + if (!static::$bCache) { + static::$bCache = true; \header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); \header('Last-Modified: '.\gmdate('D, d M Y H:i:s').' GMT'); \header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0'); @@ -223,19 +223,14 @@ class Http } } - /** - * @staticvar boolean $bCache - */ - public function ServerUseCache(string $sEtag, int $iLastModified, int $iExpires) + public static function ServerUseCache(string $sEtag = '', int $iLastModified = 0, int $iExpires = 0) : void { - static $bCache = false; - if (false === $bCache) - { - $bCache = true; + if (!static::$bCache) { + static::$bCache = true; \header('Cache-Control: private'); - \header('ETag: '.$sEtag); - \header('Last-Modified: '.\gmdate('D, d M Y H:i:s', $iLastModified).' UTC'); - \header('Expires: '.\gmdate('D, j M Y H:i:s', $iExpires).' UTC'); + $iExpires && \header('Expires: '.\gmdate('D, j M Y H:i:s', \time() + $iExpires).' UTC'); + $sEtag && static::setETag($sEtag); + $iLastModified && static::setLastModified($iLastModified); } } @@ -253,6 +248,7 @@ class Http 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', + 412 => 'Precondition Failed', 416 => 'Requested range not satisfiable', 500 => 'Internal Server Error' ); @@ -271,7 +267,7 @@ class Http public function GetPath() : string { - $sUrl = \ltrim(\substr($this->GetServer('SCRIPT_NAME', ''), 0, \strrpos($this->GetServer('SCRIPT_NAME', ''), '/')), '/'); + $sUrl = \ltrim(\substr(static::GetServer('SCRIPT_NAME', ''), 0, \strrpos(static::GetServer('SCRIPT_NAME', ''), '/')), '/'); return '' === $sUrl ? '/' : '/'.$sUrl.'/'; } 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 2b880b930..ee7952367 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -147,10 +147,10 @@ class Actions '[SM:' . APP_VERSION . '][IP:' . $oHttp->GetClientIp($this->oConfig->Get('labs', 'http_client_ip_check_proxy', false)) . '][PID:' . (\MailSo\Base\Utils::FunctionCallable('getmypid') ? \getmypid() : 'unknown') - . '][' . $oHttp->GetServer('SERVER_SOFTWARE', '~') + . '][' . \MailSo\Base\Http::GetServer('SERVER_SOFTWARE', '~') . '][' . (\MailSo\Base\Utils::FunctionCallable('php_sapi_name') ? \php_sapi_name() : '~') . '][Streams:' . \implode(',', \stream_get_transports()) - . '][' . $oHttp->GetMethod() . ' ' . $oHttp->GetScheme() . '://' . $oHttp->GetHost(false, false) . $oHttp->GetServer('REQUEST_URI', '') . ']' + . '][' . $oHttp->GetMethod() . ' ' . $oHttp->GetScheme() . '://' . $oHttp->GetHost(false, false) . \MailSo\Base\Http::GetServer('REQUEST_URI', '') . ']' ); } @@ -1062,17 +1062,18 @@ class Actions public function etag(string $sKey): string { - return \md5('Etag:' . \md5($sKey . \md5($this->oConfig->Get('cache', 'index', '')))); + return \md5($sKey . $this->oConfig->Get('cache', 'index', '')); } public function cacheByKey(string $sKey, bool $bForce = false): bool { - if (!empty($sKey) && ($bForce || ($this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)))) { - $iExpires = $this->oConfig->Get('cache', 'http_expires', 3600); - if (0 < $iExpires) { - $this->Http()->ServerUseCache($this->etag($sKey), 1382478804, \time() + $iExpires); - return true; - } + if ($sKey && ($bForce || ($this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)))) { + \MailSo\Base\Http::ServerUseCache( + $this->etag($sKey), + 1382478804, + $this->oConfig->Get('cache', 'http_expires', 3600) + ); + return true; } $this->Http()->ServerNoCache(); return false; @@ -1080,13 +1081,9 @@ class Actions public function verifyCacheByKey(string $sKey, bool $bForce = false): void { - if (!empty($sKey) && ($bForce || ($this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)))) { - $sIfNoneMatch = $this->Http()->GetHeader('If-None-Match', ''); - if ($this->etag($sKey) === $sIfNoneMatch) { - \MailSo\Base\Http::StatusHeader(304); - $this->cacheByKey($sKey); - exit(0); - } + if ($sKey && ($bForce || ($this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)))) { + \MailSo\Base\Http::checkETag($this->etag($sKey)); +// $this->cacheByKey($sKey, $bForce); } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php index 58a979b97..4d8d15c91 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php @@ -69,7 +69,7 @@ trait Localization private function getUserLanguagesFromHeader(): array { $aResult = $aList = array(); - $sAcceptLang = \strtolower($this->Http()->GetServer('HTTP_ACCEPT_LANGUAGE', 'en')); + $sAcceptLang = \strtolower(\MailSo\Base\Http::GetServer('HTTP_ACCEPT_LANGUAGE', 'en')); if (!empty($sAcceptLang) && \preg_match_all('/([a-z]{1,8}(?:-[a-z]{1,8})?)(?:;q=([0-9.]+))?/', $sAcceptLang, $aList)) { $aResult = \array_combine($aList[1], $aList[2]); foreach ($aResult as $n => $v) { diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php index 0e9c78f42..e0ad8b60a 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php @@ -117,7 +117,7 @@ trait Raw $aValues = $this->getDecodedRawKeyValue($sRawKey); - $sRange = $this->Http()->GetHeader('Range'); + $sRange = \MailSo\Base\Http::GetHeader('Range'); $aMatch = array(); $sRangeStart = $sRangeEnd = '';