diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 3b2eebb79c..be87bc21b8 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -666,6 +666,7 @@ + diff --git a/src/Umbraco.Core/UrlHelperExtensions.cs b/src/Umbraco.Core/UrlHelperExtensions.cs new file mode 100644 index 0000000000..9755dc24cc --- /dev/null +++ b/src/Umbraco.Core/UrlHelperExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Web.Mvc; +using Umbraco.Core.Configuration; + +namespace Umbraco.Core +{ + /// + /// Extension methods for UrlHelper + /// + public static class UrlHelperExtensions + { + /// + /// Returns the base path (not including the 'action') of the MVC controller "SaveFileController" + /// + /// + /// + public static string GetSaveFileServicePath(this UrlHelper url) + { + var result = url.Action("SavePartialView", "SaveFile", new {area = GlobalSettings.UmbracoMvcArea}); + return result.TrimEnd("SavePartialView"); + } + + } +} diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 6796e19173..cbdb999bb6 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -780,7 +780,11 @@ To manage your website, simply open the umbraco back office and start adding con Xslt could not be saved, check file permissions Xslt saved No errors in xslt - Content unpublished + Content unpublished + Partial view saved + Partial view saved without any errors! + Partial view not saved + An error occurred saving the file. Uses CSS syntax ex: h1, .redHeader, .blueTex diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index e024bd6197..dc593fec55 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -779,6 +779,10 @@ To manage your website, simply open the umbraco back office and start adding con Xslt could not be saved, check file permissions Xslt saved No errors in xslt + Partial view saved + Partial view saved without any errors! + Partial view not saved + An error occurred saving the file. Uses CSS syntax ex: h1, .redHeader, .blueTex diff --git a/src/Umbraco.Web.UI/umbraco/settings/views/EditView.aspx b/src/Umbraco.Web.UI/umbraco/settings/views/EditView.aspx index 91b892738a..e07c2d48d4 100644 --- a/src/Umbraco.Web.UI/umbraco/settings/views/EditView.aspx +++ b/src/Umbraco.Web.UI/umbraco/settings/views/EditView.aspx @@ -2,6 +2,7 @@ CodeBehind="EditView.aspx.cs" Inherits="Umbraco.Web.UI.Umbraco.Settings.Views.EditView" ValidateRequest="False" %> +<%@ Import Namespace="Umbraco.Core" %> <%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %> <%@ Register TagPrefix="umb" Namespace="ClientDependency.Core.Controls" Assembly="ClientDependency.Core" %> @@ -16,17 +17,12 @@ var editView = new Umbraco.Editors.EditView({ editorType: "<%= EditorType.ToString() %>", originalFileName: "<%=OriginalFileName %>", + restServiceLocation: "<%= Url.GetSaveFileServicePath() %>", masterPageDropDown: $("#<%= MasterTemplate.ClientID %>"), nameTxtBox: $("#<%= NameTxt.ClientID %>"), aliasTxtBox: $("#<%= AliasTxt.ClientID %>"), saveButton: $("#<%= ((Control)SaveButton).ClientID %>"), - templateId: '<%= Request.QueryString["templateID"] %>', - msgs: { - templateErrorHeader: "<%= umbraco.ui.Text("speechBubbles", "templateErrorHeader") %>", - templateErrorText: "<%= umbraco.ui.Text("speechBubbles", "templateErrorText") %>", - templateSavedHeader: "<%= umbraco.ui.Text("speechBubbles", "templateSavedHeader") %>", - templateSavedText: "<%= umbraco.ui.Text("speechBubbles", "templateSavedText") %>" - } + templateId: '<%= Request.QueryString["templateID"] %>' }); //initialize it. editView.init(); diff --git a/src/Umbraco.Web.UI/umbraco_client/Editors/EditView.js b/src/Umbraco.Web.UI/umbraco_client/Editors/EditView.js index a76d59d948..e1b29cd201 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Editors/EditView.js +++ b/src/Umbraco.Web.UI/umbraco_client/Editors/EditView.js @@ -2,8 +2,9 @@ (function ($) { - Umbraco.Editors.EditView = base2.Base.extend({ + /// Defines the EditView class to controll the persisting of the view file and UI interaction + //private methods/variables _opts: null, @@ -36,39 +37,48 @@ if (this._opts.editorType == "Template") { //saving a template view - - umbraco.presentation.webservices.codeEditorSave.SaveTemplate( - this._opts.nameTxtBox.val(), - this._opts.aliasTxtBox.val(), - codeVal, - this._opts.templateId, - this._opts.masterPageDropDown.val(), - function(t) { self.submitSuccess(t); }, - function(t) { self.submitFailure(t); }); + + $.post(self._opts.restServiceLocation + "SaveTemplate", + JSON.stringify({ + templateName: this._opts.nameTxtBox.val(), + templateAlias: this._opts.aliasTxtBox.val(), + templateContents: codeVal, + templateId: this._opts.templateId, + masterTemplateId: this._opts.masterPageDropDown.val() + }), + function(e) { + if (e.success) { + self.submitSuccess(e.message, e.header); + } else { + self.submitFailure(e.message, e.header); + } + }); } else { //saving a partial view - - umbraco.presentation.webservices.codeEditorSave.SavePartialView( - this._opts.nameTxtBox.val(), - this._opts.originalFileName, - codeVal, - function (t) { self.submitSuccess(t); }, - function (t) { self.submitFailure(t); }); + + $.post(self._opts.restServiceLocation + "SavePartialView", + JSON.stringify({ + filename: this._opts.nameTxtBox.val(), + oldName: this._opts.originalFileName, + contents: codeVal + }), + function(e) { + if (e.success) { + self.submitSuccess(e.message, e.header); + } else { + self.submitFailure(e.message, e.header); + } + }); } }, - submitSuccess: function (t) { - if (t != 'true') { - top.UmbSpeechBubble.ShowMessage('error', this._opts.msgs.templateErrorHeader, this._opts.msgs.templateErrorText); - } - else { - top.UmbSpeechBubble.ShowMessage('save', this._opts.msgs.templateSavedHeader, this._opts.msgs.templateSavedText); - } + submitSuccess: function (err, header) { + top.UmbSpeechBubble.ShowMessage('save', header, err); }, - submitFailure: function (t) { - top.UmbSpeechBubble.ShowMessage('error', this._opts.msgs.templateErrorHeader, this._opts.msgs.templateErrorText); + submitFailure: function (err, header) { + top.UmbSpeechBubble.ShowMessage('error', header, err); }, changeMasterPageFile: function ( ) { @@ -102,5 +112,11 @@ }); + //Set defaults for jQuery ajax calls. + $.ajaxSetup({ + dataType: 'json', + cache: false, + contentType: 'application/json; charset=utf-8' + }); })(jQuery); \ No newline at end of file diff --git a/src/Umbraco.Web/Install/UmbracoInstallAuthorizeAttribute.cs b/src/Umbraco.Web/Install/UmbracoInstallAuthorizeAttribute.cs index a990088fdf..75360a1bba 100644 --- a/src/Umbraco.Web/Install/UmbracoInstallAuthorizeAttribute.cs +++ b/src/Umbraco.Web/Install/UmbracoInstallAuthorizeAttribute.cs @@ -27,7 +27,7 @@ namespace Umbraco.Web.Install } /// - /// Ensures that the user must be in the Administrator or the Install role + /// Ensures that the user must be logged in or that the application is not configured just yet. /// /// /// diff --git a/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs b/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs new file mode 100644 index 0000000000..5d25dd69de --- /dev/null +++ b/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs @@ -0,0 +1,83 @@ +using System; +using System.Web; +using System.Web.Mvc; +using Umbraco.Core; +using umbraco.BasePages; + +namespace Umbraco.Web.Mvc +{ + /// + /// Ensures authorization occurs for the installer if it has already completed. If install has not yet occured + /// then the authorization is successful + /// + internal class UmbracoAuthorizeAttribute : AuthorizeAttribute + { + private readonly ApplicationContext _applicationContext; + + public UmbracoAuthorizeAttribute(ApplicationContext appContext) + { + if (appContext == null) throw new ArgumentNullException("appContext"); + _applicationContext = appContext; + } + + public UmbracoAuthorizeAttribute() + : this(ApplicationContext.Current) + { + + } + + /// + /// Ensures that the user must be in the Administrator or the Install role + /// + /// + /// + protected override bool AuthorizeCore(HttpContextBase httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException("httpContext"); + } + + try + { + //we need to that the app is configured and that a user is logged in + if (!_applicationContext.IsConfigured) + return false; + var isLoggedIn = BasePage.ValidateUserContextID(BasePage.umbracoUserContextID); + return isLoggedIn; + } + catch (Exception) + { + return false; + } + } + + /// + /// Override the OnAuthorization so that we can return a custom response. + /// + /// + public override void OnAuthorization(AuthorizationContext filterContext) + { + Mandate.ParameterNotNull(filterContext, "filterContext"); + if (OutputCacheAttribute.IsChildActionCacheActive(filterContext)) + throw new InvalidOperationException("Cannot use " + typeof(UmbracoAuthorizeAttribute).FullName + " on a child action"); + if (AuthorizeCore(filterContext.HttpContext)) + { + //with a little help from dotPeek... this is what it normally would do + var cache = filterContext.HttpContext.Response.Cache; + cache.SetProxyMaxAge(new TimeSpan(0L)); + cache.AddValidationCallback(CacheValidateHandler, null); + } + else + { + //they aren't authorized + throw new HttpException((int)global::System.Net.HttpStatusCode.Unauthorized, "You must login to view this resource."); + } + } + + private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus) + { + validationStatus = OnCacheAuthorization(new HttpContextWrapper(context)); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/UmbracoAuthorizedController.cs b/src/Umbraco.Web/Mvc/UmbracoAuthorizedController.cs new file mode 100644 index 0000000000..044109ee12 --- /dev/null +++ b/src/Umbraco.Web/Mvc/UmbracoAuthorizedController.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Web.Mvc; + +namespace Umbraco.Web.Mvc +{ + /// + /// A base MVC controller for use in the back office that ensures that every call to it authorizes the current user. + /// + /// + /// This controller essentially just uses a global UmbracoAuthorizeAttribute, inheritors that require more granular control over the + /// authorization of each method can use this attribute instead of inheriting from this controller. + /// + [UmbracoAuthorize] + public abstract class UmbracoAuthorizedController : Controller + { + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index bf6d842b2d..842ea217b9 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -280,6 +280,8 @@ + + @@ -1845,6 +1847,7 @@ Reference.map + diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 28e6dad464..58b62c6425 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -32,6 +32,7 @@ namespace Umbraco.Web public WebBootManager(UmbracoApplication umbracoApplication) : this(umbracoApplication, false) { + } /// @@ -158,6 +159,16 @@ namespace Umbraco.Web ); installPackageRoute.DataTokens.Add("area", umbracoPath); + //Create the REST/web/script service routes + var webServiceRoutes = RouteTable.Routes.MapRoute( + "Umbraco_web_services", + "Umbraco/RestServices/{controller}/{action}/{id}", + new {controller = "SaveFileController", action = "Index", id = UrlParameter.Optional}, + //VERY IMPORTANT! for this route, only match controllers in this namespace! + new string[] { "Umbraco.Web.WebServices" } + ); + webServiceRoutes.DataTokens.Add("area", umbracoPath); + //we need to find the surface controllers and route them var surfaceControllers = SurfaceControllerResolver.Current.RegisteredSurfaceControllers.ToArray(); diff --git a/src/Umbraco.Web/WebServices/SaveFileController.cs b/src/Umbraco.Web/WebServices/SaveFileController.cs new file mode 100644 index 0000000000..e5bac84d06 --- /dev/null +++ b/src/Umbraco.Web/WebServices/SaveFileController.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Web.Mvc; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Web.Mvc; +using umbraco; +using umbraco.BasePages; +using umbraco.cms.businesslogic.template; +using umbraco.presentation.cache; + +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. + /// + public class SaveFileController : UmbracoAuthorizedController + { + + /// + /// Saves a partial view for a partial view macr + /// + /// + /// + /// + /// + [HttpPost] + public JsonResult SavePartialView(string filename, string oldName, string contents) + { + var folderPath = SystemDirectories.MvcViews + "/Partials/"; + + // validate file + IOHelper.ValidateEditPath(IOHelper.MapPath(folderPath + filename), folderPath); + // validate extension + IOHelper.ValidateFileExtension(IOHelper.MapPath(folderPath + filename), new[] { "cshtml" }.ToList()); + + var val = contents; + var saveOldPath = oldName.StartsWith("~/") ? IOHelper.MapPath(oldName) : IOHelper.MapPath(folderPath + oldName); + var savePath = filename.StartsWith("~/") ? IOHelper.MapPath(filename) : IOHelper.MapPath(folderPath + filename); + + //Directory check.. only allow files in script dir and below to be edited + if (!savePath.StartsWith(IOHelper.MapPath(folderPath))) + { + return Failed( + ui.Text("speechBubbles", "partialViewErrorText"), ui.Text("speechBubbles", "partialViewErrorHeader"), + //pass in a new exception ... this will also append the the message + new ArgumentException("Illegal path: " + savePath)); + } + + //deletes the old file + if (savePath != saveOldPath) + { + if (System.IO.File.Exists(saveOldPath)) + System.IO.File.Delete(saveOldPath); + } + using (var sw = System.IO.File.CreateText(savePath)) + { + sw.Write(val); + } + 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) + { + Template t; + try + { + t = new Template(templateId) + { + Text = templateName, + Alias = templateAlias, + MasterTemplate = masterTemplateId, + Design = templateContents + }; + } + catch (ArgumentException ex) + { + //the template does not exist + return Failed("Template does not exist", ui.Text("speechBubbles", "templateErrorHeader"), ex); + } + + try + { + t.Save(); + + // Clear cache in rutime + if (UmbracoSettings.UseDistributedCalls) + dispatcher.Refresh(new Guid("dd12b6a0-14b9-46e8-8800-c154f74047c8"), t.Id); + else + template.ClearCachedTemplate(t.Id); + + return Success(ui.Text("speechBubbles", "templateSavedText"), ui.Text("speechBubbles", "templateSavedHeader")); + } + catch (Exception ex) + { + return Failed(ui.Text("speechBubbles", "templateErrorText"), ui.Text("speechBubbles", "templateErrorHeader"), ex); + } + } + + /// + /// 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) + { + return Json(new + { + success = true, + message = message, + header = header + }); + } + + /// + /// 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.")) + }); + } + + } +} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/codeEditorSave.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/codeEditorSave.asmx.cs index 1802307a20..9d34946b21 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/codeEditorSave.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/codeEditorSave.asmx.cs @@ -338,48 +338,48 @@ namespace umbraco.presentation.webservices return "false"; } - [WebMethod] - public string SavePartialView(string filename, string oldName, string contents) - { - if (BasePage.ValidateUserContextID(BasePage.umbracoUserContextID)) - { - var folderPath = SystemDirectories.MvcViews + "/Partials/"; + //[WebMethod] + //public string SavePartialView(string filename, string oldName, string contents) + //{ + // if (BasePage.ValidateUserContextID(BasePage.umbracoUserContextID)) + // { + // var folderPath = SystemDirectories.MvcViews + "/Partials/"; - // validate file - IOHelper.ValidateEditPath(IOHelper.MapPath(folderPath + filename), folderPath); - // validate extension - IOHelper.ValidateFileExtension(IOHelper.MapPath(folderPath + filename), new[] {"cshtml"}.ToList()); + // // validate file + // IOHelper.ValidateEditPath(IOHelper.MapPath(folderPath + filename), folderPath); + // // validate extension + // IOHelper.ValidateFileExtension(IOHelper.MapPath(folderPath + filename), new[] {"cshtml"}.ToList()); - var val = contents; - string returnValue; - var saveOldPath = oldName.StartsWith("~/") ? IOHelper.MapPath(oldName) : IOHelper.MapPath(folderPath + oldName); - var savePath = filename.StartsWith("~/") ? IOHelper.MapPath(filename) : IOHelper.MapPath(folderPath + filename); + // var val = contents; + // string returnValue; + // var saveOldPath = oldName.StartsWith("~/") ? IOHelper.MapPath(oldName) : IOHelper.MapPath(folderPath + oldName); + // var savePath = filename.StartsWith("~/") ? IOHelper.MapPath(filename) : IOHelper.MapPath(folderPath + filename); - //Directory check.. only allow files in script dir and below to be edited - if (savePath.StartsWith(IOHelper.MapPath(folderPath))) - { - //deletes the old file - if (savePath != saveOldPath) - { - if (File.Exists(saveOldPath)) - File.Delete(saveOldPath); - } - using (var sw = File.CreateText(savePath)) - { - sw.Write(val); - } - returnValue = "true"; - } - else - { - returnValue = "illegalPath"; - } + // //Directory check.. only allow files in script dir and below to be edited + // if (savePath.StartsWith(IOHelper.MapPath(folderPath))) + // { + // //deletes the old file + // if (savePath != saveOldPath) + // { + // if (File.Exists(saveOldPath)) + // File.Delete(saveOldPath); + // } + // using (var sw = File.CreateText(savePath)) + // { + // sw.Write(val); + // } + // returnValue = "true"; + // } + // else + // { + // returnValue = "illegalPath"; + // } - return returnValue; - } - return "false"; - } + // return returnValue; + // } + // return "false"; + //} [WebMethod] public string SaveScript(string filename, string oldName, string contents) @@ -445,6 +445,7 @@ namespace umbraco.presentation.webservices return "false"; } + [Obsolete("This method has been superceded by the REST service /Umbraco/RestServices/SaveFile/SaveTemplate which is powered by the SafeFileController.")] [WebMethod] public string SaveTemplate(string templateName, string templateAlias, string templateContents, int templateID, int masterTemplateID) { diff --git a/src/packages/repositories.config b/src/packages/repositories.config index ea7ace2ef7..f811520cac 100644 --- a/src/packages/repositories.config +++ b/src/packages/repositories.config @@ -1,6 +1,7 @@  + diff --git a/src/umbraco.businesslogic/BasePages/BasePage.cs b/src/umbraco.businesslogic/BasePages/BasePage.cs index a74f61b355..c6655d76a6 100644 --- a/src/umbraco.businesslogic/BasePages/BasePage.cs +++ b/src/umbraco.businesslogic/BasePages/BasePage.cs @@ -2,6 +2,8 @@ using System; using System.Data; using System.Web; using System.Linq; +using System.Web.Mvc; +using System.Web.Routing; using System.Web.Security; using Umbraco.Core.Logging; using umbraco.BusinessLogic; @@ -70,6 +72,18 @@ namespace umbraco.BasePages } } + private UrlHelper _url; + /// + /// Returns a UrlHelper + /// + /// + /// This URL helper is created without any route data and an empty request context + /// + public UrlHelper Url + { + get { return _url ?? (_url = new UrlHelper(new RequestContext(new HttpContextWrapper(Context), new RouteData()))); } + } + /// /// Returns a refernce of an instance of ClientTools for access to the pages client API /// diff --git a/src/umbraco.businesslogic/packages.config b/src/umbraco.businesslogic/packages.config new file mode 100644 index 0000000000..8fb83abb39 --- /dev/null +++ b/src/umbraco.businesslogic/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/umbraco.businesslogic/umbraco.businesslogic.csproj b/src/umbraco.businesslogic/umbraco.businesslogic.csproj index 9e8b7b478c..e07ae3cc1d 100644 --- a/src/umbraco.businesslogic/umbraco.businesslogic.csproj +++ b/src/umbraco.businesslogic/umbraco.businesslogic.csproj @@ -53,6 +53,8 @@ false true + ..\ + true bin\Debug\ @@ -102,6 +104,9 @@ AllRules.ruleset + + True + @@ -116,6 +121,30 @@ 3.5 + + True + ..\packages\Microsoft.AspNet.WebPages.2.0.20710.0\lib\net40\System.Web.Helpers.dll + + + True + ..\packages\Microsoft.AspNet.Mvc.4.0.20710.0\lib\net40\System.Web.Mvc.dll + + + True + ..\packages\Microsoft.AspNet.Razor.2.0.20715.0\lib\net40\System.Web.Razor.dll + + + True + ..\packages\Microsoft.AspNet.WebPages.2.0.20710.0\lib\net40\System.Web.WebPages.dll + + + True + ..\packages\Microsoft.AspNet.WebPages.2.0.20710.0\lib\net40\System.Web.WebPages.Deployment.dll + + + True + ..\packages\Microsoft.AspNet.WebPages.2.0.20710.0\lib\net40\System.Web.WebPages.Razor.dll + System.XML @@ -241,6 +270,9 @@ + + + @@ -248,4 +280,5 @@ + \ No newline at end of file