using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Controllers { /// /// API controller to deal with Macro data /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class MacroRenderingController : UmbracoAuthorizedJsonController { private readonly IMacroService _macroService; private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IShortStringHelper _shortStringHelper; private readonly ISiteDomainMapper _siteDomainHelper; private readonly IUmbracoMapper _umbracoMapper; private readonly IUmbracoComponentRenderer _componentRenderer; private readonly IVariationContextAccessor _variationContextAccessor; public MacroRenderingController( IUmbracoMapper umbracoMapper, IUmbracoComponentRenderer componentRenderer, IVariationContextAccessor variationContextAccessor, IMacroService macroService, IUmbracoContextAccessor umbracoContextAccessor, IShortStringHelper shortStringHelper, ISiteDomainMapper siteDomainHelper) { _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); _componentRenderer = componentRenderer ?? throw new ArgumentNullException(nameof(componentRenderer)); _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); _macroService = macroService ?? throw new ArgumentNullException(nameof(macroService)); _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); _siteDomainHelper = siteDomainHelper ?? throw new ArgumentNullException(nameof(siteDomainHelper)); } /// /// Gets the macro parameters to be filled in for a particular macro /// /// /// /// Note that ALL logged in users have access to this method because editors will need to insert macros into rte (content/media/members) and it's used for /// inserting into templates/views/etc... it doesn't expose any sensitive data. /// public ActionResult> GetMacroParameters(int macroId) { var macro = _macroService.GetById(macroId); if (macro == null) { return NotFound(); } return new ActionResult>(_umbracoMapper.Map>(macro).OrderBy(x => x.SortOrder)); } /// /// Gets a rendered macro as HTML for rendering in the rich text editor /// /// /// /// /// To send a dictionary as a GET parameter the query should be structured like: /// /// ?macroAlias=Test&pageId=3634¯oParams[0].key=myKey¯oParams[0].value=myVal¯oParams[1].key=anotherKey¯oParams[1].value=anotherVal /// /// /// [HttpGet] public async Task GetMacroResultAsHtmlForEditor(string macroAlias, int pageId, [FromQuery] IDictionary macroParams) { return await GetMacroResultAsHtml(macroAlias, pageId, macroParams); } /// /// Gets a rendered macro as HTML for rendering in the rich text editor. /// Using HTTP POST instead of GET allows for more parameters to be passed as it's not dependent on URL-length limitations like GET. /// The method using GET is kept to maintain backwards compatibility /// /// /// [HttpPost] public async Task GetMacroResultAsHtmlForEditor(MacroParameterModel model) { return await GetMacroResultAsHtml(model.MacroAlias, model.PageId, model.MacroParams); } public class MacroParameterModel { public string MacroAlias { get; set; } public int PageId { get; set; } public IDictionary MacroParams { get; set; } } private async Task GetMacroResultAsHtml(string macroAlias, int pageId, IDictionary macroParams) { var m = _macroService.GetByAlias(macroAlias); if (m == null) return NotFound(); var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); var publishedContent = umbracoContext.Content.GetById(true, pageId); //if it isn't supposed to be rendered in the editor then return an empty string //currently we cannot render a macro if the page doesn't yet exist if (pageId == -1 || publishedContent == null || m.DontRender) { //need to create a specific content result formatted as HTML since this controller has been configured //with only json formatters. return Content(string.Empty, "text/html", Encoding.UTF8); } // When rendering the macro in the backoffice the default setting would be to use the Culture of the logged in user. // Since a Macro might contain thing thats related to the culture of the "IPublishedContent" (ie Dictionary keys) we want // to set the current culture to the culture related to the content item. This is hacky but it works. // fixme // in a 1:1 situation we do not handle the language being edited // so the macro renders in the wrong language var culture = DomainUtilities.GetCultureFromDomains(publishedContent.Id, publishedContent.Path, null, umbracoContext, _siteDomainHelper); if (culture != null) Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(culture); // must have an active variation context! _variationContextAccessor.VariationContext = new VariationContext(culture); using (umbracoContext.ForcedPreview(true)) { //need to create a specific content result formatted as HTML since this controller has been configured //with only json formatters. return Content((await _componentRenderer.RenderMacroForContent(publishedContent, m.Alias, macroParams)).ToString(), "text/html", Encoding.UTF8); } } [HttpPost] public IActionResult CreatePartialViewMacroWithFile(CreatePartialViewMacroWithFileModel model) { if (model == null) throw new ArgumentNullException("model"); if (string.IsNullOrWhiteSpace(model.Filename)) throw new ArgumentException("Filename cannot be null or whitespace", "model.Filename"); if (string.IsNullOrWhiteSpace(model.VirtualPath)) throw new ArgumentException("VirtualPath cannot be null or whitespace", "model.VirtualPath"); var macroName = model.Filename.TrimEnd(".cshtml"); var macro = new Macro(_shortStringHelper) { Alias = macroName.ToSafeAlias(_shortStringHelper), Name = macroName, MacroSource = model.VirtualPath.EnsureStartsWith("~") }; _macroService.Save(macro); // may throw return Ok(); } public class CreatePartialViewMacroWithFileModel { public string Filename { get; set; } public string VirtualPath { get; set; } } } }