using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.TemporaryFile; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services; internal sealed class TemporaryFileService : ITemporaryFileService { private readonly ITemporaryFileRepository _temporaryFileRepository; private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator; private RuntimeSettings _runtimeSettings; private ContentSettings _contentSettings; public TemporaryFileService( ITemporaryFileRepository temporaryFileRepository, IOptionsMonitor runtimeOptionsMonitor, IOptionsMonitor contentOptionsMonitor, IFileStreamSecurityValidator fileStreamSecurityValidator) { _temporaryFileRepository = temporaryFileRepository; _fileStreamSecurityValidator = fileStreamSecurityValidator; _runtimeSettings = runtimeOptionsMonitor.CurrentValue; _contentSettings = contentOptionsMonitor.CurrentValue; runtimeOptionsMonitor.OnChange(x => _runtimeSettings = x); contentOptionsMonitor.OnChange(x => _contentSettings = x); } public async Task> CreateAsync(CreateTemporaryFileModel createModel) { TemporaryFileOperationStatus validationResult = Validate(createModel); if (validationResult != TemporaryFileOperationStatus.Success) { return Attempt.FailWithStatus(validationResult, null); } TemporaryFileModel? temporaryFileModel = await _temporaryFileRepository.GetAsync(createModel.Key); if (temporaryFileModel is not null) { return Attempt.FailWithStatus(TemporaryFileOperationStatus.KeyAlreadyUsed, null); } await using Stream dataStream = createModel.OpenReadStream(); dataStream.Seek(0, SeekOrigin.Begin); if (_fileStreamSecurityValidator.IsConsideredSafe(dataStream) is false) { return Attempt.FailWithStatus(TemporaryFileOperationStatus.UploadBlocked, null); } temporaryFileModel = new TemporaryFileModel { Key = createModel.Key, FileName = createModel.FileName, OpenReadStream = createModel.OpenReadStream, AvailableUntil = DateTime.Now.Add(_runtimeSettings.TemporaryFileLifeTime), }; await _temporaryFileRepository.SaveAsync(temporaryFileModel); return Attempt.Succeed(TemporaryFileOperationStatus.Success, temporaryFileModel); } private TemporaryFileOperationStatus Validate(TemporaryFileModelBase temporaryFileModel) { if (IsAllowedFileExtension(temporaryFileModel.FileName) == false) { return TemporaryFileOperationStatus.FileExtensionNotAllowed; } if (IsValidFileName(temporaryFileModel.FileName) == false) { return TemporaryFileOperationStatus.InvalidFileName; } return TemporaryFileOperationStatus.Success; } private bool IsAllowedFileExtension(string fileName) { var extension = Path.GetExtension(fileName)[1..]; return _contentSettings.IsFileAllowedForUpload(extension); } private static bool IsValidFileName(string fileName) => !string.IsNullOrEmpty(fileName) && fileName.IndexOfAny(Path.GetInvalidFileNameChars()) < 0; public async Task> DeleteAsync(Guid key) { TemporaryFileModel? model = await _temporaryFileRepository.GetAsync(key); if (model is null) { return Attempt.FailWithStatus(TemporaryFileOperationStatus.NotFound, null); } await _temporaryFileRepository.DeleteAsync(key); return Attempt.Succeed(TemporaryFileOperationStatus.Success, model); } public async Task GetAsync(Guid key) => await _temporaryFileRepository.GetAsync(key); public async Task> CleanUpOldTempFiles() => await _temporaryFileRepository.CleanUpOldTempFiles(DateTime.Now); }