diff --git a/src/Umbraco.Web/Editors/CodeFileController.cs b/src/Umbraco.Web/Editors/CodeFileController.cs index 6ee7aafb71..6a19ad06e8 100644 --- a/src/Umbraco.Web/Editors/CodeFileController.cs +++ b/src/Umbraco.Web/Editors/CodeFileController.cs @@ -1,4 +1,5 @@ -using AutoMapper; +using System; +using AutoMapper; using System.Collections.Generic; using System.IO; using System.Linq; @@ -18,9 +19,11 @@ using Umbraco.Web.Trees; namespace Umbraco.Web.Editors { + //TODO: Put some exception filters in our webapi to return 404 instead of 500 when we throw ArgumentNullException + // ref: https://www.exceptionnotfound.net/the-asp-net-web-api-exception-handling-pipeline-a-guided-tour/ [PluginController("UmbracoApi")] [PrefixlessBodyModelValidator] - [UmbracoApplicationAuthorizeAttribute(Core.Constants.Applications.Settings)] + [UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)] public class CodeFileController : BackOfficeNotificationsController { @@ -33,6 +36,9 @@ namespace Umbraco.Web.Editors [ValidationFilter] public HttpResponseMessage PostCreate(string type, CodeFileDisplay display) { + if (display == null) throw new ArgumentNullException("display"); + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); + switch (type) { case Core.Constants.Trees.PartialViews: @@ -67,10 +73,9 @@ namespace Umbraco.Web.Editors [HttpPost] public CodeFileDisplay PostCreateContainer(string type, string parentId, string name) { - if (string.IsNullOrWhiteSpace(type) || string.IsNullOrWhiteSpace(name)) - { - throw new HttpResponseException(HttpStatusCode.BadRequest); - } + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); + if (string.IsNullOrWhiteSpace(parentId)) throw new ArgumentException("Value cannot be null or whitespace.", "parentId"); + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); // if the parentId is root (-1) then we just need an empty string as we are // creating the path below and we don't wan't -1 in the path @@ -120,10 +125,8 @@ namespace Umbraco.Web.Editors /// The file and its contents from the virtualPath public CodeFileDisplay GetByPath(string type, string virtualPath) { - if (string.IsNullOrWhiteSpace(type) || string.IsNullOrWhiteSpace(virtualPath)) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); + if (string.IsNullOrWhiteSpace(virtualPath)) throw new ArgumentException("Value cannot be null or whitespace.", "virtualPath"); virtualPath = System.Web.HttpUtility.UrlDecode(virtualPath); @@ -139,7 +142,7 @@ namespace Umbraco.Web.Editors display.Id = System.Web.HttpUtility.UrlEncode(view.Path); return display; } - return null; + throw new HttpResponseException(HttpStatusCode.NotFound); case Core.Constants.Trees.PartialViewMacros: var viewMacro = Services.FileService.GetPartialViewMacro(virtualPath); @@ -151,7 +154,7 @@ namespace Umbraco.Web.Editors display.Id = System.Web.HttpUtility.UrlEncode(viewMacro.Path); return display; } - return null; + throw new HttpResponseException(HttpStatusCode.NotFound); case Core.Constants.Trees.Scripts: var script = Services.FileService.GetScriptByName(virtualPath); @@ -163,7 +166,7 @@ namespace Umbraco.Web.Editors display.Id = System.Web.HttpUtility.UrlEncode(script.Path); return display; } - return null; + throw new HttpResponseException(HttpStatusCode.NotFound); } throw new HttpResponseException(HttpStatusCode.NotFound); @@ -176,10 +179,7 @@ namespace Umbraco.Web.Editors /// Returns a list of if a correct type is sent public IEnumerable GetSnippets(string type) { - if (string.IsNullOrWhiteSpace(type)) - { - throw new HttpResponseException(HttpStatusCode.BadRequest); - } + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); IEnumerable snippets; switch (type) @@ -209,15 +209,10 @@ namespace Umbraco.Web.Editors /// /// /// - public CodeFileDisplay GetScaffold(string type, string id = null, string snippetName = null) + public CodeFileDisplay GetScaffold(string type, string id, string snippetName = null) { - if (string.IsNullOrWhiteSpace(type)) - { - throw new HttpResponseException(HttpStatusCode.BadRequest); - } - - if (id.IsNullOrWhiteSpace()) - id = string.Empty; + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Value cannot be null or whitespace.", "id"); CodeFileDisplay codeFileDisplay; @@ -246,15 +241,15 @@ namespace Umbraco.Web.Editors // Make sure that the root virtual path ends with '/' codeFileDisplay.VirtualPath = codeFileDisplay.VirtualPath.EnsureEndsWith("/"); - if (id.IsNullOrWhiteSpace() == false && id != Core.Constants.System.Root.ToInvariantString()) + if (id != Core.Constants.System.Root.ToInvariantString()) { codeFileDisplay.VirtualPath += id.TrimStart("/").EnsureEndsWith("/"); + //if it's not new then it will have a path, otherwise it won't + codeFileDisplay.Path = Url.GetTreePathFromFilePath(id); } - codeFileDisplay.VirtualPath = codeFileDisplay.VirtualPath.TrimStart("~"); - codeFileDisplay.Path = Url.GetTreePathFromFilePath(id); + codeFileDisplay.VirtualPath = codeFileDisplay.VirtualPath.TrimStart("~"); codeFileDisplay.FileType = type; - return codeFileDisplay; } @@ -268,52 +263,52 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage Delete(string type, string virtualPath) { - if (string.IsNullOrWhiteSpace(type) == false && string.IsNullOrWhiteSpace(virtualPath) == false) + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); + if (string.IsNullOrWhiteSpace(virtualPath)) throw new ArgumentException("Value cannot be null or whitespace.", "virtualPath"); + + virtualPath = System.Web.HttpUtility.UrlDecode(virtualPath); + + switch (type) { - virtualPath = System.Web.HttpUtility.UrlDecode(virtualPath); + case Core.Constants.Trees.PartialViews: + if (IsDirectory(virtualPath, SystemDirectories.PartialViews)) + { + Services.FileService.DeletePartialViewFolder(virtualPath); + return Request.CreateResponse(HttpStatusCode.OK); + } + if (Services.FileService.DeletePartialView(virtualPath, Security.CurrentUser.Id)) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Partial View or folder found with the specified path"); - switch (type) - { - case Core.Constants.Trees.PartialViews: - if (IsDirectory(virtualPath, SystemDirectories.PartialViews)) - { - Services.FileService.DeletePartialViewFolder(virtualPath); - return Request.CreateResponse(HttpStatusCode.OK); - } - if (Services.FileService.DeletePartialView(virtualPath, Security.CurrentUser.Id)) - { - return Request.CreateResponse(HttpStatusCode.OK); - } - return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Partial View or folder found with the specified path"); + case Core.Constants.Trees.PartialViewMacros: + if (IsDirectory(virtualPath, SystemDirectories.MacroPartials)) + { + Services.FileService.DeletePartialViewMacroFolder(virtualPath); + return Request.CreateResponse(HttpStatusCode.OK); + } + if (Services.FileService.DeletePartialViewMacro(virtualPath, Security.CurrentUser.Id)) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Partial View Macro or folder found with the specified path"); - case Core.Constants.Trees.PartialViewMacros: - if (IsDirectory(virtualPath, SystemDirectories.MacroPartials)) - { - Services.FileService.DeletePartialViewMacroFolder(virtualPath); - return Request.CreateResponse(HttpStatusCode.OK); - } - if (Services.FileService.DeletePartialViewMacro(virtualPath, Security.CurrentUser.Id)) - { - return Request.CreateResponse(HttpStatusCode.OK); - } - return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Partial View Macro or folder found with the specified path"); + case Core.Constants.Trees.Scripts: + if (IsDirectory(virtualPath, SystemDirectories.Scripts)) + { + Services.FileService.DeleteScriptFolder(virtualPath); + return Request.CreateResponse(HttpStatusCode.OK); + } + if (Services.FileService.GetScriptByName(virtualPath) != null) + { + Services.FileService.DeleteScript(virtualPath, Security.CurrentUser.Id); + return Request.CreateResponse(HttpStatusCode.OK); + } + return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Script or folder found with the specified path"); - case Core.Constants.Trees.Scripts: - if (IsDirectory(virtualPath, SystemDirectories.Scripts)) - { - Services.FileService.DeleteScriptFolder(virtualPath); - return Request.CreateResponse(HttpStatusCode.OK); - } - if (Services.FileService.GetScriptByName(virtualPath) != null) - { - Services.FileService.DeleteScript(virtualPath, Security.CurrentUser.Id); - return Request.CreateResponse(HttpStatusCode.OK); - } - return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Script or folder found with the specified path"); - - default: - return Request.CreateResponse(HttpStatusCode.NotFound); - } + default: + return Request.CreateResponse(HttpStatusCode.NotFound); } throw new HttpResponseException(HttpStatusCode.NotFound); @@ -326,16 +321,13 @@ namespace Umbraco.Web.Editors /// The updated CodeFileDisplay model public CodeFileDisplay PostSave(CodeFileDisplay display) { + if (display == null) throw new ArgumentNullException("display"); + if (ModelState.IsValid == false) { throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); } - if (display == null || string.IsNullOrWhiteSpace(display.FileType)) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - switch (display.FileType) { case Core.Constants.Trees.PartialViews: @@ -370,25 +362,21 @@ namespace Umbraco.Web.Editors case Core.Constants.Trees.Scripts: - var virtualPath = display.VirtualPath ?? string.Empty; - - // this is all weird, should be using relative paths everywhere! - var relPath = FileSystemProviderManager.Current.ScriptsFileSystem.GetRelativePath(virtualPath); - - var path = relPath; - if (path.EndsWith(".js") == false) - path += ".js"; - - var script = Services.FileService.GetScriptByName(path) ?? new Script(path); - script.Path = path; - script.Content = display.Content; - Services.FileService.SaveScript(script); - - display = Mapper.Map(script, display); - display.Path = Url.GetTreePathFromFilePath(script.Path); - display.Id = System.Web.HttpUtility.UrlEncode(script.Path); + var scriptResult = CreateOrUpdateScript(display); + display = Mapper.Map(scriptResult, display); + display.Path = Url.GetTreePathFromFilePath(scriptResult.Path); + display.Id = System.Web.HttpUtility.UrlEncode(scriptResult.Path); return display; + //display.AddErrorNotification( + // Services.TextService.Localize("speechBubbles/partialViewErrorHeader"), + // Services.TextService.Localize("speechBubbles/partialViewErrorText")); + + break; + + + + default: throw new HttpResponseException(HttpStatusCode.NotFound); } @@ -396,11 +384,86 @@ namespace Umbraco.Web.Editors return display; } + /// + /// Create or Update a Script + /// + /// + /// + /// + /// It's important to note that Scripts are DIFFERENT from cshtml files since scripts use IFileSystem and cshtml files + /// use a normal file system because they must exist on a real file system for ASP.NET to work. + /// + private Script CreateOrUpdateScript(CodeFileDisplay display) + { + //must always end with the correct extension + display.Name = EnsureCorrectFileExtension(display.Name, ".js"); + + var virtualPath = display.VirtualPath ?? string.Empty; + // this is all weird, should be using relative paths everywhere! + var relPath = FileSystemProviderManager.Current.ScriptsFileSystem.GetRelativePath(virtualPath); + + if (relPath.EndsWith(".js") == false) + { + //this would typically mean it's new + relPath = relPath.IsNullOrWhiteSpace() + ? relPath + display.Name + : relPath.EnsureEndsWith('/') + display.Name; + } + + var script = Services.FileService.GetScriptByName(relPath); + if (script != null) + { + // might need to find the path + var orgPath = script.OriginalPath.Substring(0, script.OriginalPath.IndexOf(script.Name)); + script.Path = orgPath + display.Name; + + script.Content = display.Content; + //try/catch? since this doesn't return an Attempt? + Services.FileService.SaveScript(script, Security.CurrentUser.Id); + } + else + { + script = new Script(relPath); + script.Content = display.Content; + Services.FileService.SaveScript(script, Security.CurrentUser.Id); + } + + return script; + } + private Attempt CreateOrUpdatePartialView(CodeFileDisplay display) { + return CreateOrUpdatePartialView(display, SystemDirectories.MacroPartials, + Services.FileService.GetPartialView, Services.FileService.SavePartialView, Services.FileService.CreatePartialView); + } + + private Attempt CreateOrUpdatePartialViewMacro(CodeFileDisplay display) + { + return CreateOrUpdatePartialView(display, SystemDirectories.MacroPartials, + Services.FileService.GetPartialViewMacro, Services.FileService.SavePartialViewMacro, Services.FileService.CreatePartialViewMacro); + } + + /// + /// Helper method to take care of persisting partial views or partial view macros - so we're not duplicating the same logic + /// + /// + /// + /// + /// + /// + /// + private Attempt CreateOrUpdatePartialView( + CodeFileDisplay display, string systemDirectory, + Func getView, + Func> saveView, + Func> createView) + { + //must always end with the correct extension + display.Name = EnsureCorrectFileExtension(display.Name, ".cshtml"); + Attempt partialViewResult; - string virtualPath = NormalizeVirtualPath(display.VirtualPath, SystemDirectories.PartialViews); - var view = Services.FileService.GetPartialView(virtualPath); + var virtualPath = NormalizeVirtualPath(display.VirtualPath, systemDirectory); + var view = getView(virtualPath); if (view != null) { // might need to find the path @@ -408,14 +471,13 @@ namespace Umbraco.Web.Editors view.Path = orgPath + display.Name; view.Content = display.Content; - partialViewResult = Services.FileService.SavePartialView(view, Security.CurrentUser.Id); + partialViewResult = saveView(view, Security.CurrentUser.Id); } else { - var fileName = EnsurePartialViewExtension(display.Name, ".cshtml"); - view = new PartialView(virtualPath + fileName); + view = new PartialView(virtualPath + display.Name); view.Content = display.Content; - partialViewResult = Services.FileService.CreatePartialView(view, display.Snippet, Security.CurrentUser.Id); + partialViewResult = createView(view, display.Snippet, Security.CurrentUser.Id); } return partialViewResult; @@ -435,29 +497,7 @@ namespace Umbraco.Web.Editors return virtualPath; } - private Attempt CreateOrUpdatePartialViewMacro(CodeFileDisplay display) - { - Attempt partialViewMacroResult; - var virtualPath = display.VirtualPath ?? string.Empty; - var viewMacro = Services.FileService.GetPartialViewMacro(virtualPath); - if (viewMacro != null) - { - viewMacro.Content = display.Content; - viewMacro.Path = display.Name; - partialViewMacroResult = Services.FileService.SavePartialViewMacro(viewMacro, Security.CurrentUser.Id); - } - else - { - var fileName = EnsurePartialViewExtension(display.Name, ".cshtml"); - viewMacro = new PartialView(virtualPath + fileName); - viewMacro.Content = display.Content; - partialViewMacroResult = Services.FileService.CreatePartialViewMacro(viewMacro, display.Snippet, Security.CurrentUser.Id); - } - - return partialViewMacroResult; - } - - private string EnsurePartialViewExtension(string value, string extension) + private string EnsureCorrectFileExtension(string value, string extension) { if (value.EndsWith(extension) == false) value += extension;