This is a migrated thread and some comments may be shown as answers.

Use real path with EditorImageBrowserController

5 Answers 587 Views
Editor
This is a migrated thread and some comments may be shown as answers.
Paul
Top achievements
Rank 1
Iron
Iron
Veteran
Paul asked on 16 Jul 2020, 07:48 PM

In the examples, a virtual path is used to the ~/Content folder. How can I remove all traces of this? The override on ContentPath can't return a real path or an error is thrown. I don't want any virtual paths at all. Only actual paths.

The reason for this is that I don't feel the images should reside in the application folder inetpub/wwwroot/MYAPP. Because if I republish MYAPP I am worried about content being replaced. Additionally it requires security right changes for the IIS_USR to this folder.

What I really want is to use a real path, even to a separate drive like d:\myapp\images\screen\username\. MyApp is the name of the application, screen is the name of the page hosing this specific editor, and username is the session user.

5 Answers, 1 is accepted

Sort by
0
Andeol
Top achievements
Rank 1
answered on 17 Jul 2020, 05:45 AM

Hi Paul,

I was wondering if you found a way to do it. I 'm having the same issue and I'm looking for another way to make it work.

I'm trying to override the method Read(string path) but no success so far.

If you have any idea, I'd be glad to hear from you.

 

Thank you 

0
Paul
Top achievements
Rank 1
Iron
Iron
Veteran
answered on 17 Jul 2020, 01:22 PM

I overrode all methods including AuthorizeRead, Read, ContentPath, etc. Then put breakpoints everywhere. The problem is that the path being passed between them changes. For example, you can step through ContentPath and look at the returned value. Next breakpoint you'll hit is AuthorizeRead and the passed in path is now different.

At this point I downloaded the Telerik source code and found EditorImageBrowserController is based on FileBrowserController. I've gone through the code in both and I think I see a path forward. My plan is to combine the two and redo the methods to use real paths. Then I'll route all image browsing to the same controller with a parameter value. Example: /EditorImageBrowser/Images?name=foo.jpg. Add a method on the controller for Images that uses a FilePathResult output to the proper file based on the name.

That said, I have not done this yet. I was hoping Telerik would have an example and I wouldn't need to spend a half day on it. They usually reply within 48 hours so I thought I'd wait a bit first. If I do tackle this and actually get it working I can post the results.

0
Paul
Top achievements
Rank 1
Iron
Iron
Veteran
answered on 17 Jul 2020, 07:01 PM

I did in fact get this working.

First Here's the CSHTML

.ImageBrowser(imageBrowser => imageBrowser
    .Image($"~/ImageBrowser/{nameof(ImageBrowserController.GetImage)}?imageName={{0}}")
    .Read(nameof(ImageBrowserController.Read), "ImageBrowser")
    .Create(nameof(ImageBrowserController.Create), "ImageBrowser")
    .Destroy(nameof(ImageBrowserController.Destroy), "ImageBrowser")
    .Upload(nameof(ImageBrowserController.Upload), "ImageBrowser")
    .Thumbnail(nameof(ImageBrowserController.Thumbnail), "ImageBrowser")
)

 

Next here's the ending controller which is named ImageBrowserController:

 

using Kendo.Mvc.UI;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;
 
namespace YourApplication.Controllers {
 
   /// <summary>
   /// Image browser handler for Kendo editor
   /// </summary>
   public class ImageBrowserController : Controller, IImageBrowserController {
 
      #region Private variables
 
      // Allowed image types to upload
      private const string ALLOWED_TYPES = "*.png,*.gif,*.jpg,*.jpeg,*.bmp";
      private static readonly IDictionary<string, ImageFormat> ImageFormats = new Dictionary<string, ImageFormat>{
         { "image/png", ImageFormat.Png },
         { "image/gif", ImageFormat.Gif },
         { "image/jpeg", ImageFormat.Jpeg },
         { "image/bmp", ImageFormat.Bmp }
      };
 
      // Thumnail sizes
      private const int ThumbnailHeight = 80;
      private const int ThumbnailWidth = 80;
 
      #endregion
 
      #region Private methods
 
      /// <summary>
      /// Gets the root path for all images - Everything must go in here.
      /// </summary>
      private string RootPath() {
 
         var path = Path.Combine(ConfigurationManager.AppSettings["ImagesPath"], "SOME USER SPECIFIC INFO");
 
         if (!Directory.Exists(path)) {
            Directory.CreateDirectory(path);
         }
 
         return path;
      }
 
      /// <summary>
      /// Checks if a specific path is allowed to be read or modified. Must exist in the root path for images, followed by organization and alias.
      /// </summary>
      private bool CanAccess(string path) {
         return path.StartsWith(RootPath(), StringComparison.OrdinalIgnoreCase);
      }
 
      /// <summary>
      /// Takes a request for a specific path and puts it in the correct root
      /// </summary>
      private string NormalizePath(string path) {
 
         if (string.IsNullOrWhiteSpace(path)) {
            return RootPath();
         }
 
         return Path.Combine(RootPath(), path);
      }
 
      /// <summary>
      /// Returns a list of files from a given path and filter. Taken directly from Telerik code.
      /// </summary>
      private IEnumerable<FileBrowserEntry> GetFiles(string path, string filter) {
 
         var directory = new DirectoryInfo(path);
 
         var extensions = (filter ?? "*").Split(new string[] { ", ", ",", "; ", ";" }, StringSplitOptions.RemoveEmptyEntries);
 
         return extensions.SelectMany(directory.GetFiles).Select(file => new FileBrowserEntry {
            Name = file.Name,
            Size = file.Length,
            EntryType = FileBrowserEntryType.File
         });
      }
 
      /// <summary>
      /// Returns a list of directories from a given path and filter. Taken directly from Telerik code.
      /// </summary>
      private IEnumerable<FileBrowserEntry> GetDirectories(string path) {
 
         var directory = new DirectoryInfo(path);
 
         return directory.GetDirectories().Select(subDirectory => new FileBrowserEntry {
            Name = subDirectory.Name,
            EntryType = FileBrowserEntryType.Directory
         });
      }
 
      /// <summary>
      /// Determins if a vile is valid based on the extension and allowed filter. Taken from Telerik code.
      /// </summary>
      private bool IsValidFile(string fileName) {
         var extension = Path.GetExtension(fileName);
         var allowedExtensions = Filter.Split(',');
 
         return allowedExtensions.Any(e => ((e.Equals("*.*")) || (e.EndsWith(extension, StringComparison.InvariantCultureIgnoreCase))));
      }
 
      /// <summary>
      /// Creates a thumbnail from a real path. Taken from Telerik code.
      /// </summary>
      private FileContentResult CreateThumbnail(string physicalPath) {
 
         using (var fileStream = System.IO.File.OpenRead(physicalPath)) {
            var desiredSize = new ImageSize {
               Width = ThumbnailWidth,
               Height = ThumbnailHeight
            };
 
            const string contentType = "image/png";
 
            return File(CreateThumbnail(fileStream, desiredSize, contentType), contentType);
         }
      }
 
      /// <summary>
      /// Scales an image to a desired size. Taken from Tekerik code.
      /// </summary>
      private void ScaleImage(Image source, Image destination) {
 
         using (var graphics = Graphics.FromImage(destination)) {
 
            graphics.CompositingMode = CompositingMode.SourceCopy;
            graphics.CompositingQuality = CompositingQuality.HighQuality;
            graphics.SmoothingMode = SmoothingMode.AntiAlias;
            graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
            graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
 
            graphics.DrawImage(source, 0, 0, destination.Width, destination.Height);
         }
      }
 
      /// <summary>
      /// Resizes an image to a desired size. Taken from Telerik code.
      /// </summary>
      private ImageSize Resize(ImageSize originalSize, ImageSize targetSize) {
 
         var aspectRatio = originalSize.Width / originalSize.Height;
 
         var width = targetSize.Width;
         var height = targetSize.Height;
 
         if ((originalSize.Width > targetSize.Width) || (originalSize.Height > targetSize.Height)) {
            if (aspectRatio > 1) {
               height = (targetSize.Height / aspectRatio);
            }
            else {
               width = (targetSize.Width * aspectRatio);
            }
         }
         else {
            width = originalSize.Width;
            height = originalSize.Height;
         }
 
         return new ImageSize {
            Width = Math.Max(width, 1),
            Height = Math.Max(height, 1)
         };
      }
 
      #endregion
 
      #region Authorization checks
 
      public bool AuthorizeCreateDirectory(string path, string name) {
         return CanAccess(path);
      }
 
      public bool AuthorizeDeleteDirectory(string path) {
         return CanAccess(path);
      }
 
      public bool AuthorizeDeleteFile(string path) {
         return CanAccess(path);
      }
 
      public bool AuthorizeRead(string path) {
         return CanAccess(path);
      }
 
      public bool AuthorizeThumbnail(string path) {
         return CanAccess(path);
      }
 
      public bool AuthorizeUpload(string path, HttpPostedFileBase file) {
         return ((CanAccess(path)) && (IsValidFile(file.FileName)));
      }
 
      #endregion
 
      #region ActionResult methods
 
      /// <summary>
      /// Uploads and saves a file
      /// </summary>
      public ActionResult Upload(string path, HttpPostedFileBase file) {
 
         path = NormalizePath(path);
 
         var fileName = Path.GetFileName(file.FileName);
 
         if (AuthorizeUpload(path, file)) {
            file.SaveAs(Path.Combine(path, fileName));
 
            return Json(new FileBrowserEntry {
               Size = file.ContentLength,
               Name = fileName
            }, "text/plain");
         }
 
         throw new HttpException((int)HttpStatusCode.Forbidden, HttpStatusCode.Forbidden.ToString());
      }
 
      /// <summary>
      /// Creates a new directory
      /// </summary>
      public ActionResult Create(string path, FileBrowserEntry entry) {
 
         path = NormalizePath(path);
         var name = entry.Name;
 
         if ((!string.IsNullOrWhiteSpace(name)) && (AuthorizeCreateDirectory(path, name))) {
            var physicalPath = Path.Combine(path, name);
 
            if (!Directory.Exists(physicalPath)) {
               Directory.CreateDirectory(physicalPath);
            }
 
            return Json(entry);
         }
 
         throw new HttpException((int)HttpStatusCode.Forbidden, HttpStatusCode.Forbidden.ToString());
      }
 
      /// <summary>
      /// Removes a directory or file
      /// </summary>
      [AcceptVerbs(HttpVerbs.Post)]
      public ActionResult Destroy(string path, FileBrowserEntry entry) {
 
         path = NormalizePath(path);
 
         if (entry != null) {
            path = Path.Combine(path, entry.Name);
 
            if (entry.EntryType == FileBrowserEntryType.File) {
               if (!AuthorizeDeleteFile(path)) {
                  throw new HttpException((int)HttpStatusCode.Forbidden, HttpStatusCode.Forbidden.ToString());
               }
 
               if (System.IO.File.Exists(path)) {
                  System.IO.File.Delete(path);
               }
            }
            else {
               if (!AuthorizeDeleteDirectory(path)) {
                  throw new HttpException((int)HttpStatusCode.Forbidden, HttpStatusCode.Forbidden.ToString());
               }
 
               if (Directory.Exists(path)) {
                  Directory.Delete(path, true);
               }
            }
 
            return Json(new object[0]);
         }
         throw new HttpException((int)HttpStatusCode.NotFound, "Not Found");
      }
 
      /// <summary>
      /// Gets a list of files and directories in the given path
      /// </summary>
      public JsonResult Read(string path) {
 
         path = NormalizePath(path);
 
         if (AuthorizeRead(path)) {
            try {
               var result = GetFiles(path, Filter).Concat(GetDirectories(path));
 
               return Json(result);
            }
            catch (DirectoryNotFoundException) {
               throw new HttpException((int)HttpStatusCode.NotFound, "Not Found");
            }
         }
 
         throw new HttpException((int)HttpStatusCode.Forbidden, HttpStatusCode.Forbidden.ToString());
      }
 
      /// <summary>
      /// Returns a thumbnail image
      /// </summary>
      [OutputCache(Duration = 3600, VaryByParam = "path")]
      public ActionResult Thumbnail(string path) {
 
         path = NormalizePath(path);
 
         if (AuthorizeThumbnail(path)) {
 
            if (System.IO.File.Exists(path)) {
               Response.AddFileDependency(path);
 
               return CreateThumbnail(path);
            }
            else {
               throw new HttpException((int)HttpStatusCode.NotFound, "Not Found");
            }
         }
         else {
            throw new HttpException((int)HttpStatusCode.Forbidden, HttpStatusCode.Forbidden.ToString());
         }
      }
 
      /// <summary>
      /// This is the actual image getter where the browser is trying to render the image
      /// </summary>
      [OutputCache(Duration = 3600, VaryByParam = "imageName")]
      public ActionResult GetImage(string imageName) {
 
         if ((!string.IsNullOrWhiteSpace(imageName)) && (IsValidFile(imageName))) {
            return new FilePathResult(Path.Combine(RootPath(), imageName), MimeMapping.GetMimeMapping(imageName));
         }
 
         throw new HttpException((int)HttpStatusCode.Forbidden, HttpStatusCode.Forbidden.ToString());
      }
 
      #endregion
 
      #region Public methods
 
      /// <summary>
      /// Creates a thumbnail image from a path and a desired size. Taken from Telerik code.
      /// </summary>
      public byte[] CreateThumbnail(Stream source, ImageSize desiredSize, string contentType) {
 
         using (var image = Image.FromStream(source)) {
            var originalSize = new ImageSize {
               Height = image.Height,
               Width = image.Width
            };
 
            var size = Resize(originalSize, desiredSize);
 
            using (var thumbnail = new Bitmap(size.Width, size.Height)) {
               ScaleImage(image, thumbnail);
 
               using (var memoryStream = new MemoryStream()) {
                  thumbnail.Save(memoryStream, ImageFormats[contentType]);
 
                  return memoryStream.ToArray();
               }
            }
         }
      }
 
      /// <summary>
      /// The allowed file types to upload
      /// </summary>
      public string Filter {
         get {
            return ALLOWED_TYPES;
         }
      }
 
      #endregion
   }
}

 

0
Paul
Top achievements
Rank 1
Iron
Iron
Veteran
answered on 17 Jul 2020, 07:41 PM

One last thing. I decided to tackle users who rename a *.exe or other file to *.jpg and then attempt to upload it to the server. There are better ways like byte scanning a few bytes in each file type to validate the file, but I will not see this method used often so the extra hit from attempting to load the image is not a problem for me. I replaced the Upload() method with this:

public ActionResult Upload(string path, HttpPostedFileBase file) {
 
   path = NormalizePath(path);
 
   var fileName = Path.GetFileName(file.FileName);
 
   if (AuthorizeUpload(path, file)) {
 
      // Validate this is a real image and not a hacker uploading something else
      try {
         using (var test = Image.FromStream(file.InputStream)) {
         }
         file.InputStream.Position = 0L;
      }
      catch {
         throw new HttpException((int)HttpStatusCode.UnsupportedMediaType, "Unsupported media type");
      }
 
      file.SaveAs(Path.Combine(path, fileName));
 
      return Json(new FileBrowserEntry {
         Size = file.ContentLength,
         Name = fileName
      }, "text/plain");
   }
 
   throw new HttpException((int)HttpStatusCode.Forbidden, HttpStatusCode.Forbidden.ToString());
}
0
Aleksandar
Telerik team
answered on 20 Jul 2020, 11:36 AM

Hello Paul,

Thank you for sharing your implementation for the scenario in question with the community. I am sure it will be beneficial to others with similar scenarios.

Regards,
Aleksandar
Progress Telerik

Tags
Editor
Asked by
Paul
Top achievements
Rank 1
Iron
Iron
Veteran
Answers by
Andeol
Top achievements
Rank 1
Paul
Top achievements
Rank 1
Iron
Iron
Veteran
Aleksandar
Telerik team
Share this question
or