Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions lib/Controller/WopiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace OCA\Richdocuments\Controller;

use OC\Files\Stream\HashWrapper;
use OCA\Files_Versions\Versions\IVersionManager;
use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\AppInfo\Application;
Expand Down Expand Up @@ -215,6 +216,13 @@ public function checkFileInfo(
'ServerPrivateInfo' => [],
];

// Only reported when we already know it. Never hash here: CheckFileInfo
// is on the document-open path and the files can be large.
$sha256 = $this->getStoredSha256($file);
if ($sha256 !== null) {
$response['SHA256'] = $sha256;
}

if ($this->capabilitiesService->hasSettingIframeSupport()) {
$token = $this->generateSettingToken($userId);
if (!$isPublic) {
Expand Down Expand Up @@ -653,6 +661,14 @@ public function putFile(

$content = fopen('php://input', 'rb');

// Hash the body as it streams past, so the editor can later tell
// whether what is in storage is what it uploaded. This costs no
// extra read: the bytes are going through us anyway.
$sha256 = null;
$content = HashWrapper::wrap($content, 'sha256', function (string $hash) use (&$sha256): void {
$sha256 = $hash;
});

$freespace = (int)$file->getStorage()->free_space($file->getInternalPath());
$contentLength = (int)$this->request->getHeader('Content-Length');

Expand All @@ -666,6 +682,8 @@ public function putFile(
return new JSONResponse(['message' => 'File locked'], Http::STATUS_INTERNAL_SERVER_ERROR);
}

$this->storeSha256($file, $sha256);

if ($wopi->hasTemplateId()) {
$wopi->setTemplateId(null);
$this->wopiMapper->update($wopi);
Expand Down Expand Up @@ -898,6 +916,64 @@ private function getLock(Wopi $wopi, string $lock): JSONResponse {
return new JSONResponse();
}

/**
* Remember the SHA-256 of the contents we have just written, in the same
* cache field the DAV endpoint uses for the checksums a sync client sends.
*
* The scanner clears that field whenever a file changes, so a stored value
* is never stale: it either describes the current contents or is absent.
* That is what lets us hand it out again without re-reading the file.
*/
private function storeSha256(File $file, ?string $sha256): void {
if ($sha256 === null || $sha256 === '') {
return;
}

try {
// After putContent(), so the rescan that clears the field has run.
$file->getStorage()->getCache()->update($file->getId(), [
'checksum' => 'SHA256:' . $sha256,
]);
} catch (\Throwable $e) {
// A checksum we fail to record only costs the editor a hint.
$this->logger->debug('Could not store the SHA-256 of the uploaded file', ['exception' => $e]);
}
}

/**
* The stored SHA-256 of this file, Base64-encoded for the WOPI SHA256
* field, or null when none is on record.
*
* Checksums are stored as space-separated TYPE:HEX pairs, so a file may
* carry an MD5 or SHA1 from a sync client and no SHA-256 at all.
*/
private function getStoredSha256(File $file): ?string {
try {
$checksums = $file->getChecksum();
} catch (\Throwable $e) {
return null;
}

if (!is_string($checksums) || $checksums === '') {
return null;
}

foreach (explode(' ', $checksums) as $checksum) {
if (stripos($checksum, 'SHA256:') !== 0) {
continue;
}

$raw = @hex2bin(substr($checksum, strlen('SHA256:')));
if ($raw === false || strlen($raw) !== 32) {
return null;
}

return base64_encode($raw);
}

return null;
}

/**
* @throws NotFoundException
* @throws GenericFileException
Expand Down
Loading