Got macro content loading in editor async updated more of the macro plugin to ensure the macro contents cannot be edited.

This commit is contained in:
Shannon
2013-09-24 13:34:29 +10:00
parent 4c77e0de0e
commit 972d33ccec
10 changed files with 256 additions and 80 deletions

View File

@@ -10,7 +10,7 @@ namespace Umbraco.Core.Macros
/// </summary>
internal class MacroTagParser
{
private static readonly Regex MacroRteContent = new Regex(@"(<div class=[""']umb-macro-holder[""'].*?>.*?<!--\s*?)(<\?UMBRACO_MACRO.*?/>)(.*?</div>)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
private static readonly Regex MacroRteContent = new Regex(@"(<div class=[""']umb-macro-holder.+?[""'].*?>.*?<!--\s*?)(<\?UMBRACO_MACRO.*?/>)(.*?</div>)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
private static readonly Regex MacroPersistedFormat = new Regex(@"<\?UMBRACO_MACRO macroAlias=[""'](\w+?)[""'].+?/>", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
/// <summary>
@@ -35,8 +35,11 @@ namespace Umbraco.Core.Macros
{
if (match.Groups.Count >= 2)
{
//<div class="umb-macro-holder" data-load-content="false">
var sb = new StringBuilder("<div class=\"umb-macro-holder\"");
//<div class="umb-macro-holder myMacro mceNonEditable">
var alias = match.Groups[1].Value;
var sb = new StringBuilder("<div class=\"umb-macro-holder ");
sb.Append(alias);
sb.Append(" mceNonEditable\"");
foreach (var htmlAttribute in htmlAttributes)
{
sb.Append(" ");
@@ -51,7 +54,7 @@ namespace Umbraco.Core.Macros
sb.AppendLine(" -->");
sb.Append("Macro alias: ");
sb.Append("<strong>");
sb.Append(match.Groups[1].Value);
sb.Append(alias);
sb.Append("</strong></div>");
return sb.ToString();
}

View File

@@ -18,7 +18,7 @@ namespace Umbraco.Tests.Macros
Assert.AreEqual(@"<p>asdfasdf</p>
<p>asdfsadf</p>
<div class=""umb-macro-holder"" test1=""value1"" test2=""value2"">
<div class=""umb-macro-holder Map mceNonEditable"" test1=""value1"" test2=""value2"">
<!-- <?UMBRACO_MACRO macroAlias=""Map"" /> -->
Macro alias: <strong>Map</strong></div>
<p>asdfasdf</p>", result);
@@ -30,7 +30,7 @@ Macro alias: <strong>Map</strong></div>
var content = @"<html>
<body>
<h1>asdfasdf</h1>
<div class='umb-macro-holder' att1='asdf' att2='asdfasdfasdf' att3=""sdfsdfd"">
<div class='umb-macro-holder Map mceNonEditable' att1='asdf' att2='asdfasdfasdf' att3=""sdfsdfd"">
<!-- <?UMBRACO_MACRO macroAlias=""myMacro"" param1=""test1"" param2=""test2"" /> -->
asdfasdf
asdfas

View File

@@ -83,3 +83,13 @@ td.mce-item-selected, th.mce-item-selected {
display:block;
margin:3px;
}
/* loader for macro loading in tinymce*/
.mce-content-body .umb-macro-holder.loading {
background: url(img/loader.gif) right no-repeat;
-moz-background-size: 18px;
-o-background-size: 18px;
-webkit-background-size: 18px;
background-size: 18px;
background-position-x: 99%;
}

View File

@@ -11,7 +11,7 @@ function macroResource($q, $http, umbRequestHelper) {
/**
* @ngdoc method
* @name umbraco.resources.entityResource#getMacroParameters
* @name umbraco.resources.macroResource#getMacroParameters
* @methodOf umbraco.resources.macroResource
*
* @description
@@ -28,6 +28,28 @@ function macroResource($q, $http, umbRequestHelper) {
"GetMacroParameters",
[{ macroId: macroId }])),
'Failed to retreive macro parameters for macro with id ' + macroId);
},
/**
* @ngdoc method
* @name umbraco.resources.macroResource#getMacroResult
* @methodOf umbraco.resources.macroResource
*
* @description
* Gets the result of a macro as html to display in the rich text editor
*
* @param {int} macroId The macro id to get parameters for
* @param {int} pageId The current page id
*
*/
getMacroResultAsHtmlForEditor: function (macroAlias, pageId) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"macroApiBaseUrl",
"GetMacroResultAsHtmlForEditor",
[{ macroAlias: macroAlias }, { pageId: pageId }])),
'Failed to retreive macro result for macro with alias ' + macroAlias);
}
};

View File

@@ -6,7 +6,7 @@
* @description
* A service containing all logic for all of the Umbraco TinyMCE plugins
*/
function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeout) {
function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeout, macroResource) {
return {
/**
@@ -126,8 +126,7 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou
* @param {Object} $scope the current controller scope
*/
createInsertMacro: function (editor, $scope) {
/** Adds custom rules for the macro plugin and custom serialization */
editor.on('preInit', function (args) {
//this is requires so that we tell the serializer that a 'div' is actually allowed in the root, otherwise the cleanup will strip it out
@@ -141,13 +140,9 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou
}
}
});
});
///** Listens for the editor saving */
//$scope.on("saving", function() {
//});
/** Adds the button instance */
editor.addButton('umbmacro', {
icon: 'custom icon-settings-alt',
@@ -194,7 +189,7 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou
//remove the event listener before re-selecting
editor.off('NodeChange', onNodeChanged);
// move selection to top element to ensure we can't edit this
editor.selection.select(macroElement);
@@ -221,6 +216,31 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou
}
/** This prevents any other commands from executing when the current element is the macro so the content cannot be edited */
editor.on('BeforeExecCommand', function (o) {
if (isOnMacroElement) {
if (o.preventDefault) {
o.preventDefault();
}
if (o.stopImmediatePropagation) {
o.stopImmediatePropagation();
}
return;
}
});
/** This double checks and ensures you can't paste content into the rendered macro */
editor.on("Paste", function (o) {
if (isOnMacroElement) {
if (o.preventDefault) {
o.preventDefault();
}
if (o.stopImmediatePropagation) {
o.stopImmediatePropagation();
}
return;
}
});
//set onNodeChanged event listener
editor.on('NodeChange', onNodeChanged);
@@ -264,7 +284,8 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou
};
//supported keys to move to the next or prev element (13-enter, 27-esc, 38-up, 40-down, 39-right, 37-left)
//supported keys to remove the macro (8-backspace, 46-delete)
//supported keys to remove the macro (8-backspace, 46-delete)
//TODO: Should we make the enter key insert a line break before or leave it as moving to the next element?
if ($.inArray(e.keyCode, [13, 40, 39]) !== -1) {
//move to next element
moveSibling(macroElement, true);
@@ -280,13 +301,13 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou
moveSibling(macroElement, false);
editor.dom.remove(macroElement);
}
return false;
return ;
}
});
},
/** The insert macro button click event handler */
onclick: function () {
dialogService.open({
@@ -298,16 +319,31 @@ function tinyMceService(dialogService, $log, imageHelper, assetsService, $timeou
//put the macro syntax in comments, we will parse this out on the server side to be used
//for persisting.
var macroSyntaxComment = "<!-- " + data.syntax + " -->";
//create an id class for this element so we can re-select it after inserting
var uniqueId = "umb-macro-" + editor.dom.uniqueId();
var macroDiv = editor.dom.create('div',
{
'class': 'umb-macro-holder ' + data.macroAlias + ' mceNonEditable ' + uniqueId
},
macroSyntaxComment + '<ins>Macro alias: <strong>' + data.macroAlias + '</strong></ins>');
editor.insertContent(
editor.dom.createHTML('div',
{
'class': 'umb-macro-holder',
// indicates whether or not this should kick off the ajax request to load in the macro contents.
'data-load-content': false
},
macroSyntaxComment + 'Macro alias: <strong>' + data.macroAlias + '</strong>'));
editor.selection.setNode(macroDiv);
var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId));
var $ins = $macroDiv.find("ins");
//show the throbber
$macroDiv.addClass("loading");
macroResource.getMacroResultAsHtmlForEditor(data.macroAlias, 1234)
.then(function (htmlResult) {
$macroDiv.removeClass("loading");
htmlResult = htmlResult.trim();
if (htmlResult !== "") {
$ins.html(htmlResult);
}
});
}
});

View File

@@ -91,7 +91,6 @@ iframe, .content-column-body {
.mce-panel{background: @grayLighter !important; border-color: @grayLight !important;}
.mce-btn-group, .mce-btn{border: none !important; background: none !important;}
.mce-ico{font-size: 12px !important; color: @blackLight !important;}
/* Special case to support helviticons for the tiny mce button controls */
.mce-ico.mce-i-custom[class^="icon-"],
.mce-ico.mce-i-custom[class*=" icon-"] {

View File

@@ -1,9 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web;
using System.Web.Http;
using AutoMapper;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Web.Models;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
using Umbraco.Web.Routing;
using umbraco;
namespace Umbraco.Web.Editors
{
@@ -31,5 +41,62 @@ namespace Umbraco.Web.Editors
return Mapper.Map<IEnumerable<MacroParameter>>(macro);
}
/// <summary>
/// Gets a rendered macro as html for rendering in the rich text editor
/// </summary>
/// <param name="macroAlias"></param>
/// <param name="pageId"></param>
/// <returns></returns>
public HttpResponseMessage GetMacroResultAsHtmlForEditor(string macroAlias, int pageId)
{
var doc = Services.ContentService.GetById(pageId);
if (doc == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
//need to get a legacy macro object - eventually we'll have a new format but nto yet
var macro = new macro(macroAlias);
if (macro == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
//if it isn't supposed to be rendered in the editor then return an empty string
if (macro.DontRenderInEditor)
{
return new HttpResponseMessage()
{
//need to create a specific content result formatted as html since this controller has been configured
//with only json formatters.
Content = new StringContent(string.Empty, Encoding.UTF8, "text/html")
};
}
//because macro's are filled with insane legacy bits and pieces we need all sorts of wierdness to make them render.
//the 'easiest' way might be to create an IPublishedContent manually and populate the legacy 'page' object with that
//and then set the legacy parameters.
var xml = doc.ToXml();
var publishedContent = new XmlPublishedContent(xml.ToXmlElement());
var legacyPage = new global::umbraco.page(publishedContent);
UmbracoContext.HttpContext.Items["pageID"] = doc.Id;
UmbracoContext.HttpContext.Items["pageElements"] = legacyPage.Elements;
UmbracoContext.HttpContext.Items[global::Umbraco.Core.Constants.Conventions.Url.AltTemplate] = null;
return new HttpResponseMessage()
{
//need to create a specific content result formatted as html since this controller has been configured
//with only json formatters.
Content = new StringContent(
Umbraco.RenderMacro(macro, new Dictionary<string, object>(), legacyPage).ToString(),
Encoding.UTF8,
"text/html"
)
};
}
}
}

View File

@@ -376,6 +376,8 @@ namespace Umbraco.Web
// TODO - this is dirty old legacy tricks, we should clean it up at some point
// also, what is a "custom page" and when should this be either null, or different
// from PublishedContentRequest.PublishedContent.Id ??
// SD: Have found out it can be different when rendering macro contents in the back office, but really youshould just be able
// to pass a page id to the macro renderer instead but due to all the legacy bits that's real difficult.
get
{
try

View File

@@ -138,71 +138,107 @@ namespace Umbraco.Web
/// <returns></returns>
public IHtmlString RenderMacro(string alias, IDictionary<string, object> parameters)
{
if (alias == null) throw new ArgumentNullException("alias");
var m = macro.GetMacro(alias);
if (_umbracoContext.PageId == null)
{
throw new InvalidOperationException("Cannot render a macro when UmbracoContext.PageId is null.");
}
if (_umbracoContext.PublishedContentRequest == null)
{
throw new InvalidOperationException("Cannot render a macro when there is no current PublishedContentRequest.");
}
var macroProps = new Hashtable();
foreach (var i in parameters)
{
//TODO: We are doing at ToLower here because for some insane reason the UpdateMacroModel method of macro.cs
// looks for a lower case match. WTF. the whole macro concept needs to be rewritten.
macroProps.Add(i.Key.ToLower(), i.Value);
}
var macroControl = m.renderMacro(macroProps,
UmbracoContext.Current.PublishedContentRequest.UmbracoPage.Elements,
_umbracoContext.PageId.Value);
string html;
if (macroControl is LiteralControl)
{
// no need to execute, we already have text
html = (macroControl as LiteralControl).Text;
}
else
{
var containerPage = new FormlessPage();
containerPage.Controls.Add(macroControl);
return RenderMacro(alias, parameters, _umbracoContext.PublishedContentRequest.UmbracoPage);
}
using (var output = new StringWriter())
{
// .Execute() does a PushTraceContext/PopTraceContext and writes trace output straight into 'output'
// and I do not see how we could wire the trace context to the current context... so it creates dirty
// trace output right in the middle of the page.
//
// The only thing we can do is fully disable trace output while .Execute() runs and restore afterwards
// which means trace output is lost if the macro is a control (.ascx or user control) that is invoked
// from within Razor -- which makes sense anyway because the control can _not_ run correctly from
// within Razor since it will never be inserted into the page pipeline (which may even not exist at all
// if we're running MVC).
//
/// <summary>
/// Renders the macro with the specified alias, passing in the specified parameters.
/// </summary>
/// <param name="alias">The alias.</param>
/// <param name="parameters">The parameters.</param>
/// <param name="umbracoPage">The legacy umbraco page object that is required for some macros</param>
/// <returns></returns>
internal IHtmlString RenderMacro(string alias, IDictionary<string, object> parameters, page umbracoPage)
{
if (alias == null) throw new ArgumentNullException("alias");
if (umbracoPage == null) throw new ArgumentNullException("umbracoPage");
var m = macro.GetMacro(alias);
if (m == null)
{
throw new KeyNotFoundException("Could not find macro with alias " + alias);
}
return RenderMacro(m, parameters, umbracoPage);
}
/// <summary>
/// Renders the macro with the specified alias, passing in the specified parameters.
/// </summary>
/// <param name="m">The macro.</param>
/// <param name="parameters">The parameters.</param>
/// <param name="umbracoPage">The legacy umbraco page object that is required for some macros</param>
/// <returns></returns>
internal IHtmlString RenderMacro(macro m, IDictionary<string, object> parameters, page umbracoPage)
{
if (umbracoPage == null) throw new ArgumentNullException("umbracoPage");
if (m == null) throw new ArgumentNullException("m");
if (_umbracoContext.PageId == null)
{
throw new InvalidOperationException("Cannot render a macro when UmbracoContext.PageId is null.");
}
var macroProps = new Hashtable();
foreach (var i in parameters)
{
//TODO: We are doing at ToLower here because for some insane reason the UpdateMacroModel method of macro.cs
// looks for a lower case match. WTF. the whole macro concept needs to be rewritten.
macroProps.Add(i.Key.ToLower(), i.Value);
}
var macroControl = m.renderMacro(macroProps,
umbracoPage.Elements,
_umbracoContext.PageId.Value);
string html;
if (macroControl is LiteralControl)
{
// no need to execute, we already have text
html = (macroControl as LiteralControl).Text;
}
else
{
var containerPage = new FormlessPage();
containerPage.Controls.Add(macroControl);
using (var output = new StringWriter())
{
// .Execute() does a PushTraceContext/PopTraceContext and writes trace output straight into 'output'
// and I do not see how we could wire the trace context to the current context... so it creates dirty
// trace output right in the middle of the page.
//
// The only thing we can do is fully disable trace output while .Execute() runs and restore afterwards
// which means trace output is lost if the macro is a control (.ascx or user control) that is invoked
// from within Razor -- which makes sense anyway because the control can _not_ run correctly from
// within Razor since it will never be inserted into the page pipeline (which may even not exist at all
// if we're running MVC).
//
// I'm sure there's more things that will get lost with this context changing but I guess we'll figure
// those out as we go along. One thing we lose is the content type response output.
// http://issues.umbraco.org/issue/U4-1599 if it is setup during the macro execution. So
// here we'll save the content type response and reset it after execute is called.
var contentType = _umbracoContext.HttpContext.Response.ContentType;
var traceIsEnabled = containerPage.Trace.IsEnabled;
containerPage.Trace.IsEnabled = false;
_umbracoContext.HttpContext.Server.Execute(containerPage, output, false);
containerPage.Trace.IsEnabled = traceIsEnabled;
var contentType = _umbracoContext.HttpContext.Response.ContentType;
var traceIsEnabled = containerPage.Trace.IsEnabled;
containerPage.Trace.IsEnabled = false;
_umbracoContext.HttpContext.Server.Execute(containerPage, output, false);
containerPage.Trace.IsEnabled = traceIsEnabled;
//reset the content type
_umbracoContext.HttpContext.Response.ContentType = contentType;
_umbracoContext.HttpContext.Response.ContentType = contentType;
//Now, we need to ensure that local links are parsed
html = TemplateUtilities.ParseInternalLinks(output.ToString());
}
}
//Now, we need to ensure that local links are parsed
html = TemplateUtilities.ParseInternalLinks(output.ToString());
}
}
return new HtmlString(html);
}
return new HtmlString(html);
}
#endregion

View File

@@ -16,6 +16,7 @@ namespace umbraco.presentation
/// <summary>
/// Summary description for macroResultWrapper.
/// </summary>
[Obsolete("This is no longer used and will be removed from the codebase")]
public partial class macroResultWrapper : BasePages.UmbracoEnsuredPage
{