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