1
0
mirror of https://github.com/flarum/core.git synced 2025-08-05 16:07:34 +02:00

Merge pull request #2117 from flarum/fl/2055-streamline-uploads

Simplify uploads, avoid Application contract
This commit is contained in:
Franz Liedke
2020-04-15 22:52:03 +02:00
committed by GitHub
8 changed files with 138 additions and 109 deletions

View File

@@ -56,7 +56,7 @@
"illuminate/support": "5.7.*", "illuminate/support": "5.7.*",
"illuminate/validation": "5.7.*", "illuminate/validation": "5.7.*",
"illuminate/view": "5.7.*", "illuminate/view": "5.7.*",
"intervention/image": "^2.3.0", "intervention/image": "^2.5.0",
"laminas/laminas-diactoros": "^1.8.4", "laminas/laminas-diactoros": "^1.8.4",
"laminas/laminas-httphandlerrunner": "^1.0", "laminas/laminas-httphandlerrunner": "^1.0",
"laminas/laminas-stratigility": "^3.0", "laminas/laminas-stratigility": "^3.0",

View File

@@ -9,12 +9,10 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Foundation\Application;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AssertPermissionTrait; use Flarum\User\AssertPermissionTrait;
use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\EmptyResponse;
use League\Flysystem\Adapter\Local; use League\Flysystem\FilesystemInterface;
use League\Flysystem\Filesystem;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
class DeleteFaviconController extends AbstractDeleteController class DeleteFaviconController extends AbstractDeleteController
@@ -27,17 +25,18 @@ class DeleteFaviconController extends AbstractDeleteController
protected $settings; protected $settings;
/** /**
* @var Application * @var FilesystemInterface
*/ */
protected $app; protected $uploadDir;
/** /**
* @param SettingsRepositoryInterface $settings * @param SettingsRepositoryInterface $settings
* @param FilesystemInterface $uploadDir
*/ */
public function __construct(SettingsRepositoryInterface $settings, Application $app) public function __construct(SettingsRepositoryInterface $settings, FilesystemInterface $uploadDir)
{ {
$this->settings = $settings; $this->settings = $settings;
$this->app = $app; $this->uploadDir = $uploadDir;
} }
/** /**
@@ -51,10 +50,8 @@ class DeleteFaviconController extends AbstractDeleteController
$this->settings->set('favicon_path', null); $this->settings->set('favicon_path', null);
$uploadDir = new Filesystem(new Local($this->app->publicPath().'/assets')); if ($this->uploadDir->has($path)) {
$this->uploadDir->delete($path);
if ($uploadDir->has($path)) {
$uploadDir->delete($path);
} }
return new EmptyResponse(204); return new EmptyResponse(204);

View File

@@ -9,12 +9,10 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Foundation\Application;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AssertPermissionTrait; use Flarum\User\AssertPermissionTrait;
use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\EmptyResponse;
use League\Flysystem\Adapter\Local; use League\Flysystem\FilesystemInterface;
use League\Flysystem\Filesystem;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
class DeleteLogoController extends AbstractDeleteController class DeleteLogoController extends AbstractDeleteController
@@ -27,17 +25,18 @@ class DeleteLogoController extends AbstractDeleteController
protected $settings; protected $settings;
/** /**
* @var Application * @var FilesystemInterface
*/ */
protected $app; protected $uploadDir;
/** /**
* @param SettingsRepositoryInterface $settings * @param SettingsRepositoryInterface $settings
* @param FilesystemInterface $uploadDir
*/ */
public function __construct(SettingsRepositoryInterface $settings, Application $app) public function __construct(SettingsRepositoryInterface $settings, FilesystemInterface $uploadDir)
{ {
$this->settings = $settings; $this->settings = $settings;
$this->app = $app; $this->uploadDir = $uploadDir;
} }
/** /**
@@ -51,10 +50,8 @@ class DeleteLogoController extends AbstractDeleteController
$this->settings->set('logo_path', null); $this->settings->set('logo_path', null);
$uploadDir = new Filesystem(new Local($this->app->publicPath().'/assets')); if ($this->uploadDir->has($path)) {
$this->uploadDir->delete($path);
if ($uploadDir->has($path)) {
$uploadDir->delete($path);
} }
return new EmptyResponse(204); return new EmptyResponse(204);

View File

@@ -9,15 +9,12 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Foundation\Application;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AssertPermissionTrait; use Flarum\User\AssertPermissionTrait;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
use League\Flysystem\Adapter\Local; use League\Flysystem\FilesystemInterface;
use League\Flysystem\Filesystem;
use League\Flysystem\MountManager;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Document;
@@ -31,17 +28,18 @@ class UploadFaviconController extends ShowForumController
protected $settings; protected $settings;
/** /**
* @var Application * @var FilesystemInterface
*/ */
protected $app; protected $uploadDir;
/** /**
* @param SettingsRepositoryInterface $settings * @param SettingsRepositoryInterface $settings
* @param FilesystemInterface $uploadDir
*/ */
public function __construct(SettingsRepositoryInterface $settings, Application $app) public function __construct(SettingsRepositoryInterface $settings, FilesystemInterface $uploadDir)
{ {
$this->settings = $settings; $this->settings = $settings;
$this->app = $app; $this->uploadDir = $uploadDir;
} }
/** /**
@@ -52,36 +50,28 @@ class UploadFaviconController extends ShowForumController
$this->assertAdmin($request->getAttribute('actor')); $this->assertAdmin($request->getAttribute('actor'));
$file = Arr::get($request->getUploadedFiles(), 'favicon'); $file = Arr::get($request->getUploadedFiles(), 'favicon');
$tmpFile = tempnam($this->app->storagePath().'/tmp', 'favicon');
$file->moveTo($tmpFile);
$extension = pathinfo($file->getClientFilename(), PATHINFO_EXTENSION); $extension = pathinfo($file->getClientFilename(), PATHINFO_EXTENSION);
if ($extension !== 'ico') { if ($extension === 'ico') {
$image = $file->getStream();
} else {
$manager = new ImageManager; $manager = new ImageManager;
$encodedImage = $manager->make($tmpFile)->resize(64, 64, function ($constraint) { $image = $manager->make($file->getStream())->resize(64, 64, function ($constraint) {
$constraint->aspectRatio(); $constraint->aspectRatio();
$constraint->upsize(); $constraint->upsize();
})->encode('png'); })->encode('png');
file_put_contents($tmpFile, $encodedImage);
$extension = 'png'; $extension = 'png';
} }
$mount = new MountManager([ if (($path = $this->settings->get('favicon_path')) && $this->uploadDir->has($path)) {
'source' => new Filesystem(new Local(pathinfo($tmpFile, PATHINFO_DIRNAME))), $this->uploadDir->delete($path);
'target' => new Filesystem(new Local($this->app->publicPath().'/assets')),
]);
if (($path = $this->settings->get('favicon_path')) && $mount->has($file = "target://$path")) {
$mount->delete($file);
} }
$uploadName = 'favicon-'.Str::lower(Str::random(8)).'.'.$extension; $uploadName = 'favicon-'.Str::lower(Str::random(8)).'.'.$extension;
$mount->move('source://'.pathinfo($tmpFile, PATHINFO_BASENAME), "target://$uploadName"); $this->uploadDir->write($uploadName, $image);
$this->settings->set('favicon_path', $uploadName); $this->settings->set('favicon_path', $uploadName);

View File

@@ -9,15 +9,12 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Foundation\Application;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AssertPermissionTrait; use Flarum\User\AssertPermissionTrait;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
use League\Flysystem\Adapter\Local; use League\Flysystem\FilesystemInterface;
use League\Flysystem\Filesystem;
use League\Flysystem\MountManager;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Document;
@@ -31,17 +28,18 @@ class UploadLogoController extends ShowForumController
protected $settings; protected $settings;
/** /**
* @var Application * @var FilesystemInterface
*/ */
protected $app; protected $uploadDir;
/** /**
* @param SettingsRepositoryInterface $settings * @param SettingsRepositoryInterface $settings
* @param FilesystemInterface $uploadDir
*/ */
public function __construct(SettingsRepositoryInterface $settings, Application $app) public function __construct(SettingsRepositoryInterface $settings, FilesystemInterface $uploadDir)
{ {
$this->settings = $settings; $this->settings = $settings;
$this->app = $app; $this->uploadDir = $uploadDir;
} }
/** /**
@@ -53,28 +51,19 @@ class UploadLogoController extends ShowForumController
$file = Arr::get($request->getUploadedFiles(), 'logo'); $file = Arr::get($request->getUploadedFiles(), 'logo');
$tmpFile = tempnam($this->app->storagePath().'/tmp', 'logo');
$file->moveTo($tmpFile);
$manager = new ImageManager; $manager = new ImageManager;
$encodedImage = $manager->make($tmpFile)->heighten(60, function ($constraint) { $encodedImage = $manager->make($file->getStream())->heighten(60, function ($constraint) {
$constraint->upsize(); $constraint->upsize();
})->encode('png'); })->encode('png');
file_put_contents($tmpFile, $encodedImage);
$mount = new MountManager([ if (($path = $this->settings->get('logo_path')) && $this->uploadDir->has($path)) {
'source' => new Filesystem(new Local(pathinfo($tmpFile, PATHINFO_DIRNAME))), $this->uploadDir->delete($path);
'target' => new Filesystem(new Local($this->app->publicPath().'/assets')),
]);
if (($path = $this->settings->get('logo_path')) && $mount->has($file = "target://$path")) {
$mount->delete($file);
} }
$uploadName = 'logo-'.Str::lower(Str::random(8)).'.png'; $uploadName = 'logo-'.Str::lower(Str::random(8)).'.png';
$mount->move('source://'.pathinfo($tmpFile, PATHINFO_BASENAME), "target://$uploadName"); $this->uploadDir->write($uploadName, $encodedImage);
$this->settings->set('logo_path', $uploadName); $this->settings->set('logo_path', $uploadName);

View File

@@ -9,8 +9,15 @@
namespace Flarum\Settings; namespace Flarum\Settings;
use Flarum\Api\Controller\DeleteFaviconController;
use Flarum\Api\Controller\DeleteLogoController;
use Flarum\Api\Controller\UploadFaviconController;
use Flarum\Api\Controller\UploadLogoController;
use Flarum\Foundation\AbstractServiceProvider; use Flarum\Foundation\AbstractServiceProvider;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Filesystem\Factory;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use League\Flysystem\FilesystemInterface;
class SettingsServiceProvider extends AbstractServiceProvider class SettingsServiceProvider extends AbstractServiceProvider
{ {
@@ -28,5 +35,18 @@ class SettingsServiceProvider extends AbstractServiceProvider
}); });
$this->app->alias(SettingsRepositoryInterface::class, 'flarum.settings'); $this->app->alias(SettingsRepositoryInterface::class, 'flarum.settings');
$assets = function (Container $app) {
return $app->make(Factory::class)->disk('flarum-assets')->getDriver();
};
$this->app->when([
DeleteFaviconController::class,
DeleteLogoController::class,
UploadFaviconController::class,
UploadLogoController::class,
])
->needs(FilesystemInterface::class)
->give($assets);
} }
} }

View File

@@ -10,14 +10,76 @@
namespace Flarum\User; namespace Flarum\User;
use Flarum\Foundation\AbstractValidator; use Flarum\Foundation\AbstractValidator;
use Flarum\Foundation\ValidationException;
use Psr\Http\Message\UploadedFileInterface;
use Symfony\Component\Mime\MimeTypes;
class AvatarValidator extends AbstractValidator class AvatarValidator extends AbstractValidator
{ {
protected $rules = [ /**
'avatar' => [ * Throw an exception if a model is not valid.
'required', *
'mimes:jpeg,png,bmp,gif', * @param array $attributes
'max:2048' */
] public function assertValid(array $attributes)
]; {
$this->assertFileRequired($attributes['avatar']);
$this->assertFileMimes($attributes['avatar']);
$this->assertFileSize($attributes['avatar']);
}
protected function assertFileRequired(UploadedFileInterface $file)
{
if ($file->getError() !== UPLOAD_ERR_OK) {
$this->raise('required');
}
}
protected function assertFileMimes(UploadedFileInterface $file)
{
$allowedTypes = $this->getAllowedTypes();
// Block PHP files masquerading as images
$phpExtensions = ['php', 'php3', 'php4', 'php5', 'phtml'];
$fileExtension = pathinfo($file->getClientFilename(), PATHINFO_EXTENSION);
if (in_array(trim(strtolower($fileExtension)), $phpExtensions)) {
$this->raise('mimes', [':values' => implode(', ', $allowedTypes)]);
}
$guessedExtension = MimeTypes::getDefault()->getExtensions($file->getClientMediaType())[0] ?? null;
if (! in_array($guessedExtension, $allowedTypes)) {
$this->raise('mimes', [':values' => implode(', ', $allowedTypes)]);
}
}
protected function assertFileSize(UploadedFileInterface $file)
{
$maxSize = $this->getMaxSize();
if ($file->getSize() / 1024 > $maxSize) {
$this->raise('max.file', [':max' => $maxSize]);
}
}
protected function raise($error, array $parameters = [])
{
$message = $this->translator->trans(
"validation.$error",
$parameters + [':attribute' => 'avatar']
);
throw new ValidationException(['avatar' => $message]);
}
protected function getMaxSize()
{
return 2048;
}
protected function getAllowedTypes()
{
return ['jpeg', 'png', 'bmp', 'gif'];
}
} }

View File

@@ -9,7 +9,6 @@
namespace Flarum\User\Command; namespace Flarum\User\Command;
use Flarum\Foundation\Application;
use Flarum\Foundation\DispatchEventsTrait; use Flarum\Foundation\DispatchEventsTrait;
use Flarum\User\AssertPermissionTrait; use Flarum\User\AssertPermissionTrait;
use Flarum\User\AvatarUploader; use Flarum\User\AvatarUploader;
@@ -18,7 +17,6 @@ use Flarum\User\Event\AvatarSaving;
use Flarum\User\UserRepository; use Flarum\User\UserRepository;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class UploadAvatarHandler class UploadAvatarHandler
{ {
@@ -30,11 +28,6 @@ class UploadAvatarHandler
*/ */
protected $users; protected $users;
/**
* @var Application
*/
protected $app;
/** /**
* @var AvatarUploader * @var AvatarUploader
*/ */
@@ -48,15 +41,13 @@ class UploadAvatarHandler
/** /**
* @param Dispatcher $events * @param Dispatcher $events
* @param UserRepository $users * @param UserRepository $users
* @param Application $app
* @param AvatarUploader $uploader * @param AvatarUploader $uploader
* @param AvatarValidator $validator * @param AvatarValidator $validator
*/ */
public function __construct(Dispatcher $events, UserRepository $users, Application $app, AvatarUploader $uploader, AvatarValidator $validator) public function __construct(Dispatcher $events, UserRepository $users, AvatarUploader $uploader, AvatarValidator $validator)
{ {
$this->events = $events; $this->events = $events;
$this->users = $users; $this->users = $users;
$this->app = $app;
$this->uploader = $uploader; $this->uploader = $uploader;
$this->validator = $validator; $this->validator = $validator;
} }
@@ -65,6 +56,7 @@ class UploadAvatarHandler
* @param UploadAvatar $command * @param UploadAvatar $command
* @return \Flarum\User\User * @return \Flarum\User\User
* @throws \Flarum\User\Exception\PermissionDeniedException * @throws \Flarum\User\Exception\PermissionDeniedException
* @throws \Flarum\Foundation\ValidationException
*/ */
public function handle(UploadAvatar $command) public function handle(UploadAvatar $command)
{ {
@@ -76,35 +68,17 @@ class UploadAvatarHandler
$this->assertCan($actor, 'edit', $user); $this->assertCan($actor, 'edit', $user);
} }
$file = $command->file; $this->validator->assertValid(['avatar' => $command->file]);
$tmpFile = tempnam($this->app->storagePath().'/tmp', 'avatar'); $image = (new ImageManager)->make($command->file->getStream());
$file->moveTo($tmpFile);
try { $this->events->dispatch(
$file = new UploadedFile( new AvatarSaving($user, $actor, $image)
$tmpFile, );
$file->getClientFilename(),
$file->getClientMediaType(),
$file->getSize(),
$file->getError(),
true
);
$this->validator->assertValid(['avatar' => $file]); $this->uploader->upload($user, $image);
$image = (new ImageManager)->make($tmpFile); $user->save();
$this->events->dispatch(
new AvatarSaving($user, $actor, $image)
);
$this->uploader->upload($user, $image);
$user->save();
} finally {
@unlink($tmpFile);
}
return $user; return $user;
} }