Completed
Pull Request — master (#225)
by Victor
11:27 queued 09:57
created

AvirWrapper   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 229
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 86.67%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 22
lcom 1
cbo 7
dl 0
loc 229
ccs 91
cts 105
cp 0.8667
rs 10
c 5
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
B file_put_contents() 0 24 5
B onScanComplete() 0 49 5
A isWritingMode() 0 9 1
B isScannableSize() 0 26 4
B fopen() 0 31 6
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 OCA\Files_Antivirus\Status;
16
use OCP\App;
17
use OCP\Files\FileContentNotAllowedException;
18
use OCP\IL10N;
19
use OCP\ILogger;
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
	 *
31
	 * @var array
32
	 */
33
	private $writingModes = array('r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+');
34
35
	/**
36
	 * @var AppConfig
37
	 */
38
	protected $appConfig;
39
40
	/**
41
	 * @var \OCA\Files_Antivirus\ScannerFactory
42
	 */
43
	protected $scannerFactory;
44
	
45
	/**
46
	 * @var IL10N 
47
	 */
48
	protected $l10n;
49
	
50
	/**
51
	 * @var ILogger;
52
	 */
53
	protected $logger;
54
55
	/**
56
	 * @var  RequestHelper
57
	 */
58
	protected $requestHelper;
59
60
	/**
61
	 * @param array $parameters
62
	 */
63 7
	public function __construct($parameters) {
64 7
		parent::__construct($parameters);
65 7
		$this->appConfig = $parameters['appConfig'];
66 7
		$this->scannerFactory = $parameters['scannerFactory'];
67 7
		$this->l10n = $parameters['l10n'];
68 7
		$this->logger = $parameters['logger'];
69 7
		$this->requestHelper = $parameters['requestHelper'];
70 7
	}
71
72
	/**
73
	 * @param string $path
74
	 * @param string $data
75
	 *
76
	 * @return bool
77
	 *
78
	 * @throws ForbiddenException
79
	 */
80 3
	public function file_put_contents($path, $data) {
81
		try {
82 3
			$scanner = $this->scannerFactory->getScanner();
83 3
			$scanner->initScanner();
84 3
			$content = new Content($data, $this->appConfig->getAvChunkSize());
85 3
			while (($chunk = $content->fread()) !== false ) {
86 3
				$scanner->onAsyncData($chunk);
87
			}
88 3
			$this->onScanComplete($scanner, $path, false);
89
90 2
			return parent::file_put_contents($path, $data);
91 1
		} catch (InitException $e) {
92
			$message = \sprintf(self::AV_EXCEPTION_MESSAGE, $e->getMessage());
93
			$this->logger->warning($message, ['app' => 'files_antivirus']);
94
			throw new ForbiddenException($message, true, $e);
95 1
		} catch (FileContentNotAllowedException $e) {
0 ignored issues
show
Bug introduced by
The class OCP\Files\FileContentNotAllowedException 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...
96 1
			throw new ForbiddenException($e->getMessage(), false, $e);
97
		} catch (\Exception $e){
98
			$message = 	\implode(' ', [ __CLASS__, __METHOD__, $e->getMessage()]);
99
			$this->logger->warning($message, ['app' => 'files_antivirus']);
100
		}
101
102
		return false;
103
	}
104
	
105
	/**
106
	 * Asynchronously scan data that are written to the file
107
	 *
108
	 * @param string $path
109
	 * @param string $mode
110
	 *
111
	 * @return resource | bool
112
	 */
113 4
	public function fopen($path, $mode) {
114 4
		$stream = $this->storage->fopen($path, $mode);
115
116 4
		if (\is_resource($stream)
117 4
			&& $this->isWritingMode($mode)
118 4
			&& $this->isScannableSize($path)
119
		) {
120
			try {
121 2
				$scanner = $this->scannerFactory->getScanner();
122 2
				$scanner->initScanner();
123 2
				return CallBackWrapper::wrap(
124 2
					$stream,
125 2
					null,
126 2
					function ($data) use ($scanner) {
127 2
						$scanner->onAsyncData($data);
128 2
					},
129 2
					function () use ($scanner, $path) {
130 2
						$this->onScanComplete($scanner, $path, true);
131 2
					}
132
				);
133
			} catch (InitException $e) {
134
				$message = \sprintf(self::AV_EXCEPTION_MESSAGE, $e->getMessage());
135
				$this->logger->warning($message, ['app' => 'files_antivirus']);
136
				throw new ForbiddenException($message, false, $e);
137
			} catch (\Exception $e){
138
				$message = 	\implode(' ', [ __CLASS__, __METHOD__, $e->getMessage()]);
139
				$this->logger->warning($message, ['app' => 'files_antivirus']);
140
			}
141
		}
142 3
		return $stream;
143
	}
144
145
	/**
146
	 * @param AbstractScanner $scanner
147
	 * @param string $path
148
	 * @param bool $shouldDelete
149
	 *
150
	 * @throws FileContentNotAllowedException
151
	 */
152 5
	private function onScanComplete($scanner, $path, $shouldDelete) {
153 5
		$status = $scanner->completeAsyncScan();
154 5
		if (\intval($status->getNumericStatus()) === Status::SCANRESULT_INFECTED) {
155 3
			$owner = $this->getOwner($path);
156
157 3
			$this->logger->warning(
158 3
				'Infected file deleted. ' . $status->getDetails()
159 3
				. ' Account: ' . $owner . ' Path: ' . $path,
160 3
				['app' => 'files_antivirus']
161
			);
162
163 3
			\OC::$server->getActivityManager()->publishActivity(
164 3
				'files_antivirus',
165 3
				Activity::SUBJECT_VIRUS_DETECTED,
166 3
				[$path, $status->getDetails()],
167 3
				Activity::MESSAGE_FILE_DELETED,
168 3
				[],
169 3
				$path,
170 3
				'',
171 3
				$owner,
172 3
				Activity::TYPE_VIRUS_DETECTED,
173 3
				Activity::PRIORITY_HIGH
174
			);
175
176 3
			if ($shouldDelete) {
177
				//prevent from going to trashbin
178 2
				if (App::isEnabled('files_trashbin')) {
179 2
					\OCA\Files_Trashbin\Storage::preRenameHook(
180
						[
181 2
							Filesystem::signal_param_oldpath => '',
182
							Filesystem::signal_param_newpath => ''
183
						]
184
					);
185
				}
186 2
				$this->unlink($path);
187 2
				if (App::isEnabled('files_trashbin')) {
188 2
					\OCA\Files_Trashbin\Storage::postRenameHook([]);
189
				}
190
			}
191
192 3
			throw new FileContentNotAllowedException(
193 3
				$this->l10n->t(
194 3
					'Virus %s is detected in the file. Upload cannot be completed.',
195 3
					$status->getDetails()
196 3
				), false
197
			);
198
		}
199
200 2
	}
201
	
202
	/**
203
	 * Checks whether passed mode is suitable for writing
204
	 *
205
	 * @param string $mode
206
	 *
207
	 * @return bool
208
	 */
209 4
	private function isWritingMode($mode) {
210
		// Strip unessential binary/text flags
211 4
		$cleanMode = \str_replace(
212 4
			['t', 'b'],
213 4
			['', ''],
214 4
			$mode
215
		);
216 4
		return \in_array($cleanMode, $this->writingModes);
217
	}
218
219
	/**
220
	 * Checks upload size against the av_max_file_size config option
221
	 *
222
	 * @param string $path
223
	 *
224
	 * @return bool
225
	 */
226 4
	private function isScannableSize($path) {
227 4
		$scanSizeLimit = \intval($this->appConfig->getAvMaxFileSize());
228 4
		$size = $this->requestHelper->getUploadSize($path);
229
230
		// No upload in progress. Skip this file.
231 4
		if (\is_null($size)) {
232 3
			$this->logger->debug(
233 3
				'No upload in progress or chunk is being uploaded. Scanning is skipped.',
234 3
				['app' => 'files_antivirus']
235
			);
236 3
			return false;
237
		}
238
239 2
		$matchesLimit = $scanSizeLimit === -1 || $scanSizeLimit >= $size;
240 2
		$action = $matchesLimit ? 'Scanning is scheduled.' : 'Scanning is skipped.';
241 2
		$this->logger->debug(
242 2
			'File size is {filesize}. av_max_file_size is {av_max_file_size}. {action}',
243
			[
244 2
				'app' => 'files_antivirus',
245 2
				'av_max_file_size' => $scanSizeLimit,
246 2
				'filesize' => $size,
247 2
				'action' => $action
248
			]
249
		);
250 2
		return $matchesLimit;
251
	}
252
}
253