Completed
Push — master ( b732ba...adfdb7 )
by
unknown
03:53
created

AvirWrapper   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 216
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 86.32%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 22
lcom 1
cbo 7
dl 0
loc 216
ccs 101
cts 117
cp 0.8632
rs 10
c 6
b 0
f 0

6 Methods

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