Completed
Pull Request — master (#198)
by Victor
15:33 queued 05:36
created

AvirWrapper::onScanComplete()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 47
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 5

Importance

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