diff --git a/.devcontainer/dev/devcontainer.json b/.devcontainer/dev/devcontainer.json
deleted file mode 100644
index 1638e2f9..00000000
--- a/.devcontainer/dev/devcontainer.json
+++ /dev/null
@@ -1,41 +0,0 @@
-// For format details, see https://aka.ms/devcontainer.json. For config options, see the
-// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
-{
- "name": "php container",
- // Update the 'dockerComposeFile' list if you have more compose files or use different names.
- // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
- "dockerComposeFile": [
- "../../devcontainer.yaml"
- ],
- // The 'service' property is the name of the service for the container that VS Code should
- // use. Update this value and .devcontainer/docker-compose.yml to the real service name.
- "service": "dev",
- // The optional 'workspaceFolder' property is the path VS Code should open by default when
- // connected. This is typically a file mount in .devcontainer/docker-compose.yml
- "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
- "features": {
- "ghcr.io/devcontainers/features/php:1": {},
- "ghcr.io/devcontainers-contrib/features/node-asdf:0": {}
- },
- "remoteUser": "www-data",
- "remoteEnv": {
- "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
- },
- "containerEnv": {
- "SHELL": "/bin/bash"
- }
- // Features to add to the dev container. More info: https://containers.dev/features.
- // "features": {},
- // Use 'forwardPorts' to make a list of ports inside the container available locally.
- // "forwardPorts": [],
- // Uncomment the next line if you want start specific services in your Docker Compose config.
- // "runServices": [],
- // Uncomment the next line if you want to keep your containers running after VS Code shuts down.
- // "shutdownAction": "none",
- // Uncomment the next line to run commands after the container is created.
- // "postCreateCommand": "cat /etc/os-release",
- // Configure tool-specific properties.
- // "customizations": {},
- // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
- // "remoteUser": "devcontainer"
-}
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..11be65ca
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,22 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+// README at: https://github.com/devcontainers/templates/tree/main/src/php
+{
+ "name": "PHP",
+ // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
+ "image": "mcr.microsoft.com/devcontainers/php:1-8.2-bullseye",
+
+ // Features to add to the dev container. More info: https://containers.dev/features.
+ // "features": {},
+
+ // Configure tool-specific properties.
+ // "customizations": {},
+
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
+ "forwardPorts": [8080]
+
+ // Use 'postCreateCommand' to run commands after the container is created.
+ // "postCreateCommand": "sudo chmod a+x \"$(pwd)\" && sudo rm -rf /var/www/html && sudo ln -s \"$(pwd)\" /var/www/html"
+
+ // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
+ // "remoteUser": "root"
+}
diff --git a/.devcontainer/nextcloud/devcontainer.json b/.devcontainer/nextcloud/devcontainer.json
deleted file mode 100644
index 0ae124b8..00000000
--- a/.devcontainer/nextcloud/devcontainer.json
+++ /dev/null
@@ -1,40 +0,0 @@
-// For format details, see https://aka.ms/devcontainer.json. For config options, see the
-// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
-{
- "name": "nextcloud",
- // Update the 'dockerComposeFile' list if you have more compose files or use different names.
- // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
- "dockerComposeFile": [
- "../../devcontainer.yaml"
- ],
- // The 'service' property is the name of the service for the container that VS Code should
- // use. Update this value and .devcontainer/docker-compose.yml to the real service name.
- "service": "nextcloud",
- // The optional 'workspaceFolder' property is the path VS Code should open by default when
- // connected. This is typically a file mount in .devcontainer/docker-compose.yml
- "features": {
- "ghcr.io/devcontainers/features/php:1": {}
- },
- "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
- "remoteEnv": {
- "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
- },
- "remoteUser": "root",
- "containerEnv": {
- "SHELL": "/bin/bash"
- }
- // Features to add to the dev container. More info: https://containers.dev/features.
- // "features": {},
- // Use 'forwardPorts' to make a list of ports inside the container available locally.
- // "forwardPorts": [],
- // Uncomment the next line if you want start specific services in your Docker Compose config.
- // "runServices": [],
- // Uncomment the next line if you want to keep your containers running after VS Code shuts down.
- // "shutdownAction": "none",
- // Uncomment the next line to run commands after the container is created.
- // "postCreateCommand": "cat /etc/os-release",
- // Configure tool-specific properties.
- // "customizations": {},
- // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
- // "remoteUser": "devcontainer"
-}
\ No newline at end of file
diff --git a/Makefile b/Makefile
index b79cea5e..32533a1f 100644
--- a/Makefile
+++ b/Makefile
@@ -110,7 +110,7 @@ distclean: clean
appstore:
rm -rf $(appstore_build_directory)
mkdir -p $(appstore_build_directory)
- tar cvzf $(appstore_package_name).tar.gz \
+ tar czf $(appstore_package_name).tar.gz \
--transform s/$(app_directory_name)/$(app_real_name)/ \
--exclude-vcs \
--exclude="../$(app_directory_name)/build" \
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 26168b84..078ea436 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -27,4 +27,12 @@
pgsql
mysql
sqlite
+
+
+ OCA\GDataVaas\Activity\Setting
+
+
+ OCA\GDataVaas\Activity\Provider
+
+
diff --git a/composer.json b/composer.json
index 3d024c09..de0aab5e 100644
--- a/composer.json
+++ b/composer.json
@@ -9,7 +9,7 @@
}
],
"require": {
- "gdata/vaas": "^8.0.0"
+ "gdata/vaas": "^8.0.2"
},
"require-dev": {
"nextcloud/ocp": "dev-stable28",
@@ -17,7 +17,7 @@
"nextcloud/coding-standard": "^v1.1.1"
},
"scripts": {
- "lint": "find . -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l",
+ "lint": "find lib -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l",
"cs:check": "php-cs-fixer fix --dry-run --diff .",
"cs:fix": "php-cs-fixer fix .",
"psalm": "vendor/psalm/phar/psalm.phar --threads=1",
diff --git a/img/favicon.svg b/img/favicon.svg
new file mode 100644
index 00000000..3e369ff8
--- /dev/null
+++ b/img/favicon.svg
@@ -0,0 +1,16 @@
+
+
+
diff --git a/install.sh b/install.sh
index 008766f4..9eb637d7 100755
--- a/install.sh
+++ b/install.sh
@@ -1,17 +1,32 @@
#!/bin/bash
docker stop nextcloud-container
-docker run -d --name nextcloud-container --rm --publish 80:80 --publish 8080:8080 --publish 8443:8443 nextcloud:stable
+sleep 1
+docker run -d --name nextcloud-container --rm --publish 80:80 nextcloud:28
echo "Waiting for sunrise..."
-sleep 15
-docker exec --user www-data -it nextcloud-container php occ maintenance:install --admin-user=admin --admin-pass=admin
+
+until docker exec --user www-data -it nextcloud-container php occ maintenance:install --admin-user=admin --admin-pass=admin >/dev/null
+do
+ echo "Try again waiting 2 seconds"
+ sleep 2
+done
+
+make build
make appstore
tar -xf ./build/artifacts/gdatavaas.tar.gz -C ./build/artifacts
-docker exec --user www-data -it nextcloud-container mkdir apps/gdatavaas
-docker cp ./build/artifacts/gdatavaas nextcloud-container:/var/www/html/apps/gdatavaas
+docker cp ./build/artifacts/gdatavaas nextcloud-container:/var/www/html/apps/
+docker exec -it nextcloud-container chown -R www-data:www-data /var/www/html/apps/gdatavaas
docker exec --user www-data -it nextcloud-container php occ app:update --all
docker exec --user www-data -it nextcloud-container php occ app:enable gdatavaas
-docker exec --user www-data -it nextcloud-container php occ config:app:set gdatavaas username --value=vaas-integration-test
+docker exec --user www-data -it nextcloud-container php occ config:app:set gdatavaas clientId --value=$CLIENT_ID
+docker exec --user www-data -it nextcloud-container php occ config:app:set gdatavaas clientSecret --value=$CLIENT_SECRET
+docker exec --user www-data -it nextcloud-container php occ config:app:set gdatavaas authMethod --value=ClientCredentials
+docker exec --user www-data -it nextcloud-container php occ config:app:set gdatavaas autoScanFiles --value=true
+docker exec --user www-data -it nextcloud-container php occ config:app:set gdatavaas scanQueueLength --value=100
+
+docker exec --user www-data -it nextcloud-container php occ log:manage --level DEBUG
+docker exec --user www-data -it nextcloud-container php occ app:disable firstrunwizard
+
-docker exec --user www-data -it nextcloud-container php cron.php
+# docker exec --user www-data -it nextcloud-container php cron.php
diff --git a/lib/Activity/Provider.php b/lib/Activity/Provider.php
new file mode 100644
index 00000000..05f8b7ea
--- /dev/null
+++ b/lib/Activity/Provider.php
@@ -0,0 +1,139 @@
+
+ *
+ * @author Roeland Jago Douma
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+namespace OCA\GDataVaas\Activity;
+
+use OCA\GDataVaas\AppInfo\Application;
+use OCP\Activity\IEvent;
+use OCP\Activity\IProvider;
+use OCP\IURLGenerator;
+use OCP\L10N\IFactory;
+use Psr\Log\LoggerInterface;
+
+class Provider implements IProvider {
+ public const TYPE_VIRUS_DETECTED = 'virus_detected';
+
+ public const SUBJECT_VIRUS_DETECTED = 'virus_detected';
+ public const SUBJECT_VIRUS_DETECTED_UPLOAD = 'virus_detected_upload';
+ public const SUBJECT_VIRUS_DETECTED_SCAN = 'virus_detected_scan';
+
+ public const MESSAGE_FILE_DELETED = 'file_deleted';
+
+ /** @var IFactory */
+ private $languageFactory;
+
+ /** @var IURLGenerator */
+ private $urlGenerator;
+ private LoggerInterface $logger;
+
+ public function __construct(IFactory $languageFactory, IURLGenerator $urlGenerator, LoggerInterface $logger) {
+ $this->languageFactory = $languageFactory;
+ $this->urlGenerator = $urlGenerator;
+ $this->logger = $logger;
+ }
+
+ public function parse($language, IEvent $event, IEvent $previousEvent = null) {
+ if ($event->getApp() !== Application::APP_ID || $event->getType() !== self::TYPE_VIRUS_DETECTED) {
+ throw new \InvalidArgumentException();
+ }
+
+ $parameters = [];
+ $subject = '';
+
+ if ($event->getSubject() === self::SUBJECT_VIRUS_DETECTED) {
+ $subject = 'File {file} is infected with {virus}';
+
+ $params = $event->getSubjectParameters();
+ $parameters['virus'] = [
+ 'type' => 'highlight',
+ 'id' => $params[1],
+ 'name' => $params[1],
+ ];
+
+ $parameters['file'] = [
+ 'type' => 'highlight',
+ 'id' => $event->getObjectName(),
+ 'name' => basename($event->getObjectName()),
+ ];
+ $event->setIcon($this->urlGenerator->imagePath('gdatavaas', 'favicon.svg'));
+
+ if ($event->getMessage() === self::MESSAGE_FILE_DELETED) {
+ $event->setParsedMessage('The file has been removed');
+ }
+ } elseif ($event->getSubject() === self::SUBJECT_VIRUS_DETECTED_UPLOAD) {
+ $subject = 'File containing {virus} detected';
+
+ $params = $event->getSubjectParameters();
+ $parameters['virus'] = [
+ 'type' => 'highlight',
+ 'id' => $params[0],
+ 'name' => $params[0],
+ ];
+
+ $event->setParsedSubject($subject);
+ $event->setRichSubject($subject);
+ $event->setIcon($this->urlGenerator->imagePath('gdatavaas', 'favicon.svg'));
+
+ if ($event->getMessage() === self::MESSAGE_FILE_DELETED) {
+ $event->setParsedMessage('The file has been removed');
+ }
+ } elseif ($event->getSubject() === self::SUBJECT_VIRUS_DETECTED_SCAN) {
+ $subject = 'File {file} is infected with {virus}';
+
+ $params = $event->getSubjectParameters();
+ $parameters['virus'] = [
+ 'type' => 'highlight',
+ 'id' => $params[0],
+ 'name' => $params[0],
+ ];
+ $parameters['file'] = [
+ 'type' => 'highlight',
+ 'id' => $event->getObjectName(),
+ 'name' => $event->getObjectName(),
+ ];
+ $event->setIcon($this->urlGenerator->imagePath('gdatavaas', 'favicon.svg'));
+
+ if ($event->getMessage() === self::MESSAGE_FILE_DELETED) {
+ $event->setParsedMessage('The file has been removed');
+ }
+ }
+
+ $this->setSubjects($event, $subject, $parameters);
+
+ return $event;
+ }
+
+ private function setSubjects(IEvent $event, string $subject, array $parameters): void {
+ $placeholders = $replacements = [];
+ foreach ($parameters as $placeholder => $parameter) {
+ $placeholders[] = '{' . $placeholder . '}';
+ if ($parameter['type'] === 'file') {
+ $replacements[] = $parameter['path'];
+ } else {
+ $replacements[] = $parameter['name'];
+ }
+ }
+
+ $event->setParsedSubject(str_replace($placeholders, $replacements, $subject))
+ ->setRichSubject($subject, $parameters);
+ }
+}
diff --git a/lib/Activity/Setting.php b/lib/Activity/Setting.php
new file mode 100644
index 00000000..c07b1db8
--- /dev/null
+++ b/lib/Activity/Setting.php
@@ -0,0 +1,59 @@
+
+ *
+ * @author Roeland Jago Douma
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+namespace OCA\GDataVaas\Activity;
+
+use OCP\Activity\ISetting;
+
+class Setting implements ISetting {
+
+ public function __construct() {
+ }
+
+ public function getIdentifier() {
+ return Provider::TYPE_VIRUS_DETECTED;
+ }
+
+ public function getName() {
+ return 'Antivirus detected a virus';
+ }
+
+ public function getPriority() {
+ return 70;
+ }
+
+ public function canChangeStream() {
+ return false;
+ }
+
+ public function isDefaultEnabledStream() {
+ return true;
+ }
+
+ public function canChangeMail() {
+ return false;
+ }
+
+ public function isDefaultEnabledMail() {
+ return false;
+ }
+}
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 2459d818..5c1db204 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -4,14 +4,26 @@
namespace OCA\GDataVaas\AppInfo;
+use OC\Files\Filesystem;
+use OCA\GDataVaas\AvirWrapper;
+use OCA\GDataVaas\Service\VerdictService;
+use OCP\Activity\IManager;
+use OCP\App\IAppManager;
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent;
use OCP\AppFramework\App;
+use OCP\AppFramework\Bootstrap\IBootContext;
+use OCP\AppFramework\Bootstrap\IBootstrap;
+use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\IHomeStorage;
+use OCP\Files\Storage;
+use OCP\Files\Storage\IStorage;
use OCP\Util;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
+use Psr\Log\LoggerInterface;
-class Application extends App
+class Application extends App implements IBootstrap
{
public const APP_ID = 'gdatavaas';
@@ -28,19 +40,61 @@ public function __construct()
$eventDispatcher->addListener(LoadAdditionalScriptsEvent::class, function () {
Util::addScript(self::APP_ID, 'gdatavaas-files-action');
});
-
- $this->register();
}
/**
* Load the composer autoloader if it exists
* @return void
*/
- public function register(): void
+ public function register(IRegistrationContext $context): void
{
$composerAutoloadFile = __DIR__ . '/../../vendor/autoload.php';
if (file_exists($composerAutoloadFile)) {
require_once $composerAutoloadFile;
}
+
+ // Util::connection is deprecated, but required ATM by FileSystem::addStorageWrapper
+ Util::connectHook('OC_Filesystem', 'preSetup', $this, 'setupWrapper');
+ }
+
+ /**
+ * * Add wrapper for local storages
+ */
+ public function setupWrapper(): void {
+ Filesystem::addStorageWrapper(
+ 'oc_gdata_vaas',
+ function (string $mountPoint, IStorage $storage) {
+ /*
+ if ($storage->instanceOfStorage(Jail::class)) {
+ // No reason to wrap jails again
+ return $storage;
+ }
+ */
+
+ $container = $this->getContainer();
+ $verdictService = $container->get(VerdictService::class);
+ // $l10n = $container->get(IL10N::class);
+ $logger = $container->get(LoggerInterface::class);
+ $activityManager = $container->get(IManager::class);
+ $eventDispatcher = $container->get(IEventDispatcher::class);
+ $appManager = $container->get(IAppManager::class);
+ return new AvirWrapper([
+ 'storage' => $storage,
+ 'verdictService' => $verdictService,
+ //'l10n' => $l10n,
+ 'logger' => $logger,
+ 'activityManager' => $activityManager,
+ 'isHomeStorage' => $storage->instanceOfStorage(IHomeStorage::class),
+ 'eventDispatcher' => $eventDispatcher,
+ 'trashEnabled' => $appManager->isEnabledForUser('files_trashbin'),
+ ]);
+ },
+ 1
+ );
+ }
+
+ public function boot(IBootContext $context): void
+ {
+ // TODO: Implement boot() method.
}
}
diff --git a/lib/AvirWrapper.php b/lib/AvirWrapper.php
new file mode 100644
index 00000000..6c0c3031
--- /dev/null
+++ b/lib/AvirWrapper.php
@@ -0,0 +1,183 @@
+
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace OCA\GDataVaas;
+
+use OC\Files\Storage\Wrapper\Wrapper;
+use OCA\Files_Trashbin\Trash\ITrashManager;
+use OCA\GDataVaas\Activity\Provider;
+use OCA\GDataVaas\AppInfo\Application;
+use OCA\GDataVaas\Service\VerdictService;
+use OCP\Activity\IManager as ActivityManager;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\InvalidContentException;
+use OCP\IL10N;
+use Psr\Log\LoggerInterface;
+use VaasSdk\Message\Verdict;
+
+class AvirWrapper extends Wrapper {
+ /**
+ * Modes that are used for writing
+ * @var array
+ */
+ private $writingModes = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'];
+
+ protected VerdictService $verdictService;
+
+ /** @var IL10N */
+ protected $l10n;
+
+ /** @var LoggerInterface */
+ protected $logger;
+
+ /** @var ActivityManager */
+ protected $activityManager;
+
+ /** @var bool */
+ protected $isHomeStorage;
+
+ /** @var bool */
+ private $shouldScan = true;
+
+ /** @var bool */
+ private $trashEnabled;
+
+ /**
+ * @param array $parameters
+ */
+ public function __construct($parameters) {
+ parent::__construct($parameters);
+ $this->verdictService = $parameters['verdictService'];
+ $this->logger = $parameters['logger'];
+ $this->activityManager = $parameters['activityManager'];
+ $this->isHomeStorage = $parameters['isHomeStorage'];
+ $this->trashEnabled = $parameters['trashEnabled'];
+
+ /** @var IEventDispatcher $eventDispatcher */
+ $eventDispatcher = $parameters['eventDispatcher'];
+ }
+
+ /**
+ * Asynchronously scan data that are written to the file
+ * @param string $path
+ * @param string $mode
+ * @return resource | false
+ */
+ public function fopen($path, $mode) {
+ $cache = $this->getCache($path);
+ $metadata = $cache->get($path);
+ $this->logger->debug(var_export($metadata, true));
+ $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 {
+ $logger = $this->logger;
+ return CallbackReadDataWrapper::wrap(
+ $stream,
+ null,
+ null,
+ function () use ($path, $logger) {
+ $localPath = $this->getLocalFile($path);
+ $filesize = $this->filesize($path);
+ $logger->debug("Closing " . $localPath . " with size " . $filesize);
+
+ $verdict = $this->verdictService->scan($localPath);
+ $logger->debug("Verdict for " . $localPath . " is " . $verdict->Verdict->value);
+
+ if ($verdict->Verdict == Verdict::MALICIOUS) {
+ $logger->debug("Removing malicious file " . $localPath);
+
+ //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. ' . $verdict->Detection
+ . ' Account: ' . $owner . ' Path: ' . $path,
+ ['app' => 'gdatavaas']
+ );
+
+ $activity = $this->activityManager->generateEvent();
+ $activity->setApp(Application::APP_ID)
+ ->setSubject(Provider::SUBJECT_VIRUS_DETECTED_UPLOAD, [$verdict->Detection ?? "no_detection_name"])
+ ->setMessage(Provider::MESSAGE_FILE_DELETED)
+ ->setObject('', 0, $path)
+ ->setAffectedUser($owner)
+ ->setType(Provider::TYPE_VIRUS_DETECTED);
+ $this->activityManager->publish($activity);
+
+ throw new InvalidContentException(
+ sprintf(
+ 'Virus %s is detected in the file. Upload cannot be completed.',
+ $verdict->Detection
+ )
+ );
+ }
+ }
+ );
+ } 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);
+ }
+}
diff --git a/lib/CallbackReadDataWrapper.php b/lib/CallbackReadDataWrapper.php
new file mode 100644
index 00000000..b854fe25
--- /dev/null
+++ b/lib/CallbackReadDataWrapper.php
@@ -0,0 +1,60 @@
+ [
+ 'source' => $source,
+ 'readData' => $read,
+ 'write' => $write,
+ 'close' => $close,
+ 'readDir' => $readDir,
+ 'preClose' => $preClose
+ ]
+ ]);
+ return Wrapper::wrapSource($source, $context, 'callbackReadData', self::class);
+ }
+
+ /**
+ * @return true
+ */
+ protected function open() {
+ $context = $this->loadContext('callbackReadData');
+
+ $this->readDataCallback = $context['readData'];
+ $this->writeCallback = $context['write'];
+ $this->closeCallback = $context['close'];
+ $this->readDirCallBack = $context['readDir'];
+ return true;
+ }
+
+ public function stream_read($count) {
+ $result = parent::stream_read($count);
+ if (is_callable($this->readDataCallback)) {
+ call_user_func($this->readDataCallback, strlen($result), $result);
+ }
+ return $result;
+ }
+}
diff --git a/lib/Service/TagService.php b/lib/Service/TagService.php
index 5b9b1b82..041de672 100644
--- a/lib/Service/TagService.php
+++ b/lib/Service/TagService.php
@@ -42,12 +42,12 @@ public function __construct(LoggerInterface $logger, ISystemTagManager $systemTa
public function getTag(string $name, bool $create = true): ISystemTag
{
try {
- $tag = $this->tagService->getTag($name, true, true);
+ $tag = $this->tagService->getTag($name, true, false);
} catch (TagNotFoundException) {
if (!$create) {
throw new TagNotFoundException();
}
- $tag = $this->tagService->createTag($name, true, true);
+ $tag = $this->tagService->createTag($name, true, false);
$this->logger->debug("Tag created: " . $name);
}
return $tag;
@@ -73,7 +73,7 @@ public function setTag(int $fileId, string $tagName): void
public function removeTagFromFile(string $tagName, int $fileId): bool
{
try {
- $tag = $this->tagService->getTag($tagName, true, true);
+ $tag = $this->tagService->getTag($tagName, true, false);
$this->tagMapper->unassignTags(strval($fileId), 'files', [$tag->getId()]);
$this->logger->debug("Tag removed: " . $tagName . " for file " . $fileId);
return true;
@@ -89,9 +89,11 @@ public function removeTagFromFile(string $tagName, int $fileId): bool
*/
public function hasCleanMaliciousOrPupTag(int $fileId): bool
{
- if ($this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::CLEAN)->getId()) ||
+ if (
+ $this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::CLEAN)->getId()) ||
$this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::MALICIOUS)->getId()) ||
- $this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::PUP)->getId())) {
+ $this->tagMapper->haveTag([$fileId], 'files', $this->getTag(self::PUP)->getId())
+ ) {
return true;
}
return false;
diff --git a/lib/Service/VerdictService.php b/lib/Service/VerdictService.php
index 6263e503..3c467d96 100644
--- a/lib/Service/VerdictService.php
+++ b/lib/Service/VerdictService.php
@@ -18,6 +18,7 @@
use VaasSdk\Message\VaasVerdict;
use VaasSdk\ResourceOwnerPasswordGrantAuthenticator;
use VaasSdk\Vaas;
+use VaasSdk\VaasOptions;
class VerdictService
{
@@ -45,7 +46,7 @@ public function __construct(LoggerInterface $logger, IConfig $appConfig, FileSer
$this->fileService = $fileService;
$this->tagService = $tagService;
- $this->authMethod = $this->appConfig->getAppValue(self::APP_ID, 'authMethod', 'ResourceOwnerPassword');
+ $this->authMethod = $this->appConfig->getAppValue(self::APP_ID, 'authMethod', 'ClientCredentials');
$this->tokenEndpoint = $this->appConfig->getAppValue(self::APP_ID, 'tokenEndpoint', 'https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token');
$this->vaasUrl = $this->appConfig->getAppValue(self::APP_ID, 'vaasUrl', 'wss://gateway.staging.vaas.gdatasecurity.de');
$this->clientId = $this->appConfig->getAppValue(self::APP_ID, 'clientId');
@@ -94,17 +95,7 @@ public function scanFileById(int $fileId): VaasVerdict
}
}
- if ($this->vaas == null) {
- $this->vaas = $this->createAndConnectVaas();
- }
-
- try {
- $verdict = $this->vaas->ForFile($filePath);
- } catch (Exception $e) {
- $this->logger->error("Vaas for file: " . $e->getMessage());
- $this->vaas = null;
- throw $e;
- }
+ $verdict = $this->scan($filePath);
$this->logger->info("VaaS scan result for " . $node->getName() . " (" . $fileId . "): Verdict: "
. $verdict->Verdict->value . ", Detection: " . $verdict->Detection . ", SHA256: " . $verdict->Sha256 .
@@ -138,6 +129,23 @@ public function scanFileById(int $fileId): VaasVerdict
return $verdict;
}
+ public function scan(string $filePath): VaasVerdict
+ {
+ if ($this->vaas == null) {
+ $this->vaas = $this->createAndConnectVaas();
+ }
+
+ try {
+ $verdict = $this->vaas->ForFile($filePath);
+
+ return $verdict;
+ } catch (Exception $e) {
+ $this->logger->error("Vaas for file: " . $e->getMessage());
+ $this->vaas = null;
+ throw $e;
+ }
+ }
+
/**
* Parses the allowlist from the app settings and returns it as an array.
* @return array
@@ -186,7 +194,8 @@ private function createAndConnectVaas(): Vaas
);
}
- $vaas = new Vaas($this->vaasUrl);
+ $options = new VaasOptions(false, false);
+ $vaas = new Vaas($this->vaasUrl, $this->logger, $options);
$vaas->Connect($this->authenticator->getToken());
return $vaas;
}