diff --git a/CHANGELOG.md b/CHANGELOG.md index e6982c17ff..420eba99c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ HumHub Changelog - Enh #4967: Module update broken with expired licence key - Enh #4972: Fix enabling to send notification on remove user from group - Fix #4985: Fix Activity Mail QueryParams on console mode +- Enh #23: Allow to read image URLs with token param - Fix #4989: Translate profile field title in admin list - Fix #5002: Fix loading of fixture spaces on tests - Fix #5018: Activity stream problems with many user accounts diff --git a/protected/humhub/modules/content/widgets/richtext/converter/RichTextToEmailHtmlConverter.php b/protected/humhub/modules/content/widgets/richtext/converter/RichTextToEmailHtmlConverter.php new file mode 100644 index 0000000000..e0369bc4e1 --- /dev/null +++ b/protected/humhub/modules/content/widgets/richtext/converter/RichTextToEmailHtmlConverter.php @@ -0,0 +1,59 @@ +tokenizeBlock($linkBlock)); + } + + /** + * Append a param 'token' to the URL in order to allow see it when user is not logged in e.g. from email inbox + * + * @param LinkParserBlock $linkBlock + * @return LinkParserBlock + */ + protected function tokenizeBlock(LinkParserBlock $linkBlock): LinkParserBlock + { + /* @var User $receiver */ + $receiver = $this->getOption('receiver'); + + if (!($receiver && $linkBlock->getUrl() && $linkBlock->getFileId())) { + return $linkBlock; + } + + $token = ''; + if ($linkBlock->getFileId() !== null) { + $file = File::findOne(['id' => $linkBlock->getFileId()]); + if ($file !== null) { + $token = DownloadAction::generateDownloadToken($file, $receiver); + } + } + + $linkBlock->setUrl($linkBlock->getUrl() . (strpos($linkBlock->getUrl(), '?') === false ? '?' : '&') . 'token=' . $token); + + return $linkBlock; + } +} diff --git a/protected/humhub/modules/content/widgets/richtext/extensions/file/FileExtension.php b/protected/humhub/modules/content/widgets/richtext/extensions/file/FileExtension.php index 31bbd2a620..aec7c59815 100644 --- a/protected/humhub/modules/content/widgets/richtext/extensions/file/FileExtension.php +++ b/protected/humhub/modules/content/widgets/richtext/extensions/file/FileExtension.php @@ -36,7 +36,7 @@ class FileExtension extends RichTextLinkExtension return; } - $linkBlock->setBlock($linkBlock->getParsedText(), $file->getUrl()); + $linkBlock->setBlock($linkBlock->getParsedText(), $file->getUrl(), null, $file->id); } public static function buildFileLink(File $file) : string diff --git a/protected/humhub/modules/content/widgets/richtext/extensions/link/LinkParserBlock.php b/protected/humhub/modules/content/widgets/richtext/extensions/link/LinkParserBlock.php index 6f87eae40f..f5a545a518 100644 --- a/protected/humhub/modules/content/widgets/richtext/extensions/link/LinkParserBlock.php +++ b/protected/humhub/modules/content/widgets/richtext/extensions/link/LinkParserBlock.php @@ -16,6 +16,7 @@ class LinkParserBlock extends Model const BLOCK_KEY_TITLE = 'title'; const BLOCK_KEY_MD = 'orig'; const BLOCK_KEY_TEXT = 'text'; + const BLOCK_KEY_FILE_ID = 'fileId'; /** * @var array @@ -88,6 +89,16 @@ class LinkParserBlock extends Model $this->block[static::BLOCK_KEY_TITLE] = $title; } + public function getFileId() : ?string + { + return $this->block[static::BLOCK_KEY_FILE_ID] ?? null; + } + + public function setFileId(string $fileId = null) + { + $this->block[static::BLOCK_KEY_FILE_ID] = $fileId; + } + public function getParsedText() { return $this->parsedText; @@ -98,11 +109,12 @@ class LinkParserBlock extends Model $this->parsedText = $text; } - public function setBlock(string $text, string $url, string $title = null) + public function setBlock(string $text, string $url, string $title = null, $fileId = null) { $this->setUrl($url); $this->setText($text); $this->setTitle($title); + $this->setFileId($fileId); } public function invalidate() diff --git a/protected/humhub/modules/file/actions/DownloadAction.php b/protected/humhub/modules/file/actions/DownloadAction.php index e106a08edc..6d7f43aa2b 100644 --- a/protected/humhub/modules/file/actions/DownloadAction.php +++ b/protected/humhub/modules/file/actions/DownloadAction.php @@ -8,7 +8,11 @@ namespace humhub\modules\file\actions; +use Firebase\JWT\JWT; +use humhub\modules\file\Module; +use humhub\modules\user\models\User; use Yii; +use yii\helpers\Url; use yii\web\HttpException; use yii\base\Action; use humhub\modules\file\models\File; @@ -50,8 +54,8 @@ class DownloadAction extends Action */ public function init() { - $this->loadFile(Yii::$app->request->get('guid')); - $this->download = (boolean) Yii::$app->request->get('download', false); + $this->loadFile(Yii::$app->request->get('guid'), Yii::$app->request->get('token')); + $this->download = (boolean)Yii::$app->request->get('download', false); $this->loadVariant(Yii::$app->request->get('variant', null)); $this->checkFileExists(); } @@ -62,7 +66,7 @@ class DownloadAction extends Action */ public function beforeRun() { - if(Yii::$app->request->isPjax) { + if (Yii::$app->request->isPjax) { throw new HttpException(400, 'File downloads are not allowed with pjax!'); } @@ -74,10 +78,10 @@ class DownloadAction extends Action } $httpCache = new HttpCache(); - $httpCache->lastModified = function() { + $httpCache->lastModified = function () { return Yii::$app->formatter->asTimestamp($this->file->updated_at); }; - $httpCache->etagSeed = function() { + $httpCache->etagSeed = function () { if (file_exists($this->getStoredFilePath())) { return md5_file($this->getStoredFilePath()); } @@ -114,23 +118,31 @@ class DownloadAction extends Action * Loads the file by given guid * * @param string $guid + * @param string $token * @return File the loaded file instance * @throws HttpException */ - protected function loadFile($guid) + protected function loadFile($guid, $token = null) { $file = File::findOne(['guid' => $guid]); if ($file == null) { throw new HttpException(404, Yii::t('FileModule.base', 'Could not find requested file!')); } - if (!$file->canRead()) { + + $user = nulL; + if ($token !== null) { + $user = static::getUserByDownloadToken($token, $file); + } + + if (!$file->canRead($user)) { throw new HttpException(401, Yii::t('FileModule.base', 'Insufficient permissions!')); } $this->file = $file; } + /** * Loads a variant and verifies * @@ -222,4 +234,60 @@ class DownloadAction extends Action return $this->file->store->get($this->variant); } + /** + * Returns the User model by given JWT token + * + * @param string $token + * @param File $file + * @return User|null + */ + public static function getUserByDownloadToken(string $token, File $file) + { + try { + $decoded = JWT::decode($token, static::getDownloadTokenKey(), ['HS256']); + } catch (\Exception $ex) { + Yii::warning('Could not decode provided JWT token. ' . $ex->getMessage()); + } + if (!empty($decoded['sub']) && !empty($decoded['aud']) && $decoded['aud'] == $file->id) { + return User::findOne(['id' => $decoded['sub']]); + } + + return null; + } + + /** + * Returns a token to access this file by JWT token + * + * @param File $file + * @param User $user + * @return string + */ + public static function generateDownloadToken(File $file, User $user) + { + $token = [ + 'iss' => 'dld-token-v1', + 'sub' => Yii::$app->user->id, + 'aud' => $file->id + ]; + return JWT::encode($token, static::getDownloadTokenKey()); + } + + + /** + * @return string the secret key for file download tokens + * @throws \yii\base\Exception + */ + private static function getDownloadTokenKey() + { + /** @var Module $module */ + $module = Yii::$app->getModule('file'); + + $key = $module->settings->get('downloadTokenKey'); + if (empty($key)) { + $key = Yii::$app->security->generateRandomString(32); + $module->settings->set('downloadTokenKey', $key); + } + + return $key; + } } diff --git a/protected/humhub/modules/file/models/File.php b/protected/humhub/modules/file/models/File.php index 5ed873fcdf..8713555101 100644 --- a/protected/humhub/modules/file/models/File.php +++ b/protected/humhub/modules/file/models/File.php @@ -8,6 +8,7 @@ namespace humhub\modules\file\models; +use humhub\modules\user\models\User; use yii\db\ActiveRecord; use Yii; use yii\helpers\Url; @@ -173,6 +174,9 @@ class File extends FileCompat * * If the file is not an instance of HActiveRecordContent or HActiveRecordContentAddon * the file is readable for all. + + * @param string|User $userId + * @return bool */ public function canRead($userId = "") {