scannerFactory = $parameters['scannerFactory']; $this->l10n = $parameters['l10n']; $this->logger = $parameters['logger']; $this->activityManager = $parameters['activityManager']; $this->isHomeStorage = $parameters['isHomeStorage']; $this->trashEnabled = $parameters['trashEnabled']; $this->mountPoint = $parameters['mount_point']; $this->blockUnscannable = $parameters['block_unscannable']; /** @var IEventDispatcher $eventDispatcher */ $eventDispatcher = $parameters['eventDispatcher']; $eventDispatcher->addListener(ScanStateEvent::class, function (ScanStateEvent $event) { $this->shouldScan = $event->getState(); }); } /** * Asynchronously scan data that are written to the file * @param string $path * @param string $mode * @return resource | false */ public function fopen($path, $mode) { $stream = $this->storage->fopen($path, $mode); /* * Only check when * - it is a resource * - it is a writing mode * - if it is a homestorage it starts with files/ * - if it is not a homestorage we always wrap (external storages) */ if ($this->shouldWrap($path) && is_resource($stream) && $this->isWritingMode($mode)) { $stream = $this->wrapSteam($path, $stream); } return $stream; } public function writeStream(string $path, $stream, ?int $size = null): int { if ($this->shouldWrap($path)) { $stream = $this->wrapSteam($path, $stream); } return parent::writeStream($path, $stream, $size); } private function shouldWrap(string $path): bool { return $this->shouldScan && (!$this->isHomeStorage || (strpos($path, 'files/') === 0 || strpos($path, '/files/') === 0) ); } private function wrapSteam(string $path, $stream) { try { $scanner = $this->scannerFactory->getScanner($this->mountPoint . $path); $scanner->initScanner(); return CallbackReadDataWrapper::wrap( $stream, function ($count, $data) use ($scanner) { $scanner->onAsyncData($data); }, function ($data) use ($scanner) { $scanner->onAsyncData($data); }, function () use ($scanner, $path) { $status = $scanner->completeAsyncScan(); if ($status->getNumericStatus() === Status::SCANRESULT_INFECTED) { $this->handleInfected($path, $status); } if ($this->blockUnscannable && $status->getNumericStatus() === Status::SCANRESULT_UNSCANNABLE) { $this->handleInfected($path, $status); } } ); } catch (\Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); } return $stream; } /** * Checks whether passed mode is suitable for writing * @param string $mode * @return bool */ private function isWritingMode($mode) { // Strip unessential binary/text flags $cleanMode = str_replace( ['t', 'b'], ['', ''], $mode ); return in_array($cleanMode, $this->writingModes); } /** * Synchronously scan data that is written to a file by the bulk upload endpoint * * @return false|float|int * @throws InvalidContentException */ public function file_put_contents($path, $data) { if ($this->shouldWrap($path)) { $scanner = $this->scannerFactory->getScanner($this->mountPoint . $path); $scanner->initScanner(); $status = $scanner->scanString($data); if ($status->getNumericStatus() === Status::SCANRESULT_INFECTED) { $this->handleInfected($path, $status); } } return parent::file_put_contents($path, $data); } /** * @throws InvalidContentException */ private function handleInfected(string $path, Status $status): void { //prevent from going to trashbin if ($this->trashEnabled) { /** @var ITrashManager $trashManager */ $trashManager = \OC::$server->query(ITrashManager::class); $trashManager->pauseTrash(); } $owner = $this->getOwner($path); $this->unlink($path); if ($this->trashEnabled) { /** @var ITrashManager $trashManager */ $trashManager = \OC::$server->query(ITrashManager::class); $trashManager->resumeTrash(); } $this->logger->warning( 'Infected file deleted. ' . $status->getDetails() . ' Account: ' . $owner . ' Path: ' . $path, ['app' => 'files_antivirus'] ); $activity = $this->activityManager->generateEvent(); $activity->setApp(Application::APP_NAME) ->setSubject(Provider::SUBJECT_VIRUS_DETECTED_UPLOAD, [$status->getDetails()]) ->setMessage(Provider::MESSAGE_FILE_DELETED) ->setObject('', 0, $path) ->setAffectedUser($owner) ->setType(Provider::TYPE_VIRUS_DETECTED); $this->activityManager->publish($activity); $this->logger->error('Infected file deleted. ' . $status->getDetails() . ' File: ' . $path . ' Account: ' . $owner, ['app' => 'files_antivirus']); throw new InvalidContentException( $this->l10n->t( 'Virus %s is detected in the file. Upload cannot be completed.', $status->getDetails() ) ); } }