using System; using System.Linq; using System.Text; using System.Web.Mvc; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Macros; using Umbraco.Web.Mvc; using umbraco; using umbraco.cms.businesslogic.macro; using System.Collections.Generic; using umbraco.cms.helpers; using Umbraco.Core; using Umbraco.Core.Configuration; using Template = umbraco.cms.businesslogic.template.Template; namespace Umbraco.Web.WebServices { /// /// A REST controller used to save files such as templates, partial views, macro files, etc... /// /// /// This isn't fully implemented yet but we should migrate all of the logic in the umbraco.presentation.webservices.codeEditorSave /// over to this controller. /// [ValidateMvcAngularAntiForgeryToken] public class SaveFileController : UmbracoAuthorizedController { /// /// Saves a partial view macro /// /// /// /// /// [HttpPost] public JsonResult SavePartialViewMacro(string filename, string oldName, string contents) { var svce = (FileService) Services.FileService; return SavePartialView(svce, filename, oldName, contents, "MacroPartials/", (s, n) => s.GetPartialViewMacro(n), (s, v) => s.ValidatePartialViewMacro((PartialView) v), (s, v) => s.SavePartialViewMacro(v)); } /// /// Saves a partial view /// /// /// /// /// [HttpPost] public JsonResult SavePartialView(string filename, string oldName, string contents) { var svce = (FileService) Services.FileService; return SavePartialView(svce, filename, oldName, contents, "Partials/", (s, n) => s.GetPartialView(n), (s, v) => s.ValidatePartialView((PartialView) v), (s, v) => s.SavePartialView(v)); } private JsonResult SavePartialView(IFileService svce, string filename, string oldname, string contents, string pathPrefix, Func get, Func validate, Func> save) { // sanitize input - partial view names have an extension filename = filename .Replace('\\', '/') .TrimStart('/'); // sharing the editor with partial views & partial view macros, // using path prefix to differenciate, // but the file service manages different filesystems, // and we need to come back to filesystem-relative paths // not sure why we still need this but not going to change it now if (filename.InvariantStartsWith(pathPrefix)) filename = filename.TrimStart(pathPrefix); if (oldname != null) { oldname = oldname.TrimStart('/', '\\'); if (oldname.InvariantStartsWith(pathPrefix)) oldname = oldname.TrimStart(pathPrefix); } var currentView = oldname.IsNullOrWhiteSpace() ? get(svce, filename) : get(svce, oldname); if (currentView == null) currentView = new PartialView(filename); else currentView.Path = filename; currentView.Content = contents; Attempt attempt; try { var partialView = currentView as PartialView; if (partialView != null && validate != null && validate(svce, partialView) == false) return Failed(ui.Text("speechBubbles", "partialViewErrorText"), ui.Text("speechBubbles", "partialViewErrorHeader"), new FileSecurityException("File '" + currentView.Path + "' is not a valid partial view file.")); attempt = save(svce, currentView); } catch (Exception e) { return Failed(ui.Text("speechBubbles", "partialViewErrorText"), ui.Text("speechBubbles", "partialViewErrorHeader"), e); } if (attempt.Success == false) { return Failed(ui.Text("speechBubbles", "partialViewErrorText"), ui.Text("speechBubbles", "partialViewErrorHeader"), attempt.Exception); } return Success(ui.Text("speechBubbles", "partialViewSavedText"), ui.Text("speechBubbles", "partialViewSavedHeader")); } /// /// Saves a template /// /// /// /// /// /// /// [HttpPost] public JsonResult SaveTemplate(string templateName, string templateAlias, string templateContents, int templateId, int masterTemplateId) { //TODO: Change this over to use the new API - Also this will be migrated to a TemplateEditor or ViewEditor when it's all moved to angular Template t; bool pathChanged = false; try { t = new Template(templateId) { Text = templateName.CleanForXss('[', ']', '(', ')', ':'), Alias = templateAlias.CleanForXss('[', ']', '(', ')', ':'), Design = templateContents }; //check if the master page has changed - we need to normalize both - if it's 0 or -1, then make it 0... this is easy // to do with Math.Max if (Math.Max(t.MasterTemplate, 0) != Math.Max(masterTemplateId, 0)) { t.MasterTemplate = Math.Max(masterTemplateId, 0); pathChanged = true; } } catch (ArgumentException ex) { //the template does not exist return Failed("Template does not exist", ui.Text("speechBubbles", "templateErrorHeader"), ex); } try { t.Save(); //ensure the correct path is synced as the parent might have been changed // http://issues.umbraco.org/issue/U4-2300 if (pathChanged) { //need to re-look it up t = new Template(templateId); } var syncPath = "-1,init," + t.Path.Replace("-1,", ""); return Success(ui.Text("speechBubbles", "templateSavedText"), ui.Text("speechBubbles", "templateSavedHeader"), new { path = syncPath, contents = t.Design }); } catch (Exception ex) { return Failed(ui.Text("speechBubbles", "templateErrorText"), ui.Text("speechBubbles", "templateErrorHeader"), ex); } } [HttpPost] public JsonResult SaveScript(string filename, string oldName, string contents) { // sanitize input - script names have an extension filename = filename .Replace('\\', '/') .TrimStart('/'); var svce = (FileService) Services.FileService; var script = svce.GetScriptByName(oldName); if (script == null) script = new Script(filename); else script.Path = filename; script.Content = contents; try { if (svce.ValidateScript(script) == false) return Failed(ui.Text("speechBubbles", "scriptErrorText"), ui.Text("speechBubbles", "scriptErrorHeader"), new FileSecurityException("File '" + filename + "' is not a valid script file.")); svce.SaveScript(script); } catch (Exception e) { return Failed(ui.Text("speechBubbles", "scriptErrorText"), ui.Text("speechBubbles", "scriptErrorHeader"), e); } return Success(ui.Text("speechBubbles", "scriptSavedText"), ui.Text("speechBubbles", "scriptSavedHeader"), new { path = DeepLink.GetTreePathFromFilePath(script.Path), name = script.Path, url = script.VirtualPath, contents = script.Content }); } [HttpPost] public JsonResult SaveStylesheet(string filename, string oldName, string contents) { // sanitize input - stylesheet names have no extension filename = filename .Replace('\\', '/') .TrimStart('/') .EnsureEndsWith(".css"); var svce = (FileService) Services.FileService; var stylesheet = svce.GetStylesheetByName(oldName); if (stylesheet == null) stylesheet = new Stylesheet(filename); else stylesheet.Path = filename; stylesheet.Content = contents; try { if (svce.ValidateStylesheet(stylesheet) == false) return Failed(ui.Text("speechBubbles", "cssErrorText"), ui.Text("speechBubbles", "cssErrorHeader"), new FileSecurityException("File '" + filename + "' is not a valid stylesheet file.")); svce.SaveStylesheet(stylesheet); } catch (Exception e) { return Failed(ui.Text("speechBubbles", "cssErrorText"), ui.Text("speechBubbles", "cssErrorHeader"), e); } return Success(ui.Text("speechBubbles", "cssSavedText"), ui.Text("speechBubbles", "cssSavedHeader"), new { path = DeepLink.GetTreePathFromFilePath(stylesheet.Path), name = stylesheet.Path, url = stylesheet.VirtualPath, contents = stylesheet.Content }); } /// /// Returns a successful message /// /// The message to display in the speach bubble /// The header to display in the speach bubble /// /// private JsonResult Success(string message, string header, object additionalVals = null) { var d = additionalVals == null ? new Dictionary() : additionalVals.ToDictionary(); d["success"] = true; d["message"] = message; d["header"] = header; return Json(d); } /// /// Returns a failed message /// /// The message to display in the speach bubble /// The header to display in the speach bubble /// The exception if there was one /// private JsonResult Failed(string message, string header, Exception exception = null) { if (exception != null) LogHelper.Error("An error occurred saving a file. " + message, exception); return Json(new { success = false, header = header, message = message + (exception == null ? "" : (exception.Message + ". Check log for details.")) }); } } }