fileSystem = $fileSystem; $this->entityTypeManager = $entityTypeManager; $this->streamWrapperManager = $streamWrapperManager; $this->eventDispatcher = $eventDispatcher; $this->mimeTypeGuesser = $mimeTypeGuesser; $this->currentUser = $currentUser; $this->requestStack = $requestStack; $this->fileRepository = $fileRepository; $this->fileValidator = $file_validator; } /** * {@inheritdoc} */ public function handleFileUpload(UploadedFileInterface $uploadedFile, array $validators = [], string $destination = 'temporary://', /*FileExists*/$fileExists = FileExists::Replace): FileUploadResult { if (!$fileExists instanceof FileExists) { // @phpstan-ignore staticMethod.deprecated $fileExists = FileExists::fromLegacyInt($fileExists, __METHOD__); } $result = new FileUploadResult(); $violations = $uploadedFile->validate($this->validatorFactory->createValidator()); if (count($violations) > 0) { $result->addViolations($violations); return $result; } $originalName = $uploadedFile->getClientOriginalName(); $extensions = $this->handleExtensionValidation($validators); // Assert that the destination contains a valid stream. $destinationScheme = $this->streamWrapperManager::getScheme($destination); if (!$this->streamWrapperManager->isValidScheme($destinationScheme)) { throw new InvalidStreamWrapperException(sprintf('The file could not be uploaded because the destination "%s" is invalid.', $destination)); } // A file URI may already have a trailing slash or look like "public://". if (!str_ends_with($destination, '/')) { $destination .= '/'; } // Call an event to sanitize the filename and to attempt to address security // issues caused by common server setups. $event = new FileUploadSanitizeNameEvent($originalName, $extensions); $this->eventDispatcher->dispatch($event); $filename = $event->getFilename(); $mimeType = $this->mimeTypeGuesser->guessMimeType($filename); $destinationFilename = $this->fileSystem->getDestinationFilename($destination . $filename, $fileExists); if ($destinationFilename === FALSE) { throw new FileExistsException(sprintf('Destination file "%s" exists', $destinationFilename)); } // Lock based on the prepared file URI. $lock_id = $this->generateLockId($destinationFilename); try { if (!$this->lock->acquire($lock_id)) { throw new LockAcquiringException( sprintf( 'File "%s" is already locked for writing.', $destinationFilename ) ); } $file = File::create([ 'uid' => $this->currentUser->id(), 'status' => 0, 'uri' => $uploadedFile->getRealPath(), ]); // This will be replaced later with a filename based on the destination. $file->setFilename($filename); $file->setMimeType($mimeType); $file->setSize($uploadedFile->getSize()); // Add in our check of the file name length. $validators['FileNameLength'] = []; // Call the validation functions specified by this function's caller. $violations = $this->fileValidator->validate($file, $validators); if (count($violations) > 0) { $result->addViolations($violations); return $result; } $file->setFileUri($destinationFilename); if (!$this->moveUploadedFile($uploadedFile, $file->getFileUri())) { throw new FileWriteException( 'File upload error. Could not move uploaded file.' ); } // Update the filename with any changes as a result of security or // renaming due to an existing file. $file->setFilename($this->fileSystem->basename($file->getFileUri())); if ($fileExists === FileExists::Replace) { $existingFile = $this->fileRepository->loadByUri($file->getFileUri()); if ($existingFile) { $file->fid = $existingFile->id(); $file->setOriginalId($existingFile->id()); } } $result->setOriginalFilename($originalName) ->setSanitizedFilename($filename) ->setFile($file); // If the filename has been modified, let the user know. if ($event->isSecurityRename()) { $result->setSecurityRename(); } // Set the permissions on the new file. $this->fileSystem->chmod($file->getFileUri()); // We can now validate the file object itself before it's saved. $violations = $file->validate(); if (count($violations) > 0) { $result->addViolations($violations); return $result; } // If we made it this far it's safe to record this file in the database. $file->save(); // Allow an anonymous user who creates a non-public file to see it. See // \Drupal\file\FileAccessControlHandler::checkAccess(). if ($this->currentUser->isAnonymous() && $destinationScheme !== 'public') { $session = $this->requestStack->getCurrentRequest()->getSession(); $allowed_temp_files = $session->get('anonymous_allowed_file_ids', []); $allowed_temp_files[$file->id()] = $file->id(); $session->set('anonymous_allowed_file_ids', $allowed_temp_files); } } finally { $this->lock->release($lock_id); } return $result; } /** * Move the uploaded file from the temporary path to the destination. * * @param \Drupal\file\Upload\UploadedFileInterface $uploadedFile * The uploaded file. * @param string $uri * The destination URI. * * @return bool * Returns FALSE if moving failed. * * @see https://www.drupal.org/project/drupal/issues/2940383 */ protected function moveUploadedFile(UploadedFileInterface $uploadedFile, string $uri): bool { if ($uploadedFile instanceof FormUploadedFile) { return $this->fileSystem->moveUploadedFile($uploadedFile->getRealPath(), $uri); } // We use FileExists::Error) as the file location has already // been determined above in FileSystem::getDestinationFilename(). return $this->fileSystem->move($uploadedFile->getRealPath(), $uri, FileExists::Error); } /** * Gets the list of allowed extensions and updates the validators. * * This will add an extension validator to the list of validators if one is * not set. * * If the extension validator is set, but no extensions are specified, it * means all extensions are allowed, so the validator is removed from the list * of validators. * * @param array $validators * The file validators in use. * * @return string * The space delimited list of allowed file extensions. */ protected function handleExtensionValidation(array &$validators): string { // No validator was provided, so add one using the default list. // Build a default non-munged safe list for // \Drupal\system\EventSubscriber\SecurityFileUploadEventSubscriber::sanitizeName(). if (!isset($validators['FileExtension'])) { $validators['FileExtension'] = ['extensions' => self::DEFAULT_EXTENSIONS]; return self::DEFAULT_EXTENSIONS; } // Check if we want to allow all extensions. if (!isset($validators['FileExtension']['extensions'])) { // If 'FileExtension' is set and the list is empty then the caller wants // to allow any extension. In this case we have to remove the validator // or else it will reject all extensions. unset($validators['FileExtension']); return ''; } return $validators['FileExtension']['extensions']; } /** * Generates a lock ID based on the file URI. */ protected static function generateLockId(string $fileUri): string { return 'file:upload:' . Crypt::hashBase64($fileUri); } }