Completed
Push — master ( 4fe26b...4d8bf0 )
by
unknown
03:55
created

AvirWrapper::onScanComplete()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 47
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 47
ccs 39
cts 39
cp 1
rs 8.5125
c 1
b 0
f 0
cc 5
eloc 30
nc 6
nop 3
crap 5
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\IL10N;
17
use OCP\ILogger;
18
use OCP\Files\InvalidContentException;
19
use OCP\Files\ForbiddenException;
20
use Icewind\Streams\CallbackWrapper;
21
22
23
class AvirWrapper extends Wrapper{
24
	
25
	/**
26
	 * Modes that are used for writing 
27
	 * @var array 
28
	 */
29
	private $writingModes = array('r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+');
30
31
	/**
32
	 * @var AppConfig
33
	 */
34
	protected $appConfig;
35
36
	/**
37
	 * @var \OCA\Files_Antivirus\ScannerFactory
38
	 */
39
	protected $scannerFactory;
40
	
41
	/**
42
	 * @var IL10N 
43
	 */
44
	protected $l10n;
45
	
46
	/**
47
	 * @var ILogger;
48
	 */
49
	protected $logger;
50
51
	/** @var  RequestHelper */
52
	protected $requestHelper;
53
54
	/**
55
	 * @param array $parameters
56
	 */
57 7
	public function __construct($parameters) {
58 7
		parent::__construct($parameters);
59 7
		$this->appConfig = $parameters['appConfig'];
60 7
		$this->scannerFactory = $parameters['scannerFactory'];
61 7
		$this->l10n = $parameters['l10n'];
62 7
		$this->logger = $parameters['logger'];
63 7
		$this->requestHelper = $parameters['requestHelper'];
64 7
	}
65
66
	/**
67
	 * @param string $path
68
	 * @param string $data
69
	 * @return bool
70
	 */
71 3
	public function file_put_contents($path, $data) {
72
		try {
73 3
			$scanner = $this->scannerFactory->getScanner();
74 3
			$scanner->initScanner();
75 3
			$content = new Content($data, $this->appConfig->getAvChunkSize());
76 3
			while (($chunk = $content->fread()) !== false ){
77 3
				$scanner->onAsyncData($chunk);
78 3
			}
79 3
			$this->onScanComplete($scanner, $path, false);
80
81 2
			return parent::file_put_contents($path, $data);
82 1
		} catch (InitException $e) {
83
			$message = sprintf(
84
				'Antivirus app is misconfigured or antivirus inaccessible. %s',
85
				$e->getMessage()
86
			);
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 1
			throw new ForbiddenException($e->getMessage(), false, $e);
91
		} 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 4
		) {
113
			try {
114 2
				$scanner = $this->scannerFactory->getScanner();
115 2
				$scanner->initScanner();
116 2
				return CallBackWrapper::wrap(
117 2
					$stream,
118 2
					null,
119
					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
					}
125 2
				);
126
			} catch (InitException $e) {
127
				$message = sprintf(
128
					'Antivirus app is misconfigured or antivirus inaccessible. %s',
129
					$e->getMessage()
130
				);
131
				$this->logger->warning($message, ['app' => 'files_antivirus']);
132
				throw new ForbiddenException($message, false, $e);
133
			} catch (\Exception $e){
134
				$message = 	implode(' ', [ __CLASS__, __METHOD__, $e->getMessage()]);
135
				$this->logger->warning($message, ['app' => 'files_antivirus']);
136 1
			}
137
		}
138 3
		return $stream;
139
	}
140
141
	/**
142
	 * @param AbstractScanner $scanner
143
	 * @param string $path
144
	 * @param bool $shouldDelete
145
	 * @throws InvalidContentException
146
	 */
147 5
	private function onScanComplete($scanner, $path, $shouldDelete){
148 5
		$status = $scanner->completeAsyncScan();
149 5
		if (intval($status->getNumericStatus()) === \OCA\Files_Antivirus\Status::SCANRESULT_INFECTED) {
150 3
			$owner = $this->getOwner($path);
151
152 3
			$this->logger->warning(
153 3
				'Infected file deleted. ' . $status->getDetails()
154 3
				. ' Account: ' . $owner . ' Path: ' . $path,
155 3
				['app' => 'files_antivirus']
156 3
			);
157
158 3
			\OC::$server->getActivityManager()->publishActivity(
159 3
				'files_antivirus',
160 3
				Activity::SUBJECT_VIRUS_DETECTED,
161 3
				[$path, $status->getDetails()],
162 3
				Activity::MESSAGE_FILE_DELETED,
163 3
				[],
164 3
				$path,
165 3
				'',
166 3
				$owner,
167 3
				Activity::TYPE_VIRUS_DETECTED,
168
				Activity::PRIORITY_HIGH
169 3
			);
170
171 3
			if ($shouldDelete){
172
				//prevent from going to trashbin
173 2
				if (App::isEnabled('files_trashbin')) {
174 2
					\OCA\Files_Trashbin\Storage::preRenameHook([
175 2
						Filesystem::signal_param_oldpath => '',
176 2
						Filesystem::signal_param_newpath => ''
177 2
					]);
178 2
				}
179 2
				$this->unlink($path);
180 2
				if (App::isEnabled('files_trashbin')) {
181 2
					\OCA\Files_Trashbin\Storage::postRenameHook([]);
182 2
				}
183 2
			}
184
185 3
			throw new InvalidContentException(
186 3
				$this->l10n->t(
187 3
					'Virus %s is detected in the file. Upload cannot be completed.',
188 3
					$status->getDetails()
189 3
				)
190 3
			);
191
		}
192
193 2
	}
194
	
195
	/**
196
	 * Checks whether passed mode is suitable for writing 
197
	 * @param string $mode
198
	 * @return bool
199
	 */
200 4
	private function isWritingMode($mode){
201
		// Strip unessential binary/text flags
202 4
		$cleanMode = str_replace(
203 4
			['t', 'b'],
204 4
			['', ''],
205
			$mode
206 4
		);
207 4
		return in_array($cleanMode, $this->writingModes);
208
	}
209
210
	/**
211
	 * Checks upload size against the av_max_file_size config option
212
	 *
213
	 * @param string $path
214
	 * @return bool
215
	 */
216 4
	private function isScannableSize($path) {
217 4
		$scanSizeLimit = intval($this->appConfig->getAvMaxFileSize());
218 4
		$size = $this->requestHelper->getUploadSize($path);
219
220
		// No upload in progress. Skip this file.
221 4
		if (is_null($size)){
222 3
			$this->logger->debug(
223 3
				'No upload in progress or chunk is being uploaded. Scanning is skipped.',
224 3
				['app' => 'files_antivirus']
225 3
			);
226 3
			return false;
227
		}
228
229 2
		$matchesLimit = $scanSizeLimit === -1 || $scanSizeLimit >= $size;
230 2
		$action = $matchesLimit ? 'Scanning is scheduled.' : 'Scanning is skipped.';
231 2
		$this->logger->debug(
232 2
			'File size is {filesize}. av_max_file_size is {av_max_file_size}. {action}',
233
			[
234 2
				'app' => 'files_antivirus',
235 2
				'av_max_file_size' => $scanSizeLimit,
236 2
				'filesize' => $size,
237
				'action' => $action
238 2
			]
239 2
		);
240 2
		return $matchesLimit;
241
	}
242
}
243