Files
Umbraco-CMS/src/Umbraco.Web.BackOffice/Controllers/MacroRenderingController.cs

191 lines
8.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Core;
using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Templates;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Exceptions;
using Umbraco.Web.Routing;
namespace Umbraco.Web.BackOffice.Controllers
{
/// <summary>
/// API controller to deal with Macro data
/// </summary>
/// <remarks>
/// Note that this implements IRequiresSessionState which will enable HttpContext.Session - generally speaking we don't normally
/// enable this for webapi controllers, however since this controller is used to render macro content and macros can access
/// Session, we don't want it to throw null reference exceptions.
/// </remarks>
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
public class MacroRenderingController : UmbracoAuthorizedJsonController
{
private readonly IMacroService _macroService;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IShortStringHelper _shortStringHelper;
private readonly ISiteDomainHelper _siteDomainHelper;
private readonly UmbracoMapper _umbracoMapper;
private readonly IUmbracoComponentRenderer _componentRenderer;
private readonly IVariationContextAccessor _variationContextAccessor;
public MacroRenderingController(
UmbracoMapper umbracoMapper,
IUmbracoComponentRenderer componentRenderer,
IVariationContextAccessor variationContextAccessor,
IMacroService macroService,
IUmbracoContextAccessor umbracoContextAccessor,
IShortStringHelper shortStringHelper,
ISiteDomainHelper 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));
}
/// <summary>
/// Gets the macro parameters to be filled in for a particular macro
/// </summary>
/// <returns></returns>
/// <remarks>
/// 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.
/// </remarks>
public IEnumerable<MacroParameter> GetMacroParameters(int macroId)
{
var macro = _macroService.GetById(macroId);
if (macro == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
return _umbracoMapper.Map<IEnumerable<MacroParameter>>(macro).OrderBy(x => x.SortOrder);
}
/// <summary>
/// Gets a rendered macro as HTML for rendering in the rich text editor
/// </summary>
/// <param name="macroAlias"></param>
/// <param name="pageId"></param>
/// <param name="macroParams">
/// To send a dictionary as a GET parameter the query should be structured like:
///
/// ?macroAlias=Test&pageId=3634&macroParams[0].key=myKey&macroParams[0].value=myVal&macroParams[1].key=anotherKey&macroParams[1].value=anotherVal
///
/// </param>
/// <returns></returns>
[HttpGet]
public IActionResult GetMacroResultAsHtmlForEditor(string macroAlias, int pageId, [FromQuery] IDictionary<string, object> macroParams)
{
return GetMacroResultAsHtml(macroAlias, pageId, macroParams);
}
/// <summary>
/// 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
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost]
public IActionResult GetMacroResultAsHtmlForEditor(MacroParameterModel model)
{
return GetMacroResultAsHtml(model.MacroAlias, model.PageId, model.MacroParams);
}
public class MacroParameterModel
{
public string MacroAlias { get; set; }
public int PageId { get; set; }
public IDictionary<string, object> MacroParams { get; set; }
}
private IActionResult GetMacroResultAsHtml(string macroAlias, int pageId, IDictionary<string, object> macroParams)
{
var m = _macroService.GetByAlias(macroAlias);
if (m == null)
throw new HttpResponseException(HttpStatusCode.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(_componentRenderer.RenderMacroForContent(publishedContent, m.Alias, macroParams).ToString(), "text/html",
Encoding.UTF8);
}
}
[HttpPost]
public HttpResponseMessage 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 new HttpResponseMessage(HttpStatusCode.OK);
}
public class CreatePartialViewMacroWithFileModel
{
public string Filename { get; set; }
public string VirtualPath { get; set; }
}
}
}