Skip to content

Commit

Permalink
feat: detect apk mimetypes (#423)
Browse files Browse the repository at this point in the history
* feat: detect apk mime

* Apply fixes from StyleCI

* chore: test on 8.4

* chore: add missing translation

---------

Co-authored-by: StyleCI Bot <[email protected]>
  • Loading branch information
imorland and StyleCIBot authored Nov 30, 2024
1 parent 039544d commit 458e061
Show file tree
Hide file tree
Showing 10 changed files with 337 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ jobs:
with:
enable_backend_testing: true
enable_phpstan: true
php_versions: '["8.1", "8.2", "8.3"]'
php_versions: '["8.1", "8.2", "8.3", "8.4"]'

backend_directory: .
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@
}
],
"require": {
"php": "^8.0",
"php": "^8.1",
"ext-json": "*",
"enshrined/svg-sanitize": "^0.15.4",
"enshrined/svg-sanitize": "^0",
"flarum/core": "^1.8.3",
"guzzlehttp/guzzle": "^6.0 || ^7.0",
"ramsey/uuid": "^3.5.2 || ^4",
"softcreatr/php-mime-detector": "^3.0"
"softcreatr/php-mime-detector": "^4.0"
},
"replace": {
"flagrow/upload": "*"
Expand Down
3 changes: 2 additions & 1 deletion extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@
->register(Providers\UtilProvider::class)
->register(Providers\StorageServiceProvider::class)
->register(Providers\DownloadProvider::class)
->register(Providers\SanitizerProvider::class),
->register(Providers\SanitizerProvider::class)
->register(Providers\MimeMappingProvider::class),

(new Extend\View())
->namespace('fof-upload.templates', __DIR__.'/resources/templates'),
Expand Down
3 changes: 2 additions & 1 deletion resources/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ fof-upload:
text-preview_description: |
Inserts a preview (first 5 lines) of the text file, with an option to expand to reveal the full contents of the file.
upload_methods:
aws-s3: S3/Compatible
aws-s3: S3 or Compatible
awss3: AWS S3
imgur: Imgur
local: Local
ovh-svfs: OVH SVFS
Expand Down
17 changes: 4 additions & 13 deletions src/Api/Controllers/InspectMimeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,11 @@
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Server\RequestHandlerInterface;
use SoftCreatR\MimeDetector\MimeDetector;
use SoftCreatR\MimeDetector\MimeDetectorException;

class InspectMimeController implements RequestHandlerInterface
{
public function __construct(
protected FileRepository $files,
protected MimeDetector $mimeDetector
protected FileRepository $files
) {
}

Expand Down Expand Up @@ -60,14 +57,8 @@ public function handle(ServerRequestInterface $request): ResponseInterface
];

try {
$this->mimeDetector->setFile($upload->getPathname());

$uploadFileData = $this->mimeDetector->getFileType();

if (isset($uploadFileData['mime'])) {
$data['mime_detector'] = $uploadFileData['mime'];
}
} catch (MimeDetectorException $e) {
$data['mime_detector'] = $this->files->determineMime($upload);
} catch (Exception $e) {
// Ignore errors. The value will be absent in response
}

Expand All @@ -78,7 +69,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface
}

try {
$data['guessed_extension'] = $upload->guessExtension() ?: 'bin';
$data['guessed_extension'] = $this->files->determineExtension($upload);
} catch (Exception $e) {
// Ignore errors. The value will be absent in response
}
Expand Down
31 changes: 31 additions & 0 deletions src/Mime/Mapping/APK.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of fof/upload.
*
* Copyright (c) FriendsOfFlarum.
* Copyright (c) Flagrow.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FoF\Upload\Mime\Mapping;

class APK extends AbstractMimeMap
{
public static function getMimeType(): string
{
return 'application/vnd.android.package-archive';
}

public static function getExtension(): string
{
return 'apk';
}

public static function getMagicBytes(): array
{
return ["\x50\x4B\x03\x04"]; // ZIP signature
}
}
37 changes: 37 additions & 0 deletions src/Mime/Mapping/AbstractMimeMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

/*
* This file is part of fof/upload.
*
* Copyright (c) FriendsOfFlarum.
* Copyright (c) Flagrow.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FoF\Upload\Mime\Mapping;

abstract class AbstractMimeMap
{
/**
* Get the MIME type.
*
* @return string
*/
abstract public static function getMimeType(): string;

/**
* Get the associated file extension.
*
* @return string
*/
abstract public static function getExtension(): string;

/**
* Get the magic bytes for identification.
*
* @return array
*/
abstract public static function getMagicBytes(): array;
}
205 changes: 205 additions & 0 deletions src/Mime/MimeTypeDetector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<?php

/*
* This file is part of fof/upload.
*
* Copyright (c) FriendsOfFlarum.
* Copyright (c) Flagrow.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FoF\Upload\Mime;

use Flarum\Foundation\ValidationException;
use SoftCreatR\MimeDetector\MimeDetector;
use Symfony\Component\HttpFoundation\File\UploadedFile;

class MimeTypeDetector
{
protected ?string $filePath = null;
protected ?UploadedFile $upload = null;

/**
* Set the file path for MIME type detection.
*
* @param string $filePath
*
* @return $this
*/
public function forFile(string $filePath): self
{
$this->filePath = $filePath;

return $this;
}

/**
* Set the upload object for fallback extension guessing.
*
* @param UploadedFile $upload
*
* @return $this
*/
public function withUpload(UploadedFile $upload): self
{
$this->upload = $upload;

return $this;
}

/**
* Determine the MIME type of the file.
*
* @throws ValidationException
*
* @return string|null
*/
public function getMimeType(): ?string
{
if (!$this->filePath) {
throw new ValidationException(['upload' => 'File path is not set.']);
}

try {
// Use existing MimeDetector library
$mimeDetector = new MimeDetector($this->filePath);
$type = $mimeDetector->getMimeType();

// If the MIME type is detected, return it
if (!empty($type)) {
return $type;
}

// Fallback to PHP's mime_content_type
$type = mime_content_type($this->filePath);

// If mime_content_type returns application/zip or empty, perform magic byte detection
if ($type === 'application/zip' || empty($type)) {
return $this->detectUsingMagicBytes();
}

return $type;
} catch (\Exception $e) {
throw new ValidationException(['upload' => 'Could not detect MIME type.']);
}
}

/**
* Detect MIME type using file magic bytes.
*
* @return string|null
*/
private function detectUsingMagicBytes(): ?string
{
$handle = fopen($this->filePath, 'rb');
if (!$handle) {
return null;
}

$magicBytes = fread($handle, 4); // Read the first 4 bytes
fclose($handle);

foreach ($this->getMappings() as $mapping) {
foreach ($mapping['magicBytes'] as $bytes) {
if ($magicBytes === $bytes) {
// Additional checks for APK-specific files
if ($mapping['extension'] === 'apk' && !$this->isApk($this->filePath)) {
continue; // Not an APK, fallback to other mappings
}

return $mapping['mime'];
}
}
}

return null;
}

/**
* Check if the file is a valid APK by inspecting its contents.
*
* @param string $filePath
*
* @return bool
*/
private function isApk(string $filePath): bool
{
$zip = new \ZipArchive();
if ($zip->open($filePath) === true) {
// APKs should contain "AndroidManifest.xml" and "classes.dex"
$requiredFiles = ['AndroidManifest.xml', 'classes.dex'];
foreach ($requiredFiles as $file) {
if ($zip->locateName($file) === false) {
$zip->close();

return false; // Required APK-specific file not found
}
}
$zip->close();

return true; // All required files found
}

return false; // Not a valid ZIP file
}

/**
* Determine the file extension based on the MIME type or original extension.
*
* @param array $whitelistedExtensions Whitelisted extensions for validation
* @param string|null $originalExtension Original client extension
*
* @return string
*/
public function getFileExtension(array $whitelistedExtensions = [], ?string $originalExtension = null): string
{
// Check if the original extension is in the whitelist
if ($originalExtension && in_array($originalExtension, $whitelistedExtensions)) {
return $originalExtension;
}

// Guess the extension based on MIME type
$mimeType = $this->getMimeType();
$guessedExtension = $this->guessExtensionFromMimeType($mimeType);

// If guessed extension is valid, return it
if ($guessedExtension) {
return $guessedExtension;
}

// Fallback to $upload->guessExtension if $upload is available
if ($this->upload) {
$fallbackExtension = $this->upload->guessExtension();
if ($fallbackExtension) {
return $fallbackExtension;
}
}

return 'bin'; // Default to binary if no extension could be determined
}

/**
* Guess file extension based on MIME type.
*
* @param string|null $mimeType
*
* @return string|null
*/
private function guessExtensionFromMimeType(?string $mimeType): ?string
{
foreach ($this->getMappings() as $mapping) {
if ($mapping['mime'] === $mimeType) {
return $mapping['extension'];
}
}

return null;
}

protected function getMappings(): array
{
return resolve('fof-upload.mime-mappings');
}
}
Loading

0 comments on commit 458e061

Please sign in to comment.