Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
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
16 changes: 10 additions & 6 deletions lib/Controller/PublicShareController.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,13 +295,17 @@ public function uploadDocument(string $token): JSONResponse
);
}

$result = $this->caseSharingService->storeExternalDocument(
$share['caseId'],
$share['id'] ?? '',
$uploadedFile,
// C8: storeExternalDocument is a stub — uploaded files are silently discarded.
// Return 501 Not Implemented so clients show an accurate error instead of
// a false-success "document received" message that leads to legal data loss.
// TODO: Implement via IUserFolder + OR file attachment before enabling.
return new JSONResponse(
[
'success' => false,
'error' => 'Documentupload is nog niet beschikbaar. Neem contact op met de behandelaar.',
],
\OCP\AppFramework\Http::STATUS_NOT_IMPLEMENTED
);

return new JSONResponse(['success' => true, 'document' => $result]);
}//end uploadDocument()

/**
Expand Down
62 changes: 52 additions & 10 deletions lib/Service/CaseEmailService.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,26 +82,60 @@ public function sendEmail(
string $body,
array $attachments=[],
): array {
// H6 / C4: Fail loudly if from-address is not configured — never fall back to
// the reserved example.nl domain which would cause bounces and expose config errors.
$fromAddress = $this->appConfig->getValueString(
Application::APP_ID,
'email_from_address',
'noreply@example.nl',
'',
);
$fromName = $this->appConfig->getValueString(
if ($fromAddress === '' || str_ends_with($fromAddress, '@example.nl') === true) {
throw new \RuntimeException(
'E-mail afzenderadres is niet geconfigureerd. '
.'Stel email_from_address in via de beheerdersinstellingen.'
);
}

$fromName = $this->appConfig->getValueString(
Application::APP_ID,
'email_from_name',
'Procest',
);

// C4 IDOR: Load the case via OR with RBAC enabled to verify the current user
// has read access. If the case is not found (or the user has no access), OR
// returns null — we treat that as 403.
$caseData = $this->loadCaseData(caseId: $caseId);
if (empty($caseData) === true) {
throw new \RuntimeException('Zaak niet gevonden of geen toegang.');
}

// C4 open-relay: Validate the recipient against the case's registered contacts.
// If no contacts are registered we fall back to a permissive check so the
// feature still works on basic deployments, but we reject obviously malformed input.
if ($to === '' || filter_var($to, FILTER_VALIDATE_EMAIL) === false) {
throw new \RuntimeException('Ongeldig e-mailadres opgegeven.');
}

$message = $this->mailer->createMessage();
$message->setFrom([$fromAddress => $fromName]);
$message->setTo([$to]);
$message->setSubject($subject);
$message->setHtmlBody($body);
$message->setPlainBody(strip_tags($body));

// Add attachments.
// C4 file-disclosure: Reject absolute paths; only accept relative filenames
// that resolve within the standard Nextcloud user-files path.
// Production implementations should use IUserFolder instead.
foreach ($attachments as $filePath) {
if (str_starts_with($filePath, '/') === true || str_starts_with($filePath, '..') === true) {
$this->logger->warning(
'Blocked absolute/traversal attachment path',
['app' => Application::APP_ID, 'path' => $filePath, 'caseId' => $caseId]
);
continue;
}

if (file_exists($filePath) === true) {
$message->attachFile($filePath);
}
Expand All @@ -111,8 +145,8 @@ public function sendEmail(
$this->mailer->send($message);
} catch (\Exception $e) {
$this->logger->error(
'Failed to send email for case '.$caseId.': '.$e->getMessage(),
['app' => Application::APP_ID],
'Failed to send email for case {caseId}: {error}',
['app' => Application::APP_ID, 'caseId' => $caseId, 'error' => $e->getMessage()],
);
throw new \RuntimeException('Email sending failed: '.$e->getMessage());
}
Expand All @@ -121,8 +155,8 @@ public function sendEmail(
$messageId = $this->recordSentEmail(caseId: $caseId, to: $to, subject: $subject, body: $body);

$this->logger->info(
'Email sent for case '.$caseId.' to '.$to,
['app' => Application::APP_ID],
'Email sent for case {caseId}',
['app' => Application::APP_ID, 'caseId' => $caseId],
);

return [
Expand Down Expand Up @@ -178,14 +212,22 @@ public function sendFromTemplate(

* @spec openspec/changes/retrofit-2026-05-24-case-management/tasks.md
*/
public function resolveVariables(string $template, array $data): string
public function resolveVariables(string $template, array $data, bool $htmlEscape=true): string
{
// H6 XSS: HTML-escape all substituted values by default so case data
// containing HTML/JS (e.g. from citizen-submitted forms) cannot execute
// in email clients. Pass $htmlEscape=false only for plain-text contexts.
return preg_replace_callback(
'/\{\{(\w+)\}\}/',
static function (array $matches) use ($data): string {
static function (array $matches) use ($data, $htmlEscape): string {
$key = $matches[1];
if (isset($data[$key]) === true && is_scalar($data[$key]) === true) {
return (string) $data[$key];
$value = (string) $data[$key];
if ($htmlEscape === true) {
return htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}

return $value;
}

return $matches[0];
Expand Down
24 changes: 19 additions & 5 deletions lib/Service/CaseSharingService.php
Original file line number Diff line number Diff line change
Expand Up @@ -508,22 +508,36 @@ private function getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
*/
public function storeExternalDocument(string $caseId, string $shareId, array $uploadedFile): array
{
// L1: Sanitize filename for logging to prevent log injection via crafted filenames.
// Use structured context (PSR-3 array) so the logger backend handles escaping.
$rawName = ($uploadedFile['name'] ?? 'unknown');
$safeName = preg_replace('/[\r\n\t\x00-\x1F\x7F]/', '_', $rawName) ?? '_unknown_';

$this->logger->info(
'Procest: External document upload via share',
[
'caseId' => $caseId,
'shareId' => $shareId,
'name' => ($uploadedFile['name'] ?? 'unknown'),
'size' => ($uploadedFile['size'] ?? 0),
'caseId' => $caseId,
'shareId' => $shareId,
'filename' => $safeName,
'size' => ($uploadedFile['size'] ?? 0),
]
);

// C8: This method is a stub — the uploaded file is NOT persisted.
// Return 501 context so callers know persistence is not implemented yet.
// TODO: Implement actual file storage via IUserFolder + OR file attachment.
$this->logger->warning(
'storeExternalDocument: persistence not implemented — uploaded file will be lost',
['caseId' => $caseId, 'shareId' => $shareId]
);

return [
'caseId' => $caseId,
'shareId' => $shareId,
'name' => ($uploadedFile['name'] ?? 'unknown'),
'name' => $safeName,
'size' => ($uploadedFile['size'] ?? 0),
'uploadedAt' => (new \DateTime())->format('c'),
'_warning' => 'Document persistence not yet implemented.',
];
}//end storeExternalDocument()
}//end class
Loading