Completed
Push — master ( 46dc92...267b49 )
by Victor
13s queued 11s
created

AvirWrapper::onScanComplete()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 49
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 5

Importance

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