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
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
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.
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
}
}
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());
}