using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Web; using ICSharpCode.SharpZipLib.Zip; using JetBrains.Annotations; using Orchard.ContentManagement; using Orchard.FileSystems.Media; using Orchard.Localization; using Orchard.Media.Models; using Orchard.Security; using Orchard.Settings; using Orchard.Validation; namespace Orchard.Media.Services { /// /// The MediaService class provides the services o manipulate media entities (files / folders). /// Among other things it provides filtering functionalities on file types. /// The actual manipulation of the files is, however, delegated to the IStorageProvider. /// [UsedImplicitly] public class MediaService : IMediaService { private readonly IStorageProvider _storageProvider; private readonly IOrchardServices _orchardServices; /// /// Initializes a new instance of the MediaService class with a given IStorageProvider and IOrchardServices. /// /// The storage provider. /// The orchard services provider. public MediaService(IStorageProvider storageProvider, IOrchardServices orchardServices) { _storageProvider = storageProvider; _orchardServices = orchardServices; T = NullLocalizer.Instance; } public Localizer T { get; set; } /// /// Retrieves the public path based on the relative path within the media directory. /// /// /// "/Media/Default/InnerDirectory/Test.txt" based on the input "InnerDirectory/Test.txt" /// /// The relative path within the media directory. /// The public path relative to the application url. public string GetPublicUrl(string relativePath) { Argument.ThrowIfNullOrEmpty(relativePath, "relativePath"); return _storageProvider.GetPublicUrl(relativePath); } /// /// Retrieves the media folders within a given relative path. /// /// The path where to retrieve the media folder from. null means root. /// The media folder in the given path. public IEnumerable GetMediaFolders(string relativePath) { return _storageProvider.ListFolders(relativePath).Select(folder => new MediaFolder { Name = folder.GetName(), Size = folder.GetSize(), LastUpdated = folder.GetLastUpdated(), MediaPath = folder.GetPath() }).Where(f => !f.Name.Equals("RecipeJournal", StringComparison.OrdinalIgnoreCase)); } /// /// Retrieves the media files within a given relative path. /// /// The path where to retrieve the media files from. null means root. /// The media files in the given path. public IEnumerable GetMediaFiles(string relativePath) { return _storageProvider.ListFiles(relativePath).Select(file => new MediaFile { Name = file.GetName(), Size = file.GetSize(), LastUpdated = file.GetLastUpdated(), Type = file.GetFileType(), FolderName = relativePath }); } /// /// Creates a media folder. /// /// The path where to create the new folder. null means root. /// The name of the folder to be created. public void CreateFolder(string relativePath, string folderName) { Argument.ThrowIfNullOrEmpty(folderName, "folderName"); _storageProvider.CreateFolder(relativePath == null ? folderName : _storageProvider.Combine(relativePath, folderName)); } /// /// Deletes a media folder. /// /// The path to the folder to be deleted. public void DeleteFolder(string folderPath) { Argument.ThrowIfNullOrEmpty(folderPath, "folderPath"); _storageProvider.DeleteFolder(folderPath); } /// /// Renames a media folder. /// /// The path to the folder to be renamed. /// The new folder name. public void RenameFolder(string folderPath, string newFolderName) { Argument.ThrowIfNullOrEmpty(folderPath, "folderPath"); Argument.ThrowIfNullOrEmpty(newFolderName, "newFolderName"); _storageProvider.RenameFolder(folderPath, _storageProvider.Combine(Path.GetDirectoryName(folderPath), newFolderName)); } /// /// Deletes a media file. /// /// The folder path. /// The file name. public void DeleteFile(string folderPath, string fileName) { Argument.ThrowIfNullOrEmpty(folderPath, "folderPath"); Argument.ThrowIfNullOrEmpty(fileName, "fileName"); _storageProvider.DeleteFile(_storageProvider.Combine(folderPath, fileName)); } /// /// Renames a media file. /// /// The path to the file's parent folder. /// The current file name. /// The new file name. public void RenameFile(string folderPath, string currentFileName, string newFileName) { Argument.ThrowIfNullOrEmpty(folderPath, "folderPath"); Argument.ThrowIfNullOrEmpty(currentFileName, "currentFileName"); Argument.ThrowIfNullOrEmpty(newFileName, "newFileName"); if (!FileAllowed(newFileName, false)) { throw new ArgumentException(T("New file name {0} is not allowed", newFileName).ToString()); } _storageProvider.RenameFile(_storageProvider.Combine(folderPath, currentFileName), _storageProvider.Combine(Path.GetDirectoryName(folderPath), newFileName)); } /// /// Uploads a media file based on a posted file. /// /// The path to the folder where to upload the file. /// The file to upload. /// Boolean value indicating weather zip files should be extracted. /// The path to the uploaded file. public string UploadMediaFile(string folderPath, HttpPostedFileBase postedFile, bool extractZip) { Argument.ThrowIfNullOrEmpty(folderPath, "folderPath"); Argument.ThrowIfNull(postedFile, "postedFile"); return UploadMediaFile(folderPath, Path.GetFileName(postedFile.FileName), postedFile.InputStream, extractZip); } /// /// Uploads a media file based on an array of bytes. /// /// The path to the folder where to upload the file. /// The file name. /// The array of bytes with the file's contents. /// Boolean value indicating weather zip files should be extracted. /// The path to the uploaded file. public string UploadMediaFile(string folderPath, string fileName, byte [] bytes, bool extractZip) { Argument.ThrowIfNullOrEmpty(folderPath, "folderPath"); Argument.ThrowIfNullOrEmpty(fileName, "fileName"); Argument.ThrowIfNull(bytes, "bytes"); return UploadMediaFile(folderPath, fileName, new MemoryStream(bytes), extractZip); } /// /// Uploads a media file based on a stream. /// /// The folder path to where to upload the file. /// The file name. /// The stream with the file's contents. /// Boolean value indicating weather zip files should be extracted. /// The path to the uploaded file. public string UploadMediaFile(string folderPath, string fileName, Stream inputStream, bool extractZip) { Argument.ThrowIfNullOrEmpty(folderPath, "folderPath"); Argument.ThrowIfNullOrEmpty(fileName, "fileName"); Argument.ThrowIfNull(inputStream, "inputStream"); if (extractZip && IsZipFile(Path.GetExtension(fileName))) { UnzipMediaFileArchive(folderPath, inputStream); // Don't save the zip file. return _storageProvider.GetPublicUrl(folderPath); } if (FileAllowed(fileName, true)) { string filePath = _storageProvider.Combine(folderPath, fileName); _storageProvider.SaveStream(filePath, inputStream); return _storageProvider.GetPublicUrl(filePath); } return null; } /// /// Verifies if a file is allowed based on its name and the policies defined by the black / white lists. /// /// The file name of the file to validate. /// Boolean value indicating weather zip files are allowed. /// True if the file is allowed; false if otherwise. protected bool FileAllowed(string fileName, bool allowZip) { string localFileName = GetFileName(fileName); string extension = GetExtension(localFileName); if (string.IsNullOrEmpty(localFileName) || string.IsNullOrEmpty(extension)) { return false; } ISite currentSite = _orchardServices.WorkContext.CurrentSite; IUser currentUser = _orchardServices.WorkContext.CurrentUser; // zip files at the top level are allowed since this is how you upload multiple files at once. if (IsZipFile(extension)) { return allowZip; } // whitelist does not apply to the superuser if (currentUser == null || !currentSite.SuperUser.Equals(currentUser.UserName, StringComparison.Ordinal)) { // must be in the whitelist MediaSettingsPart mediaSettings = currentSite.As(); if (mediaSettings == null || !mediaSettings.UploadAllowedFileTypeWhitelist.ToUpperInvariant().Split(' ').Contains(extension.ToUpperInvariant())) { return false; } } // blacklist always applies if (string.Equals(localFileName, "web.config", StringComparison.OrdinalIgnoreCase)) { return false; } return true; } /// /// Unzips a media archive file. /// /// The folder where to unzip the file. /// The archive file stream. protected void UnzipMediaFileArchive(string targetFolder, Stream zipStream) { Argument.ThrowIfNullOrEmpty(targetFolder, "targetFolder"); Argument.ThrowIfNull(zipStream, "zipStream"); var fileInflater = new ZipInputStream(zipStream); ZipEntry entry; // We want to preserve whatever directory structure the zip file contained instead // of flattening it. // The API below doesn't necessarily return the entries in the zip file in any order. // That means the files in subdirectories can be returned as entries from the stream // before the directories that contain them, so we create directories as soon as first // file below their path is encountered. while ((entry = fileInflater.GetNextEntry()) != null) { if (!entry.IsDirectory && !string.IsNullOrEmpty(entry.Name)) { // skip disallowed files if (FileAllowed(entry.Name, false)) { string fullFileName = _storageProvider.Combine(targetFolder, entry.Name); _storageProvider.TrySaveStream(fullFileName, fileInflater); } } } } /// /// Determines if a file is a Zip Archive based on its extension. /// /// The extension of the file to analyze. /// True if the file is a Zip archive; false otherwise. private static bool IsZipFile(string extension) { return string.Equals(extension.TrimStart('.'), "zip", StringComparison.OrdinalIgnoreCase); } private static string GetFileName(string fileName) { return Path.GetFileName(fileName).Trim(); } private static string GetExtension(string fileName) { return Path.GetExtension(fileName).Trim().TrimStart('.'); } } }