Completed
Pull Request — master (#219)
by Sujith
14:21 queued 06:31
created

AvirWrapper   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 216
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 83.65%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 22
lcom 1
cbo 7
dl 0
loc 216
ccs 87
cts 104
cp 0.8365
rs 10
c 5
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
B file_put_contents() 0 24 5
A __construct() 0 8 1
B fopen() 0 32 6
B onScanComplete() 0 47 5
A isWritingMode() 0 9 1
B isScannableSize() 0 26 4
1
<?php
2
/**
3
 * Copyright (c) 2014 Viktar Dubiniuk <[email protected]>
4
 * This file is licensed under the Affero General Public License version 3 or
5
 * later.
6
 * See the COPYING-README file.
7
 */
8
9
namespace OCA\Files_Antivirus;
10
11
use OC\Files\Filesystem;
12
use OC\Files\Storage\Wrapper\Wrapper;
13
use OCA\Files_Antivirus\Scanner\AbstractScanner;
14
use OCA\Files_Antivirus\Scanner\InitException;
15
use OCP\App;
16
use OCP\Files\ExcludeForbiddenException;
17
use OCP\IL10N;
18
use OCP\ILogger;
19
use OCP\Files\InvalidContentException;
20
use OCP\Files\ForbiddenException;
21
use Icewind\Streams\CallbackWrapper;
22
23
24
class AvirWrapper extends Wrapper{
25
26
	const AV_EXCEPTION_MESSAGE = 'Either the ownCloud antivirus app is misconfigured or the external antivirus service is not accessible. %s';
27
28
	/**
29
	 * Modes that are used for writing
30
	 * @var array
31
	 */
32
	private $writingModes = array('r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+');
33
34
	/**
35
	 * @var AppConfig
36
	 */
37
	protected $appConfig;
38
39
	/**
40
	 * @var \OCA\Files_Antivirus\ScannerFactory
41
	 */
42
	protected $scannerFactory;
43
	
44
	/**
45
	 * @var IL10N 
46
	 */
47
	protected $l10n;
48
	
49
	/**
50
	 * @var ILogger;
51
	 */
52
	protected $logger;
53
54
	/** @var  RequestHelper */
55
	protected $requestHelper;
56
57
	/**
58
	 * @param array $parameters
59
	 */
60 7
	public function __construct($parameters) {
61 7
		parent::__construct($parameters);
62 7
		$this->appConfig = $parameters['appConfig'];
63 7
		$this->scannerFactory = $parameters['scannerFactory'];
64 7
		$this->l10n = $parameters['l10n'];
65 7
		$this->logger = $parameters['logger'];
66 7
		$this->requestHelper = $parameters['requestHelper'];
67 7
	}
68
69
	/**
70
	 * @param string $path
71
	 * @param string $data
72
	 * @return bool
73
	 */
74 3
	public function file_put_contents($path, $data) {
75
		try {
76 3
			$scanner = $this->scannerFactory->getScanner();
77 3
			$scanner->initScanner();
78 3
			$content = new Content($data, $this->appConfig->getAvChunkSize());
79 3
			while (($chunk = $content->fread()) !== false ){
80 3
				$scanner->onAsyncData($chunk);
81
			}
82 3
			$this->onScanComplete($scanner, $path, false);
83
84 2
			return parent::file_put_contents($path, $data);
85 1
		} catch (InitException $e) {
86
			$message = sprintf(self::AV_EXCEPTION_MESSAGE, $e->getMessage());
87
			$this->logger->warning($message, ['app' => 'files_antivirus']);
88
			throw new ForbiddenException($message, true, $e);
89 1
		} catch (InvalidContentException $e) {
0 ignored issues
show
Bug introduced by
The class OCP\Files\InvalidContentException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
90
			throw new ForbiddenException($e->getMessage(), false, $e);
91 1
		} catch (\Exception $e){
92
			$message = 	implode(' ', [ __CLASS__, __METHOD__, $e->getMessage()]);
93
			$this->logger->warning($message, ['app' => 'files_antivirus']);
94
		}
95
96
		return false;
97
	}
98
	
99
	/**
100
	 * Asynchronously scan data that are written to the file
101
	 * @param string $path
102
	 * @param string $mode
103
	 * @return resource | bool
104
	 */
105 4
	public function fopen($path, $mode){
106 4
		$stream = $this->storage->fopen($path, $mode);
107
108
		if (
109 4
			is_resource($stream)
110 4
			&& $this->isWritingMode($mode)
111 4
			&& $this->isScannableSize($path)
112
		) {
113
			try {
114 2
				$scanner = $this->scannerFactory->getScanner();
115 2
				$scanner->initScanner();
116 2
				return CallBackWrapper::wrap(
117 2
					$stream,
118 2
					null,
119 2
					function ($data) use ($scanner) {
120 2
						$scanner->onAsyncData($data);
121 2
					},
122 2
					function () use ($scanner, $path) {
123 2
						$this->onScanComplete($scanner, $path, true);
124 2
					}
125
				);
126
			} catch (InitException $e) {
127
				$message = sprintf(self::AV_EXCEPTION_MESSAGE, $e->getMessage());
128
				$this->logger->warning($message, ['app' => 'files_antivirus']);
129
				throw new ForbiddenException($message, false, $e);
130
			} catch (\Exception $e){
131
				$message = 	implode(' ', [ __CLASS__, __METHOD__, $e->getMessage()]);
132
				$this->logger->warning($message, ['app' => 'files_antivirus']);
133
			}
134
		}
135 3
		return $stream;
136
	}
137
138
	/**
139
	 * @param AbstractScanner $scanner
140
	 * @param string $path
141
	 * @param bool $shouldDelete
142
	 * @throws ExcludeForbiddenException
143
	 */
144 5
	private function onScanComplete($scanner, $path, $shouldDelete){
145 5
		$status = $scanner->completeAsyncScan();
146 5
		if (intval($status->getNumericStatus()) === \OCA\Files_Antivirus\Status::SCANRESULT_INFECTED) {
147 3
			$owner = $this->getOwner($path);
148
149 3
			$this->logger->warning(
150 3
				'Infected file deleted. ' . $status->getDetails()
151 3
				. ' Account: ' . $owner . ' Path: ' . $path,
152 3
				['app' => 'files_antivirus']
153
			);
154
155 3
			\OC::$server->getActivityManager()->publishActivity(
156 3
				'files_antivirus',
157 3
				Activity::SUBJECT_VIRUS_DETECTED,
158 3
				[$path, $status->getDetails()],
159 3
				Activity::MESSAGE_FILE_DELETED,
160 3
				[],
161 3
				$path,
162 3
				'',
163 3
				$owner,
164 3
				Activity::TYPE_VIRUS_DETECTED,
165 3
				Activity::PRIORITY_HIGH
166
			);
167
168 3
			if ($shouldDelete){
169
				//prevent from going to trashbin
170 2
				if (App::isEnabled('files_trashbin')) {
171 2
					\OCA\Files_Trashbin\Storage::preRenameHook([
172 2
						Filesystem::signal_param_oldpath => '',
173
						Filesystem::signal_param_newpath => ''
174
					]);
175
				}
176 2
				$this->unlink($path);
177 2
				if (App::isEnabled('files_trashbin')) {
178 2
					\OCA\Files_Trashbin\Storage::postRenameHook([]);
179
				}
180
			}
181
182 3
			throw new ExcludeForbiddenException(
183
				$this->l10n->t(
184
					'Virus %s is detected in the file. Upload cannot be completed.',
185
					$status->getDetails()
186
				)
187
			);
188
		}
189
190 2
	}
191
	
192
	/**
193
	 * Checks whether passed mode is suitable for writing 
194
	 * @param string $mode
195
	 * @return bool
196
	 */
197 4
	private function isWritingMode($mode){
198
		// Strip unessential binary/text flags
199 4
		$cleanMode = str_replace(
200 4
			['t', 'b'],
201 4
			['', ''],
202 4
			$mode
203
		);
204 4
		return in_array($cleanMode, $this->writingModes);
205
	}
206
207
	/**
208
	 * Checks upload size against the av_max_file_size config option
209
	 *
210
	 * @param string $path
211
	 * @return bool
212
	 */
213 4
	private function isScannableSize($path) {
214 4
		$scanSizeLimit = intval($this->appConfig->getAvMaxFileSize());
215 4
		$size = $this->requestHelper->getUploadSize($path);
216
217
		// No upload in progress. Skip this file.
218 4
		if (is_null($size)){
219 3
			$this->logger->debug(
220 3
				'No upload in progress or chunk is being uploaded. Scanning is skipped.',
221 3
				['app' => 'files_antivirus']
222
			);
223 3
			return false;
224
		}
225
226 2
		$matchesLimit = $scanSizeLimit === -1 || $scanSizeLimit >= $size;
227 2
		$action = $matchesLimit ? 'Scanning is scheduled.' : 'Scanning is skipped.';
228 2
		$this->logger->debug(
229 2
			'File size is {filesize}. av_max_file_size is {av_max_file_size}. {action}',
230
			[
231 2
				'app' => 'files_antivirus',
232 2
				'av_max_file_size' => $scanSizeLimit,
233 2
				'filesize' => $size,
234 2
				'action' => $action
235
			]
236
		);
237 2
		return $matchesLimit;
238
	}
239
}
240