Completed
Push — fix-182 ( d53bbc...3f65e5 )
by Victor
05:34
created

AvirWrapper::file_put_contents()   A

Complexity

Conditions 3
Paths 11

Size

Total Lines 22
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 22
rs 9.2
cc 3
eloc 17
nc 11
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, false, $e);
86
		} catch (\Exception $e){
87
			$message = 	implode(' ', [ __CLASS__, __METHOD__, $e->getMessage()]);
88
			$this->logger->warning($message, ['app' => 'files_antivirus']);
89
		}
90
91
		return false;
92
	}
93
	
94
	/**
95
	 * Asynchronously scan data that are written to the file
96
	 * @param string $path
97
	 * @param string $mode
98
	 * @return resource | bool
99
	 */
100
	public function fopen($path, $mode){
101
		$stream = $this->storage->fopen($path, $mode);
102
103
		if (
104
			is_resource($stream)
105
			&& $this->isWritingMode($mode)
106
			&& $this->isScannableSize($path)
107
		) {
108
			try {
109
				$scanner = $this->scannerFactory->getScanner();
110
				$scanner->initScanner();
111
				return CallBackWrapper::wrap(
112
					$stream,
113
					null,
114
					function ($data) use ($scanner) {
115
						$scanner->onAsyncData($data);
116
					},
117
					function () use ($scanner, $path) {
118
						$this->onScanComplete($scanner, $path, true);
119
					}
120
				);
121
			} catch (InitException $e) {
122
				$message = sprintf(
123
					'Antivirus app is misconfigured or antivirus inaccessible. %s',
124
					$e->getMessage()
125
				);
126
				$this->logger->warning($message, ['app' => 'files_antivirus']);
127
				throw new ForbiddenException($message, false, $e);
128
			} catch (\Exception $e){
129
				$message = 	implode(' ', [ __CLASS__, __METHOD__, $e->getMessage()]);
130
				$this->logger->warning($message, ['app' => 'files_antivirus']);
131
			}
132
		}
133
		return $stream;
134
	}
135
136
	/**
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
		if (intval($status->getNumericStatus()) === \OCA\Files_Antivirus\Status::SCANRESULT_INFECTED) {
145
			$owner = $this->getOwner($path);
146
147
			$this->logger->warning(
148
				'Infected file deleted. ' . $status->getDetails()
149
				. ' Account: ' . $owner . ' Path: ' . $path,
150
				['app' => 'files_antivirus']
151
			);
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
				'',
161
				$owner,
162
				Activity::TYPE_VIRUS_DETECTED,
163
				Activity::PRIORITY_HIGH
164
			);
165
166
			if ($shouldDelete){
167
				//prevent from going to trashbin
168
				if (App::isEnabled('files_trashbin')) {
169
					\OCA\Files_Trashbin\Storage::preRenameHook([
170
						Filesystem::signal_param_oldpath => '',
171
						Filesystem::signal_param_newpath => ''
172
					]);
173
				}
174
				$this->unlink($path);
175
				if (App::isEnabled('files_trashbin')) {
176
					\OCA\Files_Trashbin\Storage::postRenameHook([]);
177
				}
178
			}
179
180
			throw new InvalidContentException(
181
				$this->l10n->t(
182
					'Virus %s is detected in the file. Upload cannot be completed.',
183
					$status->getDetails()
184
				)
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