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
|
|
|
} |
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) { |
|
|
|
|
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
|
|
|
) { |
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
|
2 |
|
} |
124
|
|
|
); |
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
|
|
|
); |
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
|
3 |
|
Activity::PRIORITY_HIGH |
165
|
|
|
); |
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
|
|
|
Filesystem::signal_param_newpath => '' |
173
|
|
|
]); |
174
|
|
|
} |
175
|
2 |
|
$this->unlink($path); |
176
|
2 |
|
if (App::isEnabled('files_trashbin')) { |
177
|
2 |
|
\OCA\Files_Trashbin\Storage::postRenameHook([]); |
178
|
|
|
} |
179
|
|
|
} |
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
|
|
|
) |
186
|
|
|
); |
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
|
4 |
|
$mode |
202
|
|
|
); |
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
|
|
|
); |
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
|
2 |
|
'action' => $action |
234
|
|
|
] |
235
|
|
|
); |
236
|
2 |
|
return $matchesLimit; |
237
|
|
|
} |
238
|
|
|
} |
239
|
|
|
|
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.