Skip to content

Commit

Permalink
Remove validator overwrite (#63)
Browse files Browse the repository at this point in the history
* Extract validation logic into own class

* Deprecate the custom validator

The is no need for a custom validator instance. It is bad practice for 3rd party packages to define rules this way.

* Adapt service provider, install validation rule as "extension"

Also, removed the custom resolver  - the logic that is overwriting an application's  validator instance.

* Add test helper

Helper is responsible for creating validator instance with desired validation rules...

* Adapt unit tests

* Remove IDE injected PHPDoc

* Fix style
  • Loading branch information
aedart authored Oct 1, 2024
1 parent 4d74b48 commit 9aa898e
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 80 deletions.
5 changes: 5 additions & 0 deletions src/ClamavValidator/ClamavValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
use Socket\Raw\Factory as SocketFactory;
use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
* @deprecated Use {@see \Sunspikes\ClamavValidator\Rules\ClamAv} validation rule instead.
*
* Clamav Validator
*/
class ClamavValidator extends Validator
{
/**
Expand Down
32 changes: 12 additions & 20 deletions src/ClamavValidator/ClamavValidatorServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Sunspikes\ClamavValidator\Rules\ClamAv;

class ClamavValidatorServiceProvider extends ServiceProvider
{
Expand All @@ -13,7 +14,7 @@ class ClamavValidatorServiceProvider extends ServiceProvider
* @var array
*/
protected $rules = [
'clamav',
'clamav' => ClamAv::class,
];

/**
Expand All @@ -28,21 +29,12 @@ public function boot()
$this->publishes([
__DIR__ . '/../../config/clamav.php' => $this->app->configPath('clamav.php'),
], 'config');

$this->publishes([
__DIR__ . '/../lang' => method_exists($this->app, 'langPath') ?
$this->app->langPath().'/vendor/clamav-validator'
: $this->app->resourcePath('lang/vendor/clamav-validator'),
], 'lang');
$this->app['validator']
->resolver(function ($translator, $data, $rules, $messages, $customAttributes = []) {
return new ClamavValidator(
$translator,
$data,
$rules,
$messages,
$customAttributes
);
});

$this->addNewRules();
}
Expand All @@ -57,32 +49,32 @@ public function getRules(): array
return $this->rules;
}


/**
* Add new rules to the validator.
*/
protected function addNewRules()
{
foreach ($this->getRules() as $rule) {
$this->extendValidator($rule);
foreach ($this->getRules() as $token => $rule) {
$this->extendValidator($token, $rule);
}
}


/**
* Extend the validator with new rules.
*
* @param string $token
* @param string $rule
*
* @return void
*/
protected function extendValidator(string $rule)
protected function extendValidator(string $token, string $rule)
{
$method = Str::studly($rule);
$translation = $this->app['translator']->get('clamav-validator::validation');

$this->app['validator']->extend(
$rule,
ClamavValidator::class . '@validate' . $method,
$translation[$rule] ?? []
$token,
$rule . '@validate',
$translation[$token] ?? []
);
}

Expand Down
138 changes: 138 additions & 0 deletions src/ClamavValidator/Rules/ClamAv.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

namespace Sunspikes\ClamavValidator\Rules;

use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Socket\Raw\Factory as SocketFactory;
use Sunspikes\ClamavValidator\ClamavValidatorException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Xenolope\Quahog\Client as QuahogClient;

/**
* ClamAV Validation Rule (Extension)
*/
class ClamAv
{
/**
* Validate the uploaded file for virus/malware with ClamAV.
*
* @param $attribute string
* @param $value mixed
* @param $parameters array
*
* @return bool
* @throws ClamavValidatorException
*/
public function validate(string $attribute, $value, array $parameters): bool
{
if (filter_var(Config::get('clamav.skip_validation'), FILTER_VALIDATE_BOOLEAN)) {
return true;
}

if(is_array($value)) {
$result = true;
foreach($value as $file) {
$result &= $this->validateFileWithClamAv($file);
}

return $result;
}

return $this->validateFileWithClamAv($value);
}

/**
* Validate the single uploaded file for virus/malware with ClamAV.
*
* @param $value mixed
*
* @return bool
* @throws ClamavValidatorException
*/
protected function validateFileWithClamAv($value): bool
{
$file = $this->getFilePath($value);
if (! is_readable($file)) {
throw ClamavValidatorException::forNonReadableFile($file);
}

try {
$socket = $this->getClamavSocket();
$scanner = $this->createQuahogScannerClient($socket);
$result = $scanner->scanResourceStream(fopen($file, 'rb'));
} catch (Exception $exception) {
if (Config::get('clamav.client_exceptions')) {
throw ClamavValidatorException::forClientException($exception);
}
return false;
}

if ($result->isError()) {
if (Config::get('clamav.client_exceptions')) {
throw ClamavValidatorException::forScanResult($result);
}
return false;
}

// Check if scan result is clean
return $result->isOk();
}

/**
* Guess the ClamAV socket.
*
* @return string
*/
protected function getClamavSocket(): string
{
$preferredSocket = Config::get('clamav.preferred_socket');

if ($preferredSocket === 'unix_socket') {
$unixSocket = Config::get('clamav.unix_socket');
if (file_exists($unixSocket)) {
return 'unix://' . $unixSocket;
}
}

// We use the tcp_socket as fallback as well
return Config::get('clamav.tcp_socket');
}

/**
* Return the file path from the passed object.
*
* @param mixed $file
* @return string
*/
protected function getFilePath($file): string
{
// if were passed an instance of UploadedFile, return the path
if ($file instanceof UploadedFile) {
return $file->getRealPath();
}

// if we're passed a PHP file upload array, return the "tmp_name"
if (is_array($file) && null !== Arr::get($file, 'tmp_name')) {
return $file['tmp_name'];
}

// fallback: we were likely passed a path already
return $file;
}

/**
* Create a new quahog ClamAV scanner client.
*
* @param string $socket
* @return QuahogClient
*/
protected function createQuahogScannerClient(string $socket): QuahogClient
{
// Create a new client socket instance
$client = (new SocketFactory())->createClient($socket, Config::get('clamav.socket_connect_timeout'));

return new QuahogClient($client, Config::get('clamav.socket_read_timeout'), PHP_NORMAL_READ);
}
}
10 changes: 5 additions & 5 deletions tests/ClamavValidatorServiceProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,18 @@ public function testBoot()

Facade::setFacadeApplication($container);

$sp = new ClamavValidatorServiceProvider($container);
$sp->boot();
$serviceProvider = new ClamavValidatorServiceProvider($container);
$serviceProvider->boot();

$validator = $factory->make([], []);

foreach ($validator->extensions as $rule => $class_and_method) {

$this->assertTrue(in_array($rule, $sp->getRules()));
$this->assertEquals(ClamavValidator::class .'@validate' . Str::studly($rule), $class_and_method);
// Ensure rule exists in service provider ~ that validator has installed it
$this->assertArrayHasKey($rule, $serviceProvider->getRules());

// Ensure that validation rule's validate method can be invoked...
list($class, $method) = Str::parseCallback($class_and_method);

$this->assertTrue(method_exists($class, $method));
}
}
Expand Down
Loading

0 comments on commit 9aa898e

Please sign in to comment.