using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Web; using System.Web.Mvc; using System.Web.Routing; using System.Web.WebPages; using Umbraco.Core.IO; using umbraco.cms.businesslogic.macro; using umbraco.interfaces; using Umbraco.Web.Mvc; using Umbraco.Core; using System.Web.Mvc.Html; namespace Umbraco.Web.Macros { /// /// A macro engine using MVC Partial Views to execute /// public class PartialViewMacroEngine : IMacroEngine { private readonly Func _getHttpContext; private readonly Func _getUmbracoContext; public const string EngineName = "Partial View Macro Engine"; public PartialViewMacroEngine() { _getHttpContext = () => { if (HttpContext.Current == null) throw new InvalidOperationException("The " + this.GetType() + " cannot execute with a null HttpContext.Current reference"); return new HttpContextWrapper(HttpContext.Current); }; _getUmbracoContext = () => { if (UmbracoContext.Current == null) throw new InvalidOperationException("The " + this.GetType() + " cannot execute with a null UmbracoContext.Current reference"); return UmbracoContext.Current; }; } /// /// Constructor generally used for unit testing /// /// /// internal PartialViewMacroEngine(HttpContextBase httpContext, UmbracoContext umbracoContext) { _getHttpContext = () => httpContext; _getUmbracoContext = () => umbracoContext; } public string Name { get { return EngineName; } } //NOTE: We do not return any supported extensions because we don't want the MacroEngineFactory to return this // macro engine when searching for engines via extension. Those types of engines are reserved for files that are // stored in the ~/macroScripts folder and each engine must support unique extensions. This is a total Hack until // we rewrite how macro engines work. public IEnumerable SupportedExtensions { get { return Enumerable.Empty(); } } //NOTE: We do not return any supported extensions because we don't want the MacroEngineFactory to return this // macro engine when searching for engines via extension. Those types of engines are reserved for files that are // stored in the ~/macroScripts folder and each engine must support unique extensions. This is a total Hack until // we rewrite how macro engines work. public IEnumerable SupportedUIExtensions { get { return Enumerable.Empty(); } } public Dictionary SupportedProperties { get { throw new NotSupportedException(); } } public bool Validate(string code, string tempFileName, INode currentPage, out string errorMessage) { var temp = GetVirtualPathFromPhysicalPath(tempFileName); try { CompileAndInstantiate(temp); } catch (Exception exception) { errorMessage = exception.Message; return false; } errorMessage = string.Empty; return true; } public string Execute(MacroModel macro, INode currentPage) { if (macro == null) throw new ArgumentNullException("macro"); if (currentPage == null) throw new ArgumentNullException("currentPage"); if (macro.ScriptName.IsNullOrWhiteSpace()) throw new ArgumentException("The ScriptName property of the macro object cannot be null or empty"); //if (!macro.ScriptName.StartsWith(SystemDirectories.MvcViews + "/MacroPartials/") // && (!Regex.IsMatch(macro.ScriptName, "~/App_Plugins/.+?/Views/MacroPartials", RegexOptions.Compiled))) //{ // throw new InvalidOperationException("Cannot render the Partial View Macro with file: " + macro.ScriptName + ". All Partial View Macros must exist in the " + SystemDirectories.MvcViews + "/MacroPartials/ folder"); //} var http = _getHttpContext(); var umbCtx = _getUmbracoContext(); var routeVals = new RouteData(); routeVals.Values.Add("controller", "PartialViewMacro"); routeVals.Values.Add("action", "Index"); routeVals.DataTokens.Add("umbraco-context", umbCtx); //required for UmbracoViewPage //lets render this controller as a child action if we are currently executing using MVC //(otherwise don't do this since we're using webforms) var mvcHandler = http.CurrentHandler as MvcHandler; var viewContext = new ViewContext {ViewData = new ViewDataDictionary()};; if (mvcHandler != null) { //try and extract the current view context from the route values, this would be set in the UmbracoViewPage. if (mvcHandler.RequestContext.RouteData.DataTokens.ContainsKey(Umbraco.Web.Mvc.Constants.DataTokenCurrentViewContext)) { viewContext = (ViewContext) mvcHandler.RequestContext.RouteData.DataTokens[Umbraco.Web.Mvc.Constants.DataTokenCurrentViewContext]; } routeVals.DataTokens.Add("ParentActionViewContext", viewContext); } var request = new RequestContext(http, routeVals); string output; using (var controller = new PartialViewMacroController(umbCtx, macro, currentPage)) { //bubble up the model state from the main view context to our custom controller. //when merging we'll create a new dictionary, otherwise you might run into an enumeration error // caused from ModelStateDictionary controller.ModelState.Merge(new ModelStateDictionary(viewContext.ViewData.ModelState)); controller.ControllerContext = new ControllerContext(request, controller); //call the action to render var result = controller.Index(); output = controller.RenderViewResultAsString(result); } return output; } private string GetVirtualPathFromPhysicalPath(string physicalPath) { string rootpath = _getHttpContext().Server.MapPath("~/"); physicalPath = physicalPath.Replace(rootpath, ""); physicalPath = physicalPath.Replace("\\", "/"); return "~/" + physicalPath; } private static PartialViewMacroPage CompileAndInstantiate(string virtualPath) { //Compile Razor - We Will Leave This To ASP.NET Compilation Engine & ASP.NET WebPages //Security in medium trust is strict around here, so we can only pass a virtual file path //ASP.NET Compilation Engine caches returned types //Changed From BuildManager As Other Properties Are Attached Like Context Path/ var webPageBase = WebPageBase.CreateInstanceFromVirtualPath(virtualPath); var webPage = webPageBase as PartialViewMacroPage; if (webPage == null) throw new InvalidCastException("All Partial View Macro views must inherit from " + typeof(PartialViewMacroPage).FullName); return webPage; } } }