Skip to content

Commit 57fddfc

Browse files
committed
refactor(FileUtils): improve filename sanitization logic and exception handling and use new method available in nextcloud 32+
1 parent bf5ab9b commit 57fddfc

1 file changed

Lines changed: 80 additions & 84 deletions

File tree

lib/Service/Utils/FileUtils.php

Lines changed: 80 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -12,123 +12,119 @@
1212
use OC;
1313

1414
class FileUtils {
15-
15+
1616
/**
17-
* Sanitize the filename to ensure it is valid, does not exceed length limits.
18-
*
19-
* @param string $filename The original filename to sanitize.
20-
* @param string $id A unique ID to append if necessary to ensure uniqueness.
21-
* @param int $recursionDepth The current recursion depth (used to prevent infinite loops).
22-
* @return string The sanitized and validated filename.
23-
*/
17+
* Sanitize the filename to ensure it is valid, does not exceed length limits.
18+
*
19+
* @param string $filename The original filename to sanitize.
20+
* @param string $id A unique ID to append if necessary to ensure uniqueness.
21+
* @param LoggerInterface $logger Logger for logging messages.
22+
* @param int $recursionDepth The current recursion depth (used to prevent infinite loops).
23+
* @param string|null $originalFilename The original filename for logging.
24+
* @return string The sanitized and validated filename.
25+
*/
2426
public static function sanitizeFilename(
2527
string $filename,
2628
string $id,
2729
LoggerInterface $logger,
2830
int $recursionDepth = 0,
29-
string $originalFilename = null
31+
?string $originalFilename = null
3032
): string {
31-
// Prevent infinite recursion by limiting the depth.
3233
if ($recursionDepth > 15) {
3334
$filename = 'Untitled_' . $id;
34-
$logger->warning('Maximum recursion depth reached while sanitizing filename: ' . $originalFilename . ' renaming to ' . $filename);
35+
$logger->warning('Maximum recursion depth reached while sanitizing filename: ' . ($originalFilename ?? $filename) . ' renaming to ' . $filename);
3536
return $filename;
3637
}
3738

38-
// If the original filename is not provided, use the current filename.
3939
if ($originalFilename === null) {
4040
$originalFilename = $filename;
4141
}
4242

43-
// Trim leading/trailing whitespace and trailing dots.
44-
$filename = rtrim(trim($filename), '.');
45-
46-
// Check if trimming altered the filename.
47-
$trimmed = ($originalFilename !== $filename);
48-
49-
// Helper function to append the ID before the file extension.
50-
$appendIdBeforeExtension = function ($filename, $id) {
51-
$pathInfo = pathinfo($filename);
52-
if (isset($pathInfo['extension'])) {
53-
return $pathInfo['filename'] . '_' . $id . '.' . $pathInfo['extension'];
54-
} else {
55-
return $filename . '_' . $id;
43+
// Use Nextcloud 32+ validator if available
44+
if (version_compare(OC::$server->getConfig()->getSystemValue('version', '0.0.0'), '32.0.0', '>=')) {
45+
$logger->debug('Using Nextcloud 32+ filename validator for sanitization.');
46+
try {
47+
return OC::$server->get(\OCP\Files\IFilenameValidator::class)->sanitizeFilename($filename);
48+
} catch (\InvalidArgumentException $exception) {
49+
$logger->error('Unable to sanitize filename: ' . $filename, ['exception' => $exception]);
50+
return 'Untitled_' . $id;
5651
}
57-
};
52+
} else {
53+
$logger->debug('Using legacy filename sanitization method.');
54+
}
55+
56+
// Trim whitespace and trailing dots
57+
$filename = rtrim(trim($filename), '.');
5858

59-
// Append the ID if trimming occurred and the ID is not already present.
60-
if ($trimmed && !str_contains($filename, $id)) {
61-
$filename = $appendIdBeforeExtension($filename, $id);
59+
// Append ID if needed
60+
if ($originalFilename !== $filename && strpos($filename, $id) === false) {
61+
$filename = self::appendIdBeforeExtension($filename, $id);
6262
}
6363

64-
// Ensure the filename length does not exceed the maximum allowed length.
64+
// Enforce max length
6565
$maxLength = 254;
6666
if (mb_strlen($filename) > $maxLength) {
67-
$pathInfo = pathinfo($filename);
68-
$baseLength = $maxLength - mb_strlen($id) - 2; // Account for '_' and '.'.
69-
if (isset($pathInfo['extension'])) {
70-
$baseLength -= mb_strlen($pathInfo['extension']);
71-
$filename = mb_substr($pathInfo['filename'], 0, $baseLength) . '_' . $id . '.' . $pathInfo['extension'];
72-
} else {
73-
$filename = mb_substr($filename, 0, $baseLength) . '_' . $id;
74-
}
67+
$filename = self::truncateAndAppendId($filename, $id, $maxLength);
7568
}
7669

7770
try {
78-
// Validate the filename using the Nextcloud filename validator.
79-
\OC::$server->get(\OCP\Files\IFilenameValidator::class)->validateFilename($filename);
80-
81-
// if recursion depth is greater than 0, log the change.
71+
OC::$server->get(\OCP\Files\IFilenameValidator::class)->validateFilename($filename);
8272
if ($recursionDepth > 0) {
8373
$logger->info('Filename sanitized successfully: "' . $filename . '" (original: "' . $originalFilename . '")');
8474
}
85-
8675
return $filename;
87-
} catch (InvalidPathException $exception) {
88-
$logger->warning('Invalid filename detected during sanitization: ' . $filename, ['exception' => $exception]);
76+
} catch (\Throwable $exception) {
77+
$logger->warning('Exception during filename validation: ' . $filename, ['exception' => $exception]);
78+
$filename = self::handleFilenameException($filename, $id, $exception, $logger);
79+
if (strpos($filename, $id) === false) {
80+
$filename = self::appendIdBeforeExtension($filename, $id);
81+
}
82+
return self::sanitizeFilename($filename, $id, $logger, $recursionDepth + 1, $originalFilename);
8983
}
84+
}
9085

91-
// Handle specific exceptions and adjust the filename accordingly.
92-
switch (true) {
93-
case $exception instanceof FileNameTooLongException:
94-
$filename = mb_substr($filename, 0, $maxLength - mb_strlen($id) - 2);
95-
break;
96-
97-
case $exception instanceof EmptyFileNameException:
98-
$filename = 'Untitled';
99-
break;
100-
101-
case $exception instanceof InvalidCharacterInPathException:
102-
if (preg_match('/"(.*?)"/', $exception->getMessage(), $matches)) {
103-
$invalidChars = array_merge(str_split($matches[1]), ['"']);
104-
$filename = str_replace($invalidChars, '-', $filename);
105-
}
106-
break;
107-
108-
case $exception instanceof InvalidDirectoryException:
109-
$logger->error('Invalid directory detected in filename: ' . $exception->getMessage());
110-
$filename = 'Untitled';
111-
break;
112-
113-
case $exception instanceof ReservedWordException:
114-
if (preg_match('/"(.*?)"/', $exception->getMessage(), $matches)) {
115-
$reservedWord = $matches[1];
116-
$filename = str_ireplace($reservedWord, '-' . $reservedWord . '-', $filename);
117-
}
118-
break;
119-
120-
default:
121-
$logger->error('Unknown exception encountered during filename sanitization: ' . $filename);
122-
$filename = 'Untitled';
123-
break;
86+
private static function appendIdBeforeExtension(string $filename, string $id): string {
87+
$pathInfo = pathinfo($filename);
88+
if (isset($pathInfo['extension'])) {
89+
return $pathInfo['filename'] . '_' . $id . '.' . $pathInfo['extension'];
12490
}
91+
return $filename . '_' . $id;
92+
}
12593

126-
// Append the ID if the filename was modified and does not already contain the ID.
127-
if (!str_contains($filename, $id)) {
128-
$filename = $appendIdBeforeExtension($filename, $id);
94+
private static function truncateAndAppendId(string $filename, string $id, int $maxLength): string {
95+
$pathInfo = pathinfo($filename);
96+
$baseLength = $maxLength - mb_strlen($id) - 2;
97+
if (isset($pathInfo['extension'])) {
98+
$baseLength -= mb_strlen($pathInfo['extension']);
99+
return mb_substr($pathInfo['filename'], 0, $baseLength) . '_' . $id . '.' . $pathInfo['extension'];
129100
}
101+
return mb_substr($filename, 0, $baseLength) . '_' . $id;
102+
}
130103

131-
// Recursively validate the adjusted filename.
132-
return self::sanitizeFilename($filename, $id, $logger, $recursionDepth + 1, $originalFilename);
104+
private static function handleFilenameException(string $filename, string $id, \Throwable $exception, LoggerInterface $logger): string {
105+
if ($exception instanceof FileNameTooLongException) {
106+
return mb_substr($filename, 0, 254 - mb_strlen($id) - 2);
107+
}
108+
if ($exception instanceof EmptyFileNameException) {
109+
return 'Untitled';
110+
}
111+
if ($exception instanceof InvalidCharacterInPathException) {
112+
if (preg_match('/"(.*?)"/', $exception->getMessage(), $matches)) {
113+
$invalidChars = array_merge(str_split($matches[1]), ['"']);
114+
return str_replace($invalidChars, '-', $filename);
115+
}
116+
}
117+
if ($exception instanceof InvalidDirectoryException) {
118+
$logger->error('Invalid directory detected in filename: ' . $exception->getMessage());
119+
return 'Untitled';
120+
}
121+
if ($exception instanceof ReservedWordException) {
122+
if (preg_match('/"(.*?)"/', $exception->getMessage(), $matches)) {
123+
$reservedWord = $matches[1];
124+
return str_ireplace($reservedWord, '-' . $reservedWord . '-', $filename);
125+
}
126+
}
127+
$logger->error('Unknown exception encountered during filename sanitization: ' . $filename);
128+
return 'Untitled';
133129
}
134-
}
130+
}

0 commit comments

Comments
 (0)