Skip to content

Commit

Permalink
add md5 hash validation during upload
Browse files Browse the repository at this point in the history
  • Loading branch information
frasmage committed Apr 1, 2024
1 parent 12607dd commit bef983d
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 15 deletions.
43 changes: 28 additions & 15 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
# Changelog

## 6.0.0

### Compatibility
- Dropped support for PHP 7.4 and 8.0
- Dropped Support for Laravel 8 and 9
- Added Support for PHP 8.3
- Added Support for Laravel 11
- Added support for intervention/image 3.0
- Modernized the database migration files to use more recent Laravel conventions.

### MediaUploader

- Added support for recording alt attributes on Media (database migration required). MediaUploader now exposes a `withAltAttribute()` method to set the alt attribute on the generated media record.
- Added `MediaUploader::applyImageManipulation()` to make changes to the original uploaded image durin the upload process.
- MediaUploader will use the visibility defined on the filesystem disk config if the `makePublic()`/`makePrivate()` methods are not called.
- MediaUploader now supports data URLs as an input source.
- Improved MediaCollection annotions to support generic types
- Removed the `\Plank\Mediable\Stream` class in favor of the `guzzlehttp/psr7` implementation. This removes the direct dependency on the `psr/http-message` library.
- Added `MediaUploader::validateMd5Hash()` to ensure that the md5 hash of the uploaded file matches a particular value during upload.
- MediaUploader will now use the visibility defined on the filesystem disk config if the `makePublic()`/`makePrivate()` methods are not called, instead of assuming public visibility.
- MediaUploader now supports data URL strings as an input source, e.g. `data:image/jpeg;base64,...`.

### SourceAdapters

All SourceAdapter classes have been significantly refactored.
- All sourceAdapters will now never load the entire file contents into memory to determine metadata about the file, in order to avoid memory exhaustion when dealing with large files. If reading the file is necessary, if applicable, the adapter will attempt use a single streamed scan of the file to load all metadata at once, to speed up to the precess.
- Removed `getStreamResource()` method. The method has been replaced with the `getStream()`, which returns a PSR-7 `StreamInterface` instead.
- Added `hash()` method which is expected to return an md5 hash of the file contents.
- The return types of the `path()`, `filename()`, `extension()`, methods are now nullable. If the adapter cannot determine the value from the information available, it should return null.
- If the extension is not available from the source adapter (e.g. source is a raw content string), the MediaUploader will attempt to infer the extension based on the MIME type.
- If the filename is not available from the source adapter (e.g. source is a raw content string), the MediaUploader will throw an exception if the filename is not provided as a configuration to the uploader.
- Removed the `getContents()` method.
- Removed the `getSource()` method

### Media
- Added `alt` attribute to the Media model.
- The Media class now exposes a dynamic `url` attribute which will generate a URL for the file (equivalent to the `getUrl()` method).
- Modernized the migration files to use more recent Laravel conventions.
- All SourceAdapter classes have been significantly refactored.
- All sourceAdapters will now never load the entire file contents into memory to determine metadata about the file, in order to avoid memory exhaustion when dealing with large files.
- If reading the file is necessary, the adapter will attempt use a single streamed scan of the file to load all metadata at once, to speed up to the precess.
- Removed `getStreamResource()` method. The method has been replaced with the `getStream()`, which returns a PSR-7 `StreamInterface` instead.
- Added `hash()` method which is expected to return an md5 hash of the file contents, if available.
- The return types of the `path()`, `filename()`, `extension()`, methods are now nullable. If the adapter cannot determine the value from the information available, it should return null.
- If the extension is not available from the source adapter (e.g. source is a raw content string), the MediaUploader will attempt to infer the extension based on the MIME type.
- If the filename is not available from the source adapter (e.g. source is a raw content string), the MediaUploader will throw an exception if the filename is not provided as a configuration.
- Removed the `getContents()` method.
- Removed the `getSource()` method

### Other
- Improved MediableCollection annotions to support generic types
- Removed the `\Plank\Mediable\Stream` class in favor of the `guzzlehttp/psr7` implementation. This removes the direct dependency on the `psr/http-message` library.

## 5.5.0 - 2022-05-09
- Filename and pathname sanitization will use the app locale when transliterating UTF-8 characters to ascii.
Expand Down
3 changes: 3 additions & 0 deletions docs/source/uploader.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ You can override the most validation configuration values set in ``config/mediab
// only allow files of specific aggregate types
->setAllowedAggregateTypes(['image'])

// ensure that the file contents match a provided md5 hash
->validateMd5Hash('3ef5e70366086147c2695325d79a25cc')

->upload();

You can also validate the file without uploading it by calling the ``verifyFile`` method.
Expand Down
13 changes: 13 additions & 0 deletions src/Exceptions/MediaUpload/InvalidHashException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Plank\Mediable\Exceptions\MediaUpload;

use Plank\Mediable\Exceptions\MediaUploadException;

class InvalidHashException extends MediaUploadException
{
public static function hashMismatch(string $expectedhash, string $actualHash): self
{
return new static("File's md5 hash `{$actualHash}` does not match expected `{$expectedhash}`.");
}
}
33 changes: 33 additions & 0 deletions src/MediaUploader.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Plank\Mediable\Exceptions\MediaUpload\FileNotSupportedException;
use Plank\Mediable\Exceptions\MediaUpload\FileSizeException;
use Plank\Mediable\Exceptions\MediaUpload\ForbiddenException;
use Plank\Mediable\Exceptions\MediaUpload\InvalidHashException;
use Plank\Mediable\Helpers\File;
use Plank\Mediable\SourceAdapters\RawContentAdapter;
use Plank\Mediable\SourceAdapters\SourceAdapterFactory;
Expand Down Expand Up @@ -415,6 +416,19 @@ public function setAllowedAggregateTypes(array $allowedTypes): self
return $this;
}

/**
* Verify the MD5 hash of the file contents matches an expected value.
* The upload process will throw an InvalidHashException if the hash of the
* uploaded file does not match the provided value.
* @param string|null $expectedHash set to null to disable hash validation
* @return $this
*/
public function validateMd5Hash(?string $expectedHash): self
{
$this->config['validate_hash'] = $expectedHash;
return $this;
}

/**
* Make the resulting file public (default behaviour)
* @return $this
Expand Down Expand Up @@ -568,6 +582,7 @@ public function possibleAggregateTypesForExtension(string $extension): array
* @throws FileNotFoundException
* @throws FileNotSupportedException
* @throws FileSizeException
* @throws InvalidHashException
*/
public function upload(): Media
{
Expand Down Expand Up @@ -804,6 +819,8 @@ public function verifyFile(): void
$this->verifyExtension(
$this->source->extension() ?? File::guessExtension($mimeType)
);

$this->verifyMd5Hash();
}

/**
Expand Down Expand Up @@ -924,6 +941,22 @@ private function verifyFileSize(int $size): int
return $size;
}

private function verifyMd5Hash(): void
{
$expectedHash = $this->config['validate_hash'] ?? null;
if ($expectedHash === null) {
return;
}

$actualHash = $this->source->hash();
if ($actualHash !== $expectedHash) {
throw InvalidHashException::hashMismatch(
$expectedHash,
$actualHash
);
}
}

/**
* Verify that the intended destination is available and handle any duplications.
* @param Media $model
Expand Down
32 changes: 32 additions & 0 deletions tests/Integration/MediaUploaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Plank\Mediable\Exceptions\MediaUpload\FileNotSupportedException;
use Plank\Mediable\Exceptions\MediaUpload\FileSizeException;
use Plank\Mediable\Exceptions\MediaUpload\ForbiddenException;
use Plank\Mediable\Exceptions\MediaUpload\InvalidHashException;
use Plank\Mediable\ImageManipulation;
use Plank\Mediable\ImageManipulator;
use Plank\Mediable\Media;
Expand Down Expand Up @@ -886,6 +887,37 @@ public function test_it_ignores_manipulations_for_non_images()
$this->assertEquals(3, $media->size);
}

public function test_it_validates_md5_hash()
{
$this->useDatabase();
$this->useFilesystem('tmp');

$media = Facade::fromSource(TestCase::sampleFilePath())
->toDestination('tmp', 'foo')
->useFilename('bar')
->validateMd5Hash('3ef5e70366086147c2695325d79a25cc')
->upload();

$this->assertInstanceOf(Media::class, $media);
$this->assertTrue($media->fileExists());
$this->assertEquals('tmp', $media->disk);
$this->assertEquals('foo/bar.png', $media->getDiskPath());
$this->assertEquals('image/png', $media->mime_type);
$this->assertEquals(self::TEST_FILE_SIZE, $media->size);
$this->assertEquals('image', $media->aggregate_type);
}

public function test_it_validates_md5_hash_failure()
{
$this->expectException(InvalidHashException::class);

Facade::fromSource(TestCase::sampleFilePath())
->toDestination('tmp', 'foo')
->useFilename('bar')
->validateMd5Hash('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
->upload();
}

protected function getUploader(): MediaUploader
{
return app('mediable.uploader');
Expand Down

0 comments on commit bef983d

Please sign in to comment.