Completed
Push — fix-182 ( 3f65e5...f87a3a )
by Victor
19:52 queued 09:55
created

AvirWrapper::file_put_contents()   B

Complexity

Conditions 4
Paths 16

Size

Total Lines 24
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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