using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Controllers { /// /// The API controller used for editing dictionary items /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [Authorize(Policy = AuthorizationPolicies.TreeAccessMacros)] [ParameterSwapControllerActionSelector(nameof(GetById), "id", typeof(int), typeof(Guid), typeof(Udi))] public class MacrosController : BackOfficeNotificationsController { private readonly ParameterEditorCollection _parameterEditorCollection; private readonly IMacroService _macroService; private readonly IShortStringHelper _shortStringHelper; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly ILogger _logger; private readonly IHostingEnvironment _hostingEnvironment; private readonly IUmbracoMapper _umbracoMapper; public MacrosController( ParameterEditorCollection parameterEditorCollection, IMacroService macroService, IShortStringHelper shortStringHelper, IBackOfficeSecurityAccessor backofficeSecurityAccessor, ILogger logger, IHostingEnvironment hostingEnvironment, IUmbracoMapper umbracoMapper) { _parameterEditorCollection = parameterEditorCollection ?? throw new ArgumentNullException(nameof(parameterEditorCollection)); _macroService = macroService ?? throw new ArgumentNullException(nameof(macroService)); _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); } /// /// Creates a new macro /// /// /// The name. /// /// /// The id of the created macro /// [HttpPost] public ActionResult Create(string name) { if (string.IsNullOrWhiteSpace(name)) { return ValidationProblem("Name can not be empty"); } var alias = name.ToSafeAlias(_shortStringHelper); if (_macroService.GetByAlias(alias) != null) { return ValidationProblem("Macro with this alias already exists"); } if (name == null || name.Length > 255) { return ValidationProblem("Name cannnot be more than 255 characters in length."); } try { var macro = new Macro(_shortStringHelper) { Alias = alias, Name = name, MacroSource = string.Empty }; _macroService.Save(macro, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); return macro.Id; } catch (Exception exception) { const string errorMessage = "Error creating macro"; _logger.LogError(exception, errorMessage); return ValidationProblem(errorMessage); } } [HttpGet] public ActionResult GetById(int id) { var macro = _macroService.GetById(id); if (macro == null) { return ValidationProblem($"Macro with id {id} does not exist"); } var macroDisplay = MapToDisplay(macro); return macroDisplay; } [HttpGet] public ActionResult GetById(Guid id) { var macro = _macroService.GetById(id); if (macro == null) { return ValidationProblem($"Macro with id {id} does not exist"); } var macroDisplay = MapToDisplay(macro); return macroDisplay; } [HttpGet] public ActionResult GetById(Udi id) { var guidUdi = id as GuidUdi; if (guidUdi == null) return ValidationProblem($"Macro with id {id} does not exist"); var macro = _macroService.GetById(guidUdi.Guid); if (macro == null) { return ValidationProblem($"Macro with id {id} does not exist"); } var macroDisplay = MapToDisplay(macro); return macroDisplay; } [HttpPost] public IActionResult DeleteById(int id) { var macro = _macroService.GetById(id); if (macro == null) { return ValidationProblem($"Macro with id {id} does not exist"); } _macroService.Delete(macro); return Ok(); } [HttpPost] public ActionResult Save(MacroDisplay macroDisplay) { if (macroDisplay == null) { return ValidationProblem("No macro data found in request"); } if (macroDisplay.Name == null || macroDisplay.Name.Length > 255) { return ValidationProblem("Name cannnot be more than 255 characters in length."); } var macro = _macroService.GetById(int.Parse(macroDisplay.Id.ToString(), CultureInfo.InvariantCulture)); if (macro == null) { return ValidationProblem($"Macro with id {macroDisplay.Id} does not exist"); } if (macroDisplay.Alias != macro.Alias) { var macroByAlias = _macroService.GetByAlias(macroDisplay.Alias); if (macroByAlias != null) { return ValidationProblem("Macro with this alias already exists"); } } macro.Alias = macroDisplay.Alias; macro.Name = macroDisplay.Name; macro.CacheByMember = macroDisplay.CacheByUser; macro.CacheByPage = macroDisplay.CacheByPage; macro.CacheDuration = macroDisplay.CachePeriod; macro.DontRender = !macroDisplay.RenderInEditor; macro.UseInEditor = macroDisplay.UseInEditor; macro.MacroSource = macroDisplay.View; macro.Properties.ReplaceAll(macroDisplay.Parameters.Select((x,i) => new MacroProperty(x.Key, x.Label, i, x.Editor))); try { _macroService.Save(macro, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); macroDisplay.Notifications.Clear(); macroDisplay.Notifications.Add(new BackOfficeNotification("Success", "Macro saved", NotificationStyle.Success)); return macroDisplay; } catch (Exception exception) { const string errorMessage = "Error creating macro"; _logger.LogError(exception, errorMessage); return ValidationProblem(errorMessage); } } /// /// Gets a list of available macro partials /// /// /// The . /// public IEnumerable GetPartialViews() { var views = new List(); views.AddRange(this.FindPartialViewsFiles()); return views; } /// /// Gets the available parameter editors /// /// /// The . /// public ParameterEditorCollection GetParameterEditors() { return _parameterEditorCollection; } /// /// Gets the available parameter editors grouped by their group. /// /// /// The . /// public IDictionary> GetGroupedParameterEditors() { var parameterEditors = _parameterEditorCollection.ToArray(); var grouped = parameterEditors .GroupBy(x => x.Group.IsNullOrWhiteSpace() ? "" : x.Group.ToLower()) .OrderBy(x => x.Key) .ToDictionary(group => group.Key, group => group.OrderBy(d => d.Name).AsEnumerable()); return grouped; } /// /// Get parameter editor by alias. /// /// /// The . /// public IDataEditor GetParameterEditorByAlias(string alias) { var parameterEditors = _parameterEditorCollection.ToArray(); var parameterEditor = parameterEditors.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); return parameterEditor; } /// /// Finds all the macro partials /// /// /// The . /// private IEnumerable FindPartialViewsFiles() { var files = new List(); files.AddRange(this.FindPartialViewFilesInViewsFolder()); files.AddRange(this.FindPartialViewFilesInPluginFolders()); return files; } /// /// Finds all macro partials in the views folder /// /// /// The . /// private IEnumerable FindPartialViewFilesInViewsFolder() { // TODO: This is inconsistent. We have FileSystems.MacroPartialsFileSystem but we basically don't use // that at all except to render the tree. In the future we may want to use it. This also means that // we are storing the virtual path of the macro like ~/Views/MacroPartials/Login.cshtml instead of the // relative path which would work with the FileSystems.MacroPartialsFileSystem, but these are incompatible. // At some point this should all be made consistent and probably just use FileSystems.MacroPartialsFileSystem. var partialsDir = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials); return this.FindPartialViewFilesInFolder( partialsDir, partialsDir, Constants.SystemDirectories.MacroPartials); } /// /// Finds partial view files in app plugin folders. /// /// /// private IEnumerable FindPartialViewFilesInPluginFolders() { var files = new List(); var appPluginsFolder = new DirectoryInfo(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.AppPlugins)); if (!appPluginsFolder.Exists) { return files; } foreach (var directory in appPluginsFolder.GetDirectories()) { var viewsFolder = directory.GetDirectories("Views"); if (viewsFolder.Any()) { var macroPartials = viewsFolder.First().GetDirectories("MacroPartials"); if (macroPartials.Any()) { files.AddRange(this.FindPartialViewFilesInFolder(macroPartials.First().FullName, macroPartials.First().FullName, Constants.SystemDirectories.AppPlugins + "/" + directory.Name + "/Views/MacroPartials")); } } } return files; } /// /// Finds all partial views in a folder and subfolders /// /// /// The org path. /// /// /// The path. /// /// /// The prefix virtual path. /// /// /// The . /// private IEnumerable FindPartialViewFilesInFolder(string orgPath, string path, string prefixVirtualPath) { var files = new List(); var dirInfo = new DirectoryInfo(path); if (dirInfo.Exists) { foreach (var dir in dirInfo.GetDirectories()) { files.AddRange(this.FindPartialViewFilesInFolder(orgPath, path + "/" + dir.Name, prefixVirtualPath)); } var fileInfo = dirInfo.GetFiles("*.*"); files.AddRange( fileInfo.Select(file => prefixVirtualPath.TrimEnd(Constants.CharArrays.ForwardSlash) + "/" + (path.Replace(orgPath, string.Empty).Trim(Constants.CharArrays.ForwardSlash) + "/" + file.Name).Trim(Constants.CharArrays.ForwardSlash))); } return files; } /// /// Used to map an instance to a /// /// /// private MacroDisplay MapToDisplay(IMacro macro) { var display = _umbracoMapper.Map(macro); var parameters = macro.Properties.Values .OrderBy(x => x.SortOrder) .Select(x => new MacroParameterDisplay() { Editor = x.EditorAlias, Key = x.Alias, Label = x.Name, Id = x.Id }); display.Parameters = parameters; return display; } } }