Allow to read image URLs with token param (#4995)

Co-authored-by: Lucas Bartholemy <luke-@users.noreply.github.com>
This commit is contained in:
Yuriy Bakhtin 2021-04-19 17:50:36 +03:00 committed by GitHub
parent c481e03787
commit 95233c1c19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 153 additions and 9 deletions

View File

@ -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

View File

@ -0,0 +1,59 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\content\widgets\richtext\converter;
use humhub\modules\content\widgets\richtext\extensions\link\LinkParserBlock;
use humhub\modules\file\actions\DownloadAction;
use humhub\modules\file\models\File;
use humhub\modules\user\models\User;
/**
* This parser can be used to convert HumHub richtext directly to email html in order to view images from email inbox where
* user is not logged in so access is restricted.
*
* @since 1.8.2
*/
class RichTextToEmailHtmlConverter extends RichTextToHtmlConverter
{
/**
* @inheritdoc
*/
protected function renderPlainImage(LinkParserBlock $linkBlock): string
{
return parent::renderPlainImage($this->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;
}
}

View File

@ -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

View File

@ -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()

View File

@ -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;
}
}

View File

@ -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 = "")
{