Skip to content

Commit

Permalink
Add background job to verify documents.
Browse files Browse the repository at this point in the history
  • Loading branch information
fancycode committed May 9, 2023
1 parent 68ebc54 commit 672d07f
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 0 deletions.
1 change: 1 addition & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
</types>

<background-jobs>
<job>OCA\Esig\BackgroundJob\BackgroundVerify</job>
<job>OCA\Esig\BackgroundJob\DeleteCompleted</job>
<job>OCA\Esig\BackgroundJob\FetchSigned</job>
<job>OCA\Esig\BackgroundJob\ResendMails</job>
Expand Down
205 changes: 205 additions & 0 deletions lib/BackgroundJob/BackgroundVerify.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2023, struktur AG.
*
* @author Joachim Bauch <bauch@struktur.de>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Esig\BackgroundJob;

use GuzzleHttp\Exception\ConnectException;
use OCA\Esig\Client;
use OCA\Esig\Config;
use OCA\Esig\Verify;
use OCP\AppFramework\Http;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\TimedJob;
use OCP\DB\IResult;
use OCP\Files\File;
use OCP\Files\IMimeTypeLoader;
use OCP\Files\IRootFolder;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;

class BackgroundVerify extends TimedJob {
private LoggerInterface $logger;
private IUserManager $userManager;
private IDBConnection $db;
private IRootFolder $rootFolder;
private IMimeTypeLoader $mimeTypeLoader;
private Config $config;
private Client $client;
private Verify $verify;

public function __construct(ITimeFactory $timeFactory,
LoggerInterface $logger,
IUserManager $userManager,
IDBConnection $db,
IRootFolder $rootFolder,
IMimeTypeLoader $mimeTypeLoader,
Config $config,
Client $client,
Verify $verify) {
parent::__construct($timeFactory);

// Every 5 minutes
$this->setInterval(60 * 5);
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);

$this->logger = $logger;
$this->userManager = $userManager;
$this->db = $db;
$this->rootFolder = $rootFolder;
$this->mimeTypeLoader = $mimeTypeLoader;
$this->config = $config;
$this->client = $client;
$this->verify = $verify;
}

protected function run($argument): void {
if (!$this->config->isBackgroundVerifyEnabled()) {
$this->logger->info('Background verification disabled');
return;
}

$account = $this->config->getAccount();
if (!$account['id'] || !$account['secret']) {
$this->logger->info('No account configured');
return;
}

$this->logger->debug('Starting background verification');

try {
$result = $this->getPendingFiles();
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return;
}

$batchSize = $this->getBatchSize();
$server = $this->config->getServer();
$cnt = 0;
while (($row = $result->fetch()) && $cnt < $batchSize) {
try {
$fileId = $row['fileid'];
$users = $this->getUserWithAccessToStorage((int)$row['storage']);

foreach ($users as $user) {
/** @var IUser $owner */
$owner = $this->userManager->get($user['user_id']);
if (!$owner instanceof IUser) {
continue;
}

$userFolder = $this->rootFolder->getUserFolder($owner->getUID());
$files = $userFolder->getById($fileId);
if (empty($files)) {
continue;
}

$file = array_pop($files);
if (!$file instanceof File) {
$this->logger->error('Tried to verify non file at ' . $file->getPath());
break;
}

if (!$userFolder->nodeExists($userFolder->getRelativePath($file->getPath()))) {
$this->logger->error('Tried to verify non-existing file at ' . $file->getPath());
break;
}

$this->verifyFile($file, $account, $server);
$cnt++;
break;
}
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
}
}
}

private function getBatchSize(): int {
// TODO: Make this configurable?
return 10;
}

protected function getUserWithAccessToStorage(int $storageId): array {
$qb = $this->db->getQueryBuilder();
$qb->select('user_id')
->from('mounts')
->where($qb->expr()->eq('storage_id', $qb->createNamedParameter($storageId)));

$cursor = $qb->executeQuery();
$data = $cursor->fetchAll();
$cursor->closeCursor();
return $data;
}

private function getPendingFiles(): IResult {
$pdfMimeTypeId = $this->mimeTypeLoader->getId('application/pdf');

$query = $this->db->getQueryBuilder();
$query->select('fc.fileid', 'storage')
->from('filecache', 'fc')
->leftJoin('fc', 'esig_file_signatures', 'fs', $query->expr()->eq('fc.fileid', 'fs.file_id'))
->where($query->expr()->isNull('fs.file_id'))
->andWhere($query->expr()->eq('mimetype', $query->expr()->literal($pdfMimeTypeId)))
->andWhere($query->expr()->like('path', $query->expr()->literal('files/%')))
->setMaxResults($this->getBatchSize() * 10);

return $query->executeQuery();
}

private function verifyFile(File $file, array $account, string $server) {
$this->logger->debug('Verifying file ' . $file->getPath());

try {
$signatures = $this->client->verifySignatures($file, $account, $server);
} catch (ConnectException $e) {
$this->logger->error('Error connecting to ' . $server . ' for ' . $file->getPath(), [
'exception' => $e,
]);
return;
} catch (\Exception $e) {
switch ($e->getCode()) {
case Http::STATUS_NOT_FOUND:
/** @var BadResponseException $e */
$response = $e->getResponse();
$body = (string) $response->getBody();
$signatures = json_decode($body, true);
if ($signatures) {
$this->verify->storeFileSignatures($file, $signatures);
}
return;
}

$this->logger->error('Error sending request to ' . $server . ' for ' . $file->getPath(), [
'exception' => $e,
]);
return;
}

$this->verify->storeFileSignatures($file, $signatures);
}
}
4 changes: 4 additions & 0 deletions lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ public function insecureSkipVerify(): bool {
return $this->config->getAppValue('esig', 'insecure_skip_verify', 'false') === 'true';
}

public function isBackgroundVerifyEnabled(): bool {
return $this->config->getAppValue('esig', 'background_verify', 'true') === 'true';
}

public function getSignatureImage(IUser $user): ?ISimpleFile {
try {
$folder = $this->appData->getFolder($user->getUID());
Expand Down
1 change: 1 addition & 0 deletions lib/Settings/Admin/AdminSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public function getForm(): TemplateResponse {
'signed_save_mode' => $this->config->getSignedSaveMode(),
'intranet_instance' => $this->config->isIntranetInstance(),
'insecure_skip_verify' => $this->config->insecureSkipVerify(),
'background_verify' => $this->config->isBackgroundVerifyEnabled(),
'delete_max_age' => $this->config->getDeleteMaxAge(),
]);

Expand Down
27 changes: 27 additions & 0 deletions src/components/AdminSettings/InstanceSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@
{{ t('esig', 'This is potentially insecure and should only be enabled during development (if necessary).') }}
</NcCheckboxRadioSwitch>
</div>
<div>
<NcCheckboxRadioSwitch :checked.sync="settings.background_verify"
type="switch"
@update:checked="debounceUpdateBackgroundVerify">
{{ t('esig', 'Verify document signatures in the background.') }}
</NcCheckboxRadioSwitch>
</div>
</NcSettingsSection>
</template>

Expand Down Expand Up @@ -106,6 +113,26 @@ export default {
},
})
},
debounceUpdateBackgroundVerify: debounce(function() {
this.updateBackgroundVerify()
}, 500),
updateBackgroundVerify() {
this.loading = true
const self = this
OCP.AppConfig.setValue('esig', 'background_verify', this.settings.background_verify, {
success() {
showSuccess(t('esig', 'Settings saved'))
self.loading = false
},
error() {
showError(t('esig', 'Could not save settings'))
self.loading = false
},
})
},
},
}
</script>

0 comments on commit 672d07f

Please sign in to comment.