AvirWrapper::fopen()   B
last analyzed

Complexity

Conditions 6
Paths 8

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7.0143

Importance

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