Skip to content
Open
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
282 changes: 259 additions & 23 deletions AllowedUploadsPlugin.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,12 @@ function manage($args, $request) {
if ($request->getUserVar('save')) {
$form->readInputData();
if ($form->validate()) {
$form->execute();
return new JSONMessage(true);
$executeResult = $form->execute();
if ($executeResult !== false) {
return new JSONMessage(true);
}
}
return new JSONMessage(true, $form->fetch($request));
} else {
$form->initData();
}
Expand All @@ -106,7 +109,228 @@ function manage($args, $request) {
}

/**
* Check the uploaded file in wizard
* Get MIME type from file content
* @param string $filePath Path to the file
* @return string|false MIME type or false if detection fails
*/
private function getMimeTypeFromFile($filePath) {
if (!file_exists($filePath)) {
error_log("AllowedUploads: File not found for MIME detection: " . $filePath);
return false;
}

// Use finfo if available
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
if ($finfo) {
$mimeType = finfo_file($finfo, $filePath);
finfo_close($finfo);
if ($mimeType !== false) {
return $mimeType;
}
error_log("AllowedUploads: finfo_file failed for: " . $filePath);
} else {
error_log("AllowedUploads: finfo_open failed");
}
}

// Fallback to mime_content_type as failsafe
if (function_exists('mime_content_type')) {
$mimeType = mime_content_type($filePath);
if ($mimeType !== false) {
return $mimeType;
}
error_log("AllowedUploads: mime_content_type failed for: " . $filePath);
}

error_log("AllowedUploads: All MIME detection methods failed for: " . $filePath);
return false;
}

/**
* Get expected MIME types for file extension
* @param string $extension File extension
* @return array Array of acceptable MIME types
*/
private function getExpectedMimeTypes($extension) {
$mimeMap = [
// Document formats
'doc' => ['application/msword'],
'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'odt' => ['application/vnd.oasis.opendocument.text'],
'pdf' => ['application/pdf'],
'rtf' => ['application/rtf', 'text/rtf'],
'txt' => ['text/plain'],

// LaTeX and related
'bib' => ['text/x-bibtex', 'application/x-bibtex'],
'latex' => ['text/x-tex', 'application/x-latex'],
'tex' => ['text/x-tex', 'application/x-tex'],

// Spreadsheets and data
'csv' => ['text/csv', 'text/plain'],
'ods' => ['application/vnd.oasis.opendocument.spreadsheet'],
'tsv' => ['text/tab-separated-values', 'text/plain'],
'xls' => ['application/vnd.ms-excel'],
'xlsx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],

// Data formats
'json' => ['application/json'],
'xml' => ['application/xml', 'text/xml'],
'yaml' => ['text/yaml', 'application/yaml'],
'yml' => ['text/yaml', 'application/yaml'],

// Presentations
'odp' => ['application/vnd.oasis.opendocument.presentation'],
'ppt' => ['application/vnd.ms-powerpoint'],
'pptx' => ['application/vnd.openxmlformats-officedocument.presentationml.presentation'],

// Images
'bmp' => ['image/bmp'],
'eps' => ['application/postscript'],
'gif' => ['image/gif'],
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
'svg' => ['image/svg+xml'],
'tif' => ['image/tiff'],
'tiff' => ['image/tiff'],

// Vector graphics
'ai' => ['application/postscript'],
'ps' => ['application/postscript'],

// Archives
'7z' => ['application/x-7z-compressed'],
'gz' => ['application/gzip'],
'tar' => ['application/x-tar'],
'tar.bz2' => ['application/x-bzip2'],
'tar.gz' => ['application/gzip'],
'tar.xz' => ['application/x-xz'],
'zip' => ['application/zip'],

// Code and markup
'htm' => ['text/html'],
'html' => ['text/html'],
'ipynb' => ['application/json'],
'md' => ['text/markdown', 'text/plain'],
'py' => ['text/x-python', 'text/plain'],
'r' => ['text/plain'],

// Statistical formats
'dta' => ['application/x-stata-dta'],
'sas7bdat' => ['application/x-sas-data'],
'sav' => ['application/x-spss-sav'],

// Audio/Video
'avi' => ['video/x-msvideo'],
'mp3' => ['audio/mpeg'],
'mp4' => ['video/mp4'],
'wav' => ['audio/wav'],
];

return $mimeMap[strtolower($extension)] ?? [];
}

/**
* Validate file extension and optionally MIME type
* @param string $fileName Original filename
* @param string|null $filePath Path to uploaded file
* @param string $allowedExtensions Semicolon-separated list of allowed extensions
* @param int $contextId Context ID
* @return array Array with 'valid' boolean and 'error' message
*/
private function validateFileType($fileName, $filePath, $allowedExtensions, $contextId) {
$request = Application::get()->getRequest();
$user = $request->getUser();
$userId = $user ? $user->getId() : 'anonymous';

$parts = explode('.', $fileName);
$allowedExtensionsArray = array_filter(array_map('trim', explode(';', $allowedExtensions)), 'strlen');

// Check for extensionless files
if (count($parts) < 2) {
return [
'valid' => false,
'error' => __('plugins.generic.allowedUploads.error.noExtension', ['fileName' => $fileName])
];
}

// Check for empty files (if we have a file path and empty files are disallowed)
$allowEmptyFiles = $this->getSetting($contextId, 'allowEmptyFiles') ?? true;
if (!$allowEmptyFiles && $filePath && file_exists($filePath) && filesize($filePath) === 0) {
return [
'valid' => false,
'error' => __('plugins.generic.allowedUploads.error.emptyFile', ['fileName' => $fileName])
];
}

// Check for multiple extensions
if (count($parts) > 2) {
// Check if a double extension is explicitly allowed
$doubleExtension = strtolower($parts[count($parts)-2] . '.' . $parts[count($parts)-1]);

if (in_array($doubleExtension, $allowedExtensionsArray)) {
$extension = $doubleExtension; // Use the double extension for MIME type checking
} else {
return [
'valid' => false,
'error' => __('plugins.generic.allowedUploads.error.multiExtension', ['fileName' => $fileName])
];
}
} else {
$extension = strtolower(end($parts));
}

// Check extension against allowlist
if (!in_array($extension, $allowedExtensionsArray)) {
return [
'valid' => false,
'error' => __('plugins.generic.allowedUploads.error', ['allowedExtensions' => $allowedExtensions])
];
}

// Check if MIME type validation is enabled and we have a file to check
$validateMimeType = $this->getSetting($contextId, 'validateMimeType');
if (!$validateMimeType || !$filePath || !file_exists($filePath)) {
return ['valid' => true, 'error' => null];
}

// Perform MIME type validation
$detectedMimeType = $this->getMimeTypeFromFile($filePath);
if ($detectedMimeType === false) {
error_log("AllowedUploads: Could not determine MIME type of file " . $fileName);
return ['valid' => true, 'error' => null];
}

// Allow empty MIME types to pass (handled separately above)
if ($detectedMimeType === 'application/x-empty' || $detectedMimeType === 'inode/x-empty') {
return ['valid' => true, 'error' => null];
}

$expectedMimeTypes = $this->getExpectedMimeTypes($extension);
if (!empty($expectedMimeTypes) && !in_array($detectedMimeType, $expectedMimeTypes)) {
error_log("AllowedUploads: SECURITY - MIME type mismatch for user {$userId} in context {$contextId}: {$fileName} " .
"(detected: {$detectedMimeType}, expected: " . implode(', ', $expectedMimeTypes) . ")");
return [
'valid' => false,
'error' => __('plugins.generic.allowedUploads.error.mimeType', [
'fileName' => $fileName,
'detectedType' => $detectedMimeType,
'allowedExtensions' => $allowedExtensions
])
];
}

return ['valid' => true, 'error' => null];
}

/**
* Check the uploaded file in the submission wizard
* Hook: SubmissionFile::validate
* @param string $hookName Name of hook being called
* @param array $params Hook parameters: errors array, submission, props, actions, locale
* @return bool Always returns false to allow other hooks to process
*/
function checkUploadWizard($hookName, $params) {
$props = $params[2];
Expand All @@ -116,49 +340,61 @@ function checkUploadWizard($hookName, $params) {
$errors =& $params[0];
$request = Application::get()->getRequest();
$context = $request->getContext();
$fileName = $props['name'][$locale];
$tmp = explode('.',$fileName);
$extension = strtolower(end($tmp));
$contextId = $context->getId();

$allowedExtensions = $this->getSetting($context->getId(), 'allowedExtensions');
$allowedExtensions = $this->getSetting($contextId, 'allowedExtensions');

if ($allowedExtensions){
$allowedExtensionsArray = array_filter(array_map('trim', explode(';', $allowedExtensions )), 'strlen');
if (!in_array($extension, $allowedExtensionsArray)){
$errors[] = __('plugins.generic.allowedUploads.error', array('allowedExtensions' => $allowedExtensions));
// Get the uploaded file path from $_FILES
$filePath = null;
if (isset($_FILES['file']) && isset($_FILES['file']['tmp_name'])) {
$filePath = $_FILES['file']['tmp_name'];
}
}


$validation = $this->validateFileType($fileName, $filePath, $allowedExtensions, $contextId);
if (!$validation['valid']) {
$errors[] = $validation['error'];
}
}
}

return false;
}

/**
* Check the uploaded file
* Check the uploaded file in the upload form
* Hook: submissionfilesuploadform::validate
* @param string $hookName Name of hook being called
* @param array $params Hook parameters: form object
* @return bool Always returns fale to allow other hooks to process
*/
function checkUpload($hookName, $params) {
$form = $params[0];
$request = Application::get()->getRequest();
$context = $request->getContext();
$contextId = $context->getId();
$userVars = $request->getUserVars();
$fileName = $userVars['name'];
$tmp = explode('.',$fileName);
$extension = strtolower(end($tmp));
$fileName = $userVars['name'] ?? null;
if (!$fileName) {
return false;
}

$allowedExtensions = $this->getSetting($context->getId(), 'allowedExtensions');
$allowedExtensions = $this->getSetting($contextId, 'allowedExtensions');

if ($allowedExtensions){

$allowedExtensionsArray = array_filter(array_map('trim', explode(';', $allowedExtensions )), 'strlen');

if (!in_array($extension, $allowedExtensionsArray)){
$form->addError('allowedFileType', __('plugins.generic.allowedUploads.error', ['allowedExtensions' => $allowedExtensions]));
// Get the uploaded file path from $_FILES
$filePath = null;
if (isset($_FILES['file']) && isset($_FILES['file']['tmp_name'])) {
$filePath = $_FILES['file']['tmp_name'];
}

$validation = $this->validateFileType($fileName, $filePath, $allowedExtensions, $contextId);
if (!$validation['valid']) {
$form->addError('allowedFileType', $validation['error']);
}
}
return false;
}


}
?>
Loading