1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
/** |
5
|
|
|
* Copyright (c) 2012 Bart Visscher <[email protected]> |
6
|
|
|
* This file is licensed under the Affero General Public License version 3 or |
7
|
|
|
* later. |
8
|
|
|
* See the COPYING-README file. |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace OCA\Files_Antivirus\BackgroundJob; |
12
|
|
|
|
13
|
|
|
use OC\BackgroundJob\TimedJob; |
14
|
|
|
use OCA\Files_Antivirus\AppConfig; |
15
|
|
|
use OCA\Files_Antivirus\ItemFactory; |
16
|
|
|
use OCA\Files_Antivirus\Scanner\ScannerFactory; |
17
|
|
|
use OCP\DB\QueryBuilder\IQueryBuilder; |
18
|
|
|
use OCP\Files\File; |
19
|
|
|
use OCP\Files\IMimeTypeLoader; |
20
|
|
|
use OCP\IDBConnection; |
21
|
|
|
use OCP\Files\IRootFolder; |
22
|
|
|
use OCP\ILogger; |
23
|
|
|
use OCP\IUser; |
24
|
|
|
use OCP\IUserManager; |
25
|
|
|
|
26
|
|
|
class BackgroundScanner extends TimedJob { |
27
|
|
|
/** @var IRootFolder */ |
28
|
|
|
protected $rootFolder; |
29
|
|
|
|
30
|
|
|
/** @var ScannerFactory */ |
31
|
|
|
private $scannerFactory; |
32
|
|
|
|
33
|
|
|
/** @var AppConfig */ |
34
|
|
|
private $appConfig; |
35
|
|
|
|
36
|
|
|
/** @var ILogger */ |
37
|
|
|
protected $logger; |
38
|
|
|
|
39
|
|
|
/** @var IUserManager */ |
40
|
|
|
protected $userManager; |
41
|
|
|
|
42
|
|
|
/** @var IDBConnection */ |
43
|
|
|
protected $db; |
44
|
|
|
|
45
|
|
|
/** @var IMimeTypeLoader */ |
46
|
|
|
protected $mimeTypeLoader; |
47
|
|
|
|
48
|
|
|
/** @var ItemFactory */ |
49
|
|
|
protected $itemFactory; |
50
|
|
|
/** @var bool */ |
51
|
|
|
private $isCLI; |
52
|
|
|
|
53
|
|
|
public function __construct(ScannerFactory $scannerFactory, |
54
|
|
|
AppConfig $appConfig, |
55
|
|
|
IRootFolder $rootFolder, |
56
|
|
|
ILogger $logger, |
57
|
|
|
IUserManager $userManager, |
58
|
|
|
IDBConnection $db, |
59
|
|
|
IMimeTypeLoader $mimeTypeLoader, |
60
|
|
|
ItemFactory $itemFactory, |
61
|
|
|
bool $isCLI |
62
|
|
|
) { |
63
|
|
|
$this->rootFolder = $rootFolder; |
64
|
|
|
$this->scannerFactory = $scannerFactory; |
65
|
|
|
$this->appConfig = $appConfig; |
66
|
|
|
$this->logger = $logger; |
67
|
|
|
$this->userManager = $userManager; |
68
|
|
|
$this->db = $db; |
69
|
|
|
$this->mimeTypeLoader = $mimeTypeLoader; |
70
|
|
|
$this->itemFactory = $itemFactory; |
71
|
|
|
$this->isCLI = $isCLI; |
72
|
|
|
|
73
|
|
|
// Run once per 15 minutes |
74
|
|
|
$this->setInterval(60 * 15); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Background scanner main job |
79
|
|
|
*/ |
80
|
|
|
public function run($args): void { |
81
|
|
|
if ($this->appConfig->getAppValue('av_background_scan') !== 'on') { |
82
|
|
|
// Background checking disabled no need to continue |
83
|
|
|
$this->logger->debug('Antivirus background scan disablled, skipping'); |
|
|
|
|
84
|
|
|
return; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
// locate files that are not checked yet |
88
|
|
|
try { |
89
|
|
|
$result = $this->getUnscannedFiles(); |
90
|
|
|
} catch (\Exception $e) { |
91
|
|
|
$this->logger->logException($e); |
|
|
|
|
92
|
|
|
return; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
$this->logger->debug('Start background scan'); |
|
|
|
|
96
|
|
|
$batchSize = $this->getBatchSize(); |
97
|
|
|
|
98
|
|
|
// Run for unscanned files |
99
|
|
|
$cnt = 0; |
100
|
|
View Code Duplication |
while (($row = $result->fetch()) && $cnt < $batchSize) { |
|
|
|
|
101
|
|
|
try { |
102
|
|
|
$fileId = $row['fileid']; |
103
|
|
|
$users = $this->getUserWithAccessToStorage((int)$row['storage']); |
104
|
|
|
|
105
|
|
|
foreach ($users as $user) { |
106
|
|
|
/** @var IUser $owner */ |
107
|
|
|
$owner = $this->userManager->get($user['user_id']); |
108
|
|
|
if (!$owner instanceof IUser) { |
109
|
|
|
continue; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
$userFolder = $this->rootFolder->getUserFolder($owner->getUID()); |
113
|
|
|
$files = $userFolder->getById($fileId); |
114
|
|
|
|
115
|
|
|
if ($files === []) { |
116
|
|
|
continue; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
$file = array_pop($files); |
120
|
|
|
if ($file instanceof File) { |
121
|
|
|
if ($userFolder->nodeExists($userFolder->getRelativePath($file->getPath()))) { |
122
|
|
|
$this->scanOneFile($file); |
123
|
|
|
} |
124
|
|
|
} else { |
125
|
|
|
$this->logger->error('Tried to scan non file'); |
|
|
|
|
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
// increased only for successfully scanned files |
129
|
|
|
$cnt++; |
130
|
|
|
break; |
131
|
|
|
} |
132
|
|
|
} catch (\Exception $e) { |
133
|
|
|
$this->logger->logException($e, ['app' => 'files_antivirus']); |
|
|
|
|
134
|
|
|
} |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
if ($cnt === $batchSize) { |
138
|
|
|
// we are done |
139
|
|
|
return; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
// Run for updated files |
143
|
|
|
try { |
144
|
|
|
$result = $this->getToRescanFiles(); |
145
|
|
|
} catch (\Exception $e) { |
146
|
|
|
$this->logger->logException($e); |
|
|
|
|
147
|
|
|
return; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
View Code Duplication |
while (($row = $result->fetch()) && $cnt < $batchSize) { |
|
|
|
|
151
|
|
|
try { |
152
|
|
|
$fileId = $row['fileid']; |
153
|
|
|
$users = $this->getUserWithAccessToStorage((int)$row['storage']); |
154
|
|
|
|
155
|
|
|
foreach ($users as $user) { |
156
|
|
|
/** @var IUser $owner */ |
157
|
|
|
$owner = $this->userManager->get($user['user_id']); |
158
|
|
|
if (!$owner instanceof IUser) { |
159
|
|
|
continue; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
$userFolder = $this->rootFolder->getUserFolder($owner->getUID()); |
163
|
|
|
$files = $userFolder->getById($fileId); |
164
|
|
|
|
165
|
|
|
if ($files === []) { |
166
|
|
|
continue; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
$file = array_pop($files); |
170
|
|
|
|
171
|
|
|
if ($file instanceof File) { |
172
|
|
|
if ($userFolder->nodeExists($userFolder->getRelativePath($file->getPath()))) { |
173
|
|
|
$this->scanOneFile($file); |
174
|
|
|
} |
175
|
|
|
} else { |
176
|
|
|
$this->logger->error('Tried to scan non file'); |
|
|
|
|
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
// increased only for successfully scanned files |
180
|
|
|
$cnt++; |
181
|
|
|
break; |
182
|
|
|
} |
183
|
|
|
} catch (\Exception $e) { |
184
|
|
|
$this->logger->error(__METHOD__ . ', exception: ' . $e->getMessage(), ['app' => 'files_antivirus']); |
|
|
|
|
185
|
|
|
} |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
|
189
|
|
|
// Run for files that have been scanned in the past. Just start to rescan them as the virus definitaions might have been updated |
190
|
|
|
try { |
191
|
|
|
$result = $this->getOutdatedFiles(); |
192
|
|
|
} catch (\Exception $e) { |
193
|
|
|
$this->logger->logException($e); |
|
|
|
|
194
|
|
|
return; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
View Code Duplication |
while (($row = $result->fetch()) && $cnt < $batchSize) { |
|
|
|
|
198
|
|
|
try { |
199
|
|
|
$fileId = $row['fileid']; |
200
|
|
|
$users = $this->getUserWithAccessToStorage((int)$row['storage']); |
201
|
|
|
|
202
|
|
|
foreach ($users as $user) { |
203
|
|
|
/** @var IUser $owner */ |
204
|
|
|
$owner = $this->userManager->get($user['user_id']); |
205
|
|
|
if (!$owner instanceof IUser) { |
206
|
|
|
continue; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
$userFolder = $this->rootFolder->getUserFolder($owner->getUID()); |
210
|
|
|
$files = $userFolder->getById($fileId); |
211
|
|
|
|
212
|
|
|
if ($files === []) { |
213
|
|
|
continue; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
$file = array_pop($files); |
217
|
|
|
if ($file instanceof File) { |
218
|
|
|
if ($userFolder->nodeExists($userFolder->getRelativePath($file->getPath()))) { |
219
|
|
|
$this->scanOneFile($file); |
220
|
|
|
} |
221
|
|
|
} else { |
222
|
|
|
$this->logger->error('Tried to scan non file'); |
|
|
|
|
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
// increased only for successfully scanned files |
226
|
|
|
$cnt++; |
227
|
|
|
break; |
228
|
|
|
} |
229
|
|
|
} catch (\Exception $e) { |
230
|
|
|
$this->logger->error(__METHOD__ . ', exception: ' . $e->getMessage(), ['app' => 'files_antivirus']); |
|
|
|
|
231
|
|
|
} |
232
|
|
|
} |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
protected function getBatchSize(): int { |
236
|
|
|
$batchSize = 10; |
237
|
|
|
if ($this->isCLI) { |
238
|
|
|
$batchSize = 100; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
$this->logger->debug('Batch size is: ' . $batchSize); |
|
|
|
|
242
|
|
|
|
243
|
|
|
return $batchSize; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
protected function getSizeLimitExpression(IQueryBuilder $qb) { |
247
|
|
|
$sizeLimit = (int)$this->appConfig->getAvMaxFileSize(); |
248
|
|
|
if ($sizeLimit === -1) { |
249
|
|
|
$sizeLimitExpr = $qb->expr()->neq('fc.size', $qb->expr()->literal('0')); |
250
|
|
|
} else { |
251
|
|
|
$sizeLimitExpr = $qb->expr()->andX( |
252
|
|
|
$qb->expr()->neq('fc.size', $qb->expr()->literal('0')), |
253
|
|
|
$qb->expr()->lt('fc.size', $qb->createNamedParameter($sizeLimit)) |
254
|
|
|
); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
return $sizeLimitExpr; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
protected function getUserWithAccessToStorage(int $storageId): array { |
261
|
|
|
$qb = $this->db->getQueryBuilder(); |
262
|
|
|
|
263
|
|
|
$qb->select('user_id') |
264
|
|
|
->from('mounts') |
265
|
|
|
->where($qb->expr()->eq('storage_id', $qb->createNamedParameter($storageId))); |
266
|
|
|
|
267
|
|
|
$cursor = $qb->execute(); |
|
|
|
|
268
|
|
|
$data = $cursor->fetchAll(); |
269
|
|
|
$cursor->closeCursor(); |
270
|
|
|
return $data; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
protected function getUnscannedFiles() { |
274
|
|
|
$dirMimeTypeId = $this->mimeTypeLoader->getId('httpd/unix-directory'); |
275
|
|
|
|
276
|
|
|
$qb1 = $this->db->getQueryBuilder(); |
277
|
|
|
$qb1->select('fileid') |
278
|
|
|
->from('files_antivirus'); |
279
|
|
|
|
280
|
|
|
$qb2 = $this->db->getQueryBuilder(); |
281
|
|
|
$qb2->select('fileid', 'storage') |
282
|
|
|
->from('filecache', 'fc') |
283
|
|
|
->where($qb2->expr()->notIn('fileid', $qb2->createFunction($qb1->getSQL()))) |
284
|
|
|
->andWhere($qb2->expr()->neq('mimetype', $qb2->expr()->literal($dirMimeTypeId))) |
285
|
|
|
->andWhere($qb2->expr()->like('path', $qb2->expr()->literal('files/%'))) |
286
|
|
|
->andWhere($this->getSizeLimitExpression($qb2)) |
287
|
|
|
->setMaxResults($this->getBatchSize() * 10); |
288
|
|
|
|
289
|
|
|
return $qb2->execute(); |
|
|
|
|
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
protected function getToRescanFiles() { |
293
|
|
|
$qb = $this->db->getQueryBuilder(); |
294
|
|
|
$qb->select('fc.fileid', 'fc.storage') |
295
|
|
|
->from('filecache', 'fc') |
296
|
|
|
->join('fc', 'files_antivirus', 'fa', $qb->expr()->eq('fc.fileid', 'fa.fileid')) |
297
|
|
|
->andWhere($qb->expr()->lt('fa.check_time', 'fc.mtime')) |
298
|
|
|
->andWhere($this->getSizeLimitExpression($qb)) |
299
|
|
|
->setMaxResults($this->getBatchSize() * 10); |
300
|
|
|
|
301
|
|
|
return $qb->execute(); |
|
|
|
|
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
protected function getOutdatedFiles() { |
305
|
|
|
$dirMimeTypeId = $this->mimeTypeLoader->getId('httpd/unix-directory'); |
306
|
|
|
|
307
|
|
|
// We do not want to keep scanning the same files. So only scan them once per 28 days at most. |
308
|
|
|
$yesterday = time() - (28 * 24 * 60 * 60); |
309
|
|
|
|
310
|
|
|
$qb1 = $this->db->getQueryBuilder(); |
311
|
|
|
$qb2 = $this->db->getQueryBuilder(); |
312
|
|
|
|
313
|
|
|
$qb1->select('fileid') |
314
|
|
|
->from('files_antivirus') |
315
|
|
|
->andWhere($qb2->expr()->lt('check_time', $qb2->createNamedParameter($yesterday))) |
316
|
|
|
->orderBy('check_time', 'ASC'); |
317
|
|
|
|
318
|
|
|
$qb2->select('fileid', 'storage') |
319
|
|
|
->from('filecache', 'fc') |
320
|
|
|
->where($qb2->expr()->in('fileid', $qb2->createFunction($qb1->getSQL()))) |
321
|
|
|
->andWhere($qb2->expr()->neq('mimetype', $qb2->expr()->literal($dirMimeTypeId))) |
322
|
|
|
->andWhere($qb2->expr()->like('path', $qb2->expr()->literal('files/%'))) |
323
|
|
|
->andWhere($this->getSizeLimitExpression($qb2)) |
324
|
|
|
->setMaxResults($this->getBatchSize() * 10); |
325
|
|
|
|
326
|
|
|
$x = $qb2->getSQL(); |
|
|
|
|
327
|
|
|
|
328
|
|
|
return $qb2->execute(); |
|
|
|
|
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
protected function scanOneFile(File $file): void { |
332
|
|
|
$this->logger->debug('Scanning file with fileid: ' . $file->getId()); |
|
|
|
|
333
|
|
|
|
334
|
|
|
$item = $this->itemFactory->newItem($file, true); |
335
|
|
|
$scanner = $this->scannerFactory->getScanner(); |
336
|
|
|
$status = $scanner->scan($item); |
337
|
|
|
$status->dispatch($item); |
338
|
|
|
} |
339
|
|
|
} |
340
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.