No more using TemplateUtilities

This commit is contained in:
Shannon
2019-10-21 23:53:14 +11:00
parent 8ccebd8006
commit 4f9e0fcb92
16 changed files with 337 additions and 243 deletions

View File

@@ -24,6 +24,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
internal sealed class ContentRepositoryBase
{
/// <summary>
///
/// This is used for unit tests ONLY
/// </summary>
public static bool ThrowOnWarning = false;
@@ -43,7 +44,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected ILanguageRepository LanguageRepository { get; }
protected PropertyEditorCollection PropertyEditors => Current.PropertyEditors; // TODO: inject
protected PropertyEditorCollection PropertyEditors => Current.PropertyEditors; // TODO: inject ... this causes circular refs, not sure which refs they are though
#region Versions

View File

@@ -11,6 +11,7 @@ using Umbraco.Core.Models;
using Umbraco.Web.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web;
using Umbraco.Web.Templates;
namespace Umbraco.Tests.PublishedContent
{
@@ -38,9 +39,11 @@ namespace Umbraco.Tests.PublishedContent
base.Initialize();
var converters = Factory.GetInstance<PropertyValueConverterCollection>();
var umbracoCtxAccessor = Mock.Of<IUmbracoContextAccessor>();
var logger = Mock.Of<ILogger>();
var dataTypeService = new TestObjects.TestDataTypeService(
new DataType(new RichTextPropertyEditor(Mock.Of<ILogger>(), Mock.Of<IMediaService>(), Mock.Of<IContentTypeBaseServiceProvider>(), Mock.Of<IUmbracoContextAccessor>())) { Id = 1 });
new DataType(new RichTextPropertyEditor(logger, umbracoCtxAccessor, new MediaParser(umbracoCtxAccessor, logger, Mock.Of<IMediaService>(), Mock.Of<IContentTypeBaseServiceProvider>()))) { Id = 1 });
var publishedContentTypeFactory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), converters, dataTypeService);

View File

@@ -21,6 +21,7 @@ using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing;
using Umbraco.Web.Models.PublishedContent;
using Umbraco.Web.PropertyEditors;
using Umbraco.Web.Templates;
namespace Umbraco.Tests.PublishedContent
{
@@ -45,11 +46,12 @@ namespace Umbraco.Tests.PublishedContent
var mediaService = Mock.Of<IMediaService>();
var contentTypeBaseServiceProvider = Mock.Of<IContentTypeBaseServiceProvider>();
var umbracoContextAccessor = Mock.Of<IUmbracoContextAccessor>();
var mediaParser = new MediaParser(umbracoContextAccessor, logger, mediaService, contentTypeBaseServiceProvider);
var dataTypeService = new TestObjects.TestDataTypeService(
new DataType(new VoidEditor(logger)) { Id = 1 },
new DataType(new TrueFalsePropertyEditor(logger)) { Id = 1001 },
new DataType(new RichTextPropertyEditor(logger, mediaService, contentTypeBaseServiceProvider, umbracoContextAccessor)) { Id = 1002 },
new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, mediaParser)) { Id = 1002 },
new DataType(new IntegerPropertyEditor(logger)) { Id = 1003 },
new DataType(new TextboxPropertyEditor(logger)) { Id = 1004 },
new DataType(new MediaPickerPropertyEditor(logger)) { Id = 1005 });

View File

@@ -79,7 +79,7 @@ namespace Umbraco.Tests.Web
{
var linkParser = new InternalLinkParser(umbracoContextAccessor);
var output = linkParser.ParseInternalLinks(input);
var output = linkParser.EnsureInternalLinks(input);
Assert.AreEqual(result, output);
}

View File

@@ -28,14 +28,16 @@ namespace Umbraco.Web.PropertyEditors
private IMediaService _mediaService;
private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
private IUmbracoContextAccessor _umbracoContextAccessor;
private readonly MediaParser _mediaParser;
private ILogger _logger;
public GridPropertyEditor(ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor)
public GridPropertyEditor(ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor, MediaParser mediaParser)
: base(logger)
{
_mediaService = mediaService;
_contentTypeBaseServiceProvider = contentTypeBaseServiceProvider;
_umbracoContextAccessor = umbracoContextAccessor;
_mediaParser = mediaParser;
_logger = logger;
}
@@ -45,7 +47,7 @@ namespace Umbraco.Web.PropertyEditors
/// Overridden to ensure that the value is validated
/// </summary>
/// <returns></returns>
protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _mediaService, _contentTypeBaseServiceProvider, _umbracoContextAccessor, _logger);
protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _mediaService, _contentTypeBaseServiceProvider, _umbracoContextAccessor, _logger, _mediaParser);
protected override IConfigurationEditor CreateConfigurationEditor() => new GridConfigurationEditor();
@@ -55,14 +57,16 @@ namespace Umbraco.Web.PropertyEditors
private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
private IUmbracoContextAccessor _umbracoContextAccessor;
private ILogger _logger;
private readonly MediaParser _mediaParser;
public GridPropertyValueEditor(DataEditorAttribute attribute, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor, ILogger logger)
public GridPropertyValueEditor(DataEditorAttribute attribute, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, MediaParser _mediaParser)
: base(attribute)
{
_mediaService = mediaService;
_contentTypeBaseServiceProvider = contentTypeBaseServiceProvider;
_umbracoContextAccessor = umbracoContextAccessor;
_logger = logger;
this._mediaParser = _mediaParser;
}
/// <summary>
@@ -97,8 +101,8 @@ namespace Umbraco.Web.PropertyEditors
// Parse the HTML
var html = rte.Value?.ToString();
var parseAndSavedTempImages = TemplateUtilities.FindAndPersistPastedTempImages(html, mediaParentId, userId, _mediaService, _contentTypeBaseServiceProvider, _logger);
var editorValueWithMediaUrlsRemoved = TemplateUtilities.RemoveMediaUrlsFromTextString(parseAndSavedTempImages);
var parseAndSavedTempImages = _mediaParser.FindAndPersistPastedTempImages(html, mediaParentId, userId);
var editorValueWithMediaUrlsRemoved = _mediaParser.RemoveImageSources(parseAndSavedTempImages);
rte.Value = editorValueWithMediaUrlsRemoved;
}
@@ -127,7 +131,7 @@ namespace Umbraco.Web.PropertyEditors
{
var html = rte.Value?.ToString();
var propertyValueWithMediaResolved = TemplateUtilities.ResolveMediaFromTextString(html);
var propertyValueWithMediaResolved = _mediaParser.EnsureImageSources(html);
rte.Value = propertyValueWithMediaResolved;
}

View File

@@ -24,27 +24,24 @@ namespace Umbraco.Web.PropertyEditors
Icon = "icon-browser-window")]
public class RichTextPropertyEditor : DataEditor
{
private IMediaService _mediaService;
private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
private IUmbracoContextAccessor _umbracoContextAccessor;
private ILogger _logger;
private readonly MediaParser _mediaParser;
/// <summary>
/// The constructor will setup the property editor based on the attribute if one is found
/// </summary>
public RichTextPropertyEditor(ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor) : base(logger)
public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, MediaParser mediaParser)
: base(logger)
{
_mediaService = mediaService;
_contentTypeBaseServiceProvider = contentTypeBaseServiceProvider;
_umbracoContextAccessor = umbracoContextAccessor;
_logger = logger;
_mediaParser = mediaParser;
}
/// <summary>
/// Create a custom value editor
/// </summary>
/// <returns></returns>
protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _mediaService, _contentTypeBaseServiceProvider, _umbracoContextAccessor, _logger);
protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _mediaParser);
protected override IConfigurationEditor CreateConfigurationEditor() => new RichTextConfigurationEditor();
@@ -53,20 +50,16 @@ namespace Umbraco.Web.PropertyEditors
/// <summary>
/// A custom value editor to ensure that macro syntax is parsed when being persisted and formatted correctly for display in the editor
/// </summary>
internal class RichTextPropertyValueEditor : DataValueEditor
internal class RichTextPropertyValueEditor : DataValueEditor, IDataValueReference
{
private IMediaService _mediaService;
private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
private IUmbracoContextAccessor _umbracoContextAccessor;
private ILogger _logger;
private readonly MediaParser _mediaParser;
public RichTextPropertyValueEditor(DataEditorAttribute attribute, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor, ILogger logger)
public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, MediaParser _mediaParser)
: base(attribute)
{
_mediaService = mediaService;
_contentTypeBaseServiceProvider = contentTypeBaseServiceProvider;
_umbracoContextAccessor = umbracoContextAccessor;
_logger = logger;
this._mediaParser = _mediaParser;
}
/// <inheritdoc />
@@ -98,7 +91,7 @@ namespace Umbraco.Web.PropertyEditors
if (val == null)
return null;
var propertyValueWithMediaResolved = TemplateUtilities.ResolveMediaFromTextString(val.ToString());
var propertyValueWithMediaResolved = _mediaParser.EnsureImageSources(val.ToString());
var parsed = MacroTagParser.FormatRichTextPersistedDataForEditor(propertyValueWithMediaResolved, new Dictionary<string, string>());
return parsed;
}
@@ -120,12 +113,22 @@ namespace Umbraco.Web.PropertyEditors
var mediaParent = config?.MediaParentId;
var mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid;
var parseAndSavedTempImages = TemplateUtilities.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId, _mediaService, _contentTypeBaseServiceProvider, _logger);
var editorValueWithMediaUrlsRemoved = TemplateUtilities.RemoveMediaUrlsFromTextString(parseAndSavedTempImages);
var parseAndSavedTempImages = _mediaParser.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId);
var editorValueWithMediaUrlsRemoved = _mediaParser.RemoveImageSources(parseAndSavedTempImages);
var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved);
return parsed;
}
/// <summary>
/// Resolve references from <see cref="IDataValueEditor"/> values
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public IEnumerable<Udi> GetReferences(object value)
{
throw new NotImplementedException();
}
}
internal class RichTextPropertyIndexValueFactory : IPropertyIndexValueFactory

View File

@@ -13,10 +13,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
public class MarkdownEditorValueConverter : PropertyValueConverterBase
{
private readonly InternalLinkParser _localLinkParser;
private readonly UrlParser _urlResolver;
public MarkdownEditorValueConverter(InternalLinkParser localLinkParser)
public MarkdownEditorValueConverter(InternalLinkParser localLinkParser, UrlParser urlResolver)
{
_localLinkParser = localLinkParser;
_urlResolver = urlResolver;
}
public override bool IsConverter(IPublishedPropertyType propertyType)
@@ -34,8 +36,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
var sourceString = source.ToString();
// ensures string is parsed for {localLink} and urls are resolved correctly
sourceString = _localLinkParser.ParseInternalLinks(sourceString, preview);
sourceString = TemplateUtilities.ResolveUrlsFromTextString(sourceString);
sourceString = _localLinkParser.EnsureInternalLinks(sourceString, preview);
sourceString = _urlResolver.EnsureUrls(sourceString);
return sourceString;
}

View File

@@ -25,6 +25,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IMacroRenderer _macroRenderer;
private readonly InternalLinkParser _internalLinkParser;
private readonly UrlParser _urlResolver;
private readonly MediaParser _mediaParser;
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
{
@@ -33,11 +35,14 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
return PropertyCacheLevel.Snapshot;
}
public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, InternalLinkParser internalLinkParser)
public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer,
InternalLinkParser internalLinkParser, UrlParser urlResolver, MediaParser mediaParser)
{
_umbracoContextAccessor = umbracoContextAccessor;
_macroRenderer = macroRenderer;
_internalLinkParser = internalLinkParser;
_urlResolver = urlResolver;
_mediaParser = mediaParser;
}
// NOT thread-safe over a request because it modifies the
@@ -83,9 +88,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
var sourceString = source.ToString();
// ensures string is parsed for {localLink} and urls and media are resolved correctly
sourceString = _internalLinkParser.ParseInternalLinks(sourceString, preview);
sourceString = TemplateUtilities.ResolveUrlsFromTextString(sourceString);
sourceString = TemplateUtilities.ResolveMediaFromTextString(sourceString);
sourceString = _internalLinkParser.EnsureInternalLinks(sourceString, preview);
sourceString = _urlResolver.EnsureUrls(sourceString);
sourceString = _mediaParser.EnsureImageSources(sourceString);
// ensure string is parsed for macros and macros are executed correctly
sourceString = RenderRteMacros(sourceString, preview);

View File

@@ -11,9 +11,10 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
[DefaultPropertyValueConverter]
public class TextStringValueConverter : PropertyValueConverterBase
{
public TextStringValueConverter(InternalLinkParser internalLinkParser)
public TextStringValueConverter(InternalLinkParser internalLinkParser, UrlParser urlParser)
{
_internalLinkParser = internalLinkParser;
_urlParser = urlParser;
}
private static readonly string[] PropertyTypeAliases =
@@ -22,6 +23,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
Constants.PropertyEditors.Aliases.TextArea
};
private readonly InternalLinkParser _internalLinkParser;
private readonly UrlParser _urlParser;
public override bool IsConverter(IPublishedPropertyType propertyType)
=> PropertyTypeAliases.Contains(propertyType.EditorAlias);
@@ -38,8 +40,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
var sourceString = source.ToString();
// ensures string is parsed for {localLink} and urls are resolved correctly
sourceString = _internalLinkParser.ParseInternalLinks(sourceString, preview);
sourceString = TemplateUtilities.ResolveUrlsFromTextString(sourceString);
sourceString = _internalLinkParser.EnsureInternalLinks(sourceString, preview);
sourceString = _urlParser.EnsureUrls(sourceString);
return sourceString;
}

View File

@@ -108,6 +108,8 @@ namespace Umbraco.Web.Runtime
composition.RegisterUnique<IUmbracoComponentRenderer, UmbracoComponentRenderer>();
composition.RegisterUnique<InternalLinkParser>();
composition.RegisterUnique<UrlParser>();
composition.RegisterUnique<MediaParser>();
// register the umbraco helper - this is Transient! very important!
// also, if not level.Run, we cannot really use the helper (during upgrade...)

View File

@@ -23,17 +23,23 @@ namespace Umbraco.Web.Templates
_umbracoContextAccessor = umbracoContextAccessor;
}
public string ParseInternalLinks(string text, bool preview)
/// <summary>
/// Parses the string looking for the {localLink} syntax and updates them to their correct links.
/// </summary>
/// <param name="text"></param>
/// <param name="preview"></param>
/// <returns></returns>
public string EnsureInternalLinks(string text, bool preview)
{
if (_umbracoContextAccessor.UmbracoContext == null)
throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext");
if (!preview)
return ParseInternalLinks(text);
return EnsureInternalLinks(text);
using (_umbracoContextAccessor.UmbracoContext.ForcedPreview(preview)) // force for url provider
{
return ParseInternalLinks(text);
return EnsureInternalLinks(text);
}
}
@@ -43,7 +49,7 @@ namespace Umbraco.Web.Templates
/// <param name="text"></param>
/// <param name="urlProvider"></param>
/// <returns></returns>
public string ParseInternalLinks(string text)
public string EnsureInternalLinks(string text)
{
if (_umbracoContextAccessor.UmbracoContext == null)
throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext");

View File

@@ -0,0 +1,186 @@
using HtmlAgilityPack;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Umbraco.Core;
using Umbraco.Core.Exceptions;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
namespace Umbraco.Web.Templates
{
public sealed class MediaParser
{
public MediaParser(IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider)
{
_umbracoContextAccessor = umbracoContextAccessor;
_logger = logger;
_mediaService = mediaService;
_contentTypeBaseServiceProvider = contentTypeBaseServiceProvider;
}
private static readonly Regex ResolveImgPattern = new Regex(@"(<img[^>]*src="")([^""\?]*)([^""]*""[^>]*data-udi="")([^""]*)(""[^>]*>)",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly ILogger _logger;
private readonly IMediaService _mediaService;
private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
const string TemporaryImageDataAttribute = "data-tmpimg";
/// <summary>
/// Parses the string looking for Umbraco image tags and updates them to their up-to-date image sources.
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
/// <remarks>Umbraco image tags are identified by their data-udi attributes</remarks>
public string EnsureImageSources(string text)
{
// don't attempt to proceed without a context
if (_umbracoContextAccessor?.UmbracoContext?.Media == null)
{
return text;
}
return ResolveImgPattern.Replace(text, match =>
{
// match groups:
// - 1 = from the beginning of the image tag until src attribute value begins
// - 2 = the src attribute value excluding the querystring (if present)
// - 3 = anything after group 2 and before the data-udi attribute value begins
// - 4 = the data-udi attribute value
// - 5 = anything after group 4 until the image tag is closed
var udi = match.Groups[4].Value;
if (udi.IsNullOrWhiteSpace() || GuidUdi.TryParse(udi, out var guidUdi) == false)
{
return match.Value;
}
var media = _umbracoContextAccessor?.UmbracoContext?.Media.GetById(guidUdi.Guid);
if (media == null)
{
// image does not exist - we could choose to remove the image entirely here (return empty string),
// but that would leave the editors completely in the dark as to why the image doesn't show
return match.Value;
}
var url = media.Url;
return $"{match.Groups[1].Value}{url}{match.Groups[3].Value}{udi}{match.Groups[5].Value}";
});
}
/// <summary>
/// Removes media urls from &lt;img&gt; tags where a data-udi attribute is present
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
internal string RemoveImageSources(string text)
// see comment in ResolveMediaFromTextString for group reference
=> ResolveImgPattern.Replace(text, "$1$3$4$5");
internal string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId)
{
// Find all img's that has data-tmpimg attribute
// Use HTML Agility Pack - https://html-agility-pack.net
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html);
var tmpImages = htmlDoc.DocumentNode.SelectNodes($"//img[@{TemporaryImageDataAttribute}]");
if (tmpImages == null || tmpImages.Count == 0)
return html;
// An array to contain a list of URLs that
// we have already processed to avoid dupes
var uploadedImages = new Dictionary<string, GuidUdi>();
foreach (var img in tmpImages)
{
// The data attribute contains the path to the tmp img to persist as a media item
var tmpImgPath = img.GetAttributeValue(TemporaryImageDataAttribute, string.Empty);
if (string.IsNullOrEmpty(tmpImgPath))
continue;
var absoluteTempImagePath = IOHelper.MapPath(tmpImgPath);
var fileName = Path.GetFileName(absoluteTempImagePath);
var safeFileName = fileName.ToSafeFileName();
var mediaItemName = safeFileName.ToFriendlyName();
IMedia mediaFile;
GuidUdi udi;
if (uploadedImages.ContainsKey(tmpImgPath) == false)
{
if (mediaParentFolder == Guid.Empty)
mediaFile = _mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId);
else
mediaFile = _mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId);
var fileInfo = new FileInfo(absoluteTempImagePath);
var fileStream = fileInfo.OpenReadWithRetry();
if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream");
using (fileStream)
{
mediaFile.SetValue(_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream);
}
_mediaService.Save(mediaFile, userId);
udi = mediaFile.GetUdi();
}
else
{
// Already been uploaded & we have it's UDI
udi = uploadedImages[tmpImgPath];
}
// Add the UDI to the img element as new data attribute
img.SetAttributeValue("data-udi", udi.ToString());
// Get the new persisted image url
var mediaTyped = _umbracoContextAccessor?.UmbracoContext?.Media.GetById(udi.Guid);
if (mediaTyped == null)
throw new PanicException($"Could not find media by id {udi.Guid} or there was no UmbracoContext available.");
var location = mediaTyped.Url;
// Find the width & height attributes as we need to set the imageprocessor QueryString
var width = img.GetAttributeValue("width", int.MinValue);
var height = img.GetAttributeValue("height", int.MinValue);
if (width != int.MinValue && height != int.MinValue)
{
location = $"{location}?width={width}&height={height}&mode=max";
}
img.SetAttributeValue("src", location);
// Remove the data attribute (so we do not re-process this)
img.Attributes.Remove(TemporaryImageDataAttribute);
// Add to the dictionary to avoid dupes
if (uploadedImages.ContainsKey(tmpImgPath) == false)
{
uploadedImages.Add(tmpImgPath, udi);
// Delete folder & image now its saved in media
// The folder should contain one image - as a unique guid folder created
// for each image uploaded from TinyMceController
var folderName = Path.GetDirectoryName(absoluteTempImagePath);
try
{
Directory.Delete(folderName, true);
}
catch (Exception ex)
{
_logger.Error(typeof(MediaParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath);
}
}
}
return htmlDoc.DocumentNode.OuterHtml;
}
}
}

View File

@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Umbraco.Core;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
@@ -16,19 +15,9 @@ using File = System.IO.File;
namespace Umbraco.Web.Templates
{
/// <summary>
/// Utility class used for templates
/// </summary>
[Obsolete("This class is obsolete, all methods have been moved to other classes such as InternalLinkHelper, UrlResolver and MediaParser")]
public static class TemplateUtilities
{
const string TemporaryImageDataAttribute = "data-tmpimg";
private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
private static readonly Regex ResolveImgPattern = new Regex(@"(<img[^>]*src="")([^""\?]*)([^""]*""[^>]*data-udi="")([^""]*)(""[^>]*>)",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
[Obsolete("Inject and use an instance of InternalLinkParser instead")]
internal static string ParseInternalLinks(string text, bool preview, UmbracoContext umbracoContext)
{
@@ -41,201 +30,27 @@ namespace Umbraco.Web.Templates
}
[Obsolete("Inject and use an instance of InternalLinkParser instead")]
public static string ParseInternalLinks(string text, UrlProvider urlProvider) =>
Current.Factory.GetInstance<InternalLinkParser>().ParseInternalLinks(text);
public static string ParseInternalLinks(string text, UrlProvider urlProvider)
=> Current.Factory.GetInstance<InternalLinkParser>().EnsureInternalLinks(text);
/// <summary>
/// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl to replace the tilde with the application path.
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
/// <remarks>
/// When used with a Virtual-Directory set-up, this would resolve all URLs correctly.
/// The recommendation is that the "ResolveUrlsFromTextString" option (in umbracoSettings.config) is set to false for non-Virtual-Directory installs.
/// </remarks>
[Obsolete("Inject and use an instance of UrlResolver")]
public static string ResolveUrlsFromTextString(string text)
{
if (Current.Configs.Settings().Content.ResolveUrlsFromTextString == false) return text;
using (var timer = Current.ProfilingLogger.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete"))
{
// find all relative urls (ie. urls that contain ~)
var tags = ResolveUrlPattern.Matches(text);
Current.Logger.Debug(typeof(IOHelper), "After regex: {Duration} matched: {TagsCount}", timer.Stopwatch.ElapsedMilliseconds, tags.Count);
foreach (Match tag in tags)
{
var url = "";
if (tag.Groups[1].Success)
url = tag.Groups[1].Value;
// The richtext editor inserts a slash in front of the url. That's why we need this little fix
// if (url.StartsWith("/"))
// text = text.Replace(url, ResolveUrl(url.Substring(1)));
// else
if (String.IsNullOrEmpty(url) == false)
{
var resolvedUrl = (url.Substring(0, 1) == "/") ? IOHelper.ResolveUrl(url.Substring(1)) : IOHelper.ResolveUrl(url);
text = text.Replace(url, resolvedUrl);
}
}
}
return text;
}
=> Current.Factory.GetInstance<UrlParser>().EnsureUrls(text);
[Obsolete("Use StringExtensions.CleanForXss instead")]
public static string CleanForXss(string text, params char[] ignoreFromClean)
{
return text.CleanForXss(ignoreFromClean);
}
=> text.CleanForXss(ignoreFromClean);
/// <summary>
/// Parses the string looking for Umbraco image tags and updates them to their up-to-date image sources.
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
/// <remarks>Umbraco image tags are identified by their data-udi attributes</remarks>
[Obsolete("Use MediaParser.EnsureImageSources instead")]
public static string ResolveMediaFromTextString(string text)
{
// don't attempt to proceed without a context
if (Current.UmbracoContext == null || Current.UmbracoContext.Media == null)
{
return text;
}
return ResolveImgPattern.Replace(text, match =>
{
// match groups:
// - 1 = from the beginning of the image tag until src attribute value begins
// - 2 = the src attribute value excluding the querystring (if present)
// - 3 = anything after group 2 and before the data-udi attribute value begins
// - 4 = the data-udi attribute value
// - 5 = anything after group 4 until the image tag is closed
var udi = match.Groups[4].Value;
if(udi.IsNullOrWhiteSpace() || GuidUdi.TryParse(udi, out var guidUdi) == false)
{
return match.Value;
}
var media = Current.UmbracoContext.Media.GetById(guidUdi.Guid);
if(media == null)
{
// image does not exist - we could choose to remove the image entirely here (return empty string),
// but that would leave the editors completely in the dark as to why the image doesn't show
return match.Value;
}
var url = media.Url;
return $"{match.Groups[1].Value}{url}{match.Groups[3].Value}{udi}{match.Groups[5].Value}";
});
}
/// <summary>
/// Removes media urls from &lt;img&gt; tags where a data-udi attribute is present
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
=> Current.Factory.GetInstance<MediaParser>().EnsureImageSources(text);
[Obsolete("Use MediaParser.RemoveImageSources instead")]
internal static string RemoveMediaUrlsFromTextString(string text)
// see comment in ResolveMediaFromTextString for group reference
=> ResolveImgPattern.Replace(text, "$1$3$4$5");
=> Current.Factory.GetInstance<MediaParser>().RemoveImageSources(text);
[Obsolete("Use MediaParser.RemoveImageSources instead")]
internal static string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, ILogger logger)
{
// Find all img's that has data-tmpimg attribute
// Use HTML Agility Pack - https://html-agility-pack.net
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html);
var tmpImages = htmlDoc.DocumentNode.SelectNodes($"//img[@{TemporaryImageDataAttribute}]");
if (tmpImages == null || tmpImages.Count == 0)
return html;
// An array to contain a list of URLs that
// we have already processed to avoid dupes
var uploadedImages = new Dictionary<string, GuidUdi>();
foreach (var img in tmpImages)
{
// The data attribute contains the path to the tmp img to persist as a media item
var tmpImgPath = img.GetAttributeValue(TemporaryImageDataAttribute, string.Empty);
if (string.IsNullOrEmpty(tmpImgPath))
continue;
var absoluteTempImagePath = IOHelper.MapPath(tmpImgPath);
var fileName = Path.GetFileName(absoluteTempImagePath);
var safeFileName = fileName.ToSafeFileName();
var mediaItemName = safeFileName.ToFriendlyName();
IMedia mediaFile;
GuidUdi udi;
if (uploadedImages.ContainsKey(tmpImgPath) == false)
{
if (mediaParentFolder == Guid.Empty)
mediaFile = mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId);
else
mediaFile = mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId);
var fileInfo = new FileInfo(absoluteTempImagePath);
var fileStream = fileInfo.OpenReadWithRetry();
if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream");
using (fileStream)
{
mediaFile.SetValue(contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream);
}
mediaService.Save(mediaFile, userId);
udi = mediaFile.GetUdi();
}
else
{
// Already been uploaded & we have it's UDI
udi = uploadedImages[tmpImgPath];
}
// Add the UDI to the img element as new data attribute
img.SetAttributeValue("data-udi", udi.ToString());
// Get the new persisted image url
var mediaTyped = Current.UmbracoHelper.Media(udi.Guid);
var location = mediaTyped.Url;
// Find the width & height attributes as we need to set the imageprocessor QueryString
var width = img.GetAttributeValue("width", int.MinValue);
var height = img.GetAttributeValue("height", int.MinValue);
if(width != int.MinValue && height != int.MinValue)
{
location = $"{location}?width={width}&height={height}&mode=max";
}
img.SetAttributeValue("src", location);
// Remove the data attribute (so we do not re-process this)
img.Attributes.Remove(TemporaryImageDataAttribute);
// Add to the dictionary to avoid dupes
if(uploadedImages.ContainsKey(tmpImgPath) == false)
{
uploadedImages.Add(tmpImgPath, udi);
// Delete folder & image now its saved in media
// The folder should contain one image - as a unique guid folder created
// for each image uploaded from TinyMceController
var folderName = Path.GetDirectoryName(absoluteTempImagePath);
try
{
Directory.Delete(folderName, true);
}
catch (Exception ex)
{
logger.Error(typeof(TemplateUtilities), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath);
}
}
}
return htmlDoc.DocumentNode.OuterHtml;
}
=> Current.Factory.GetInstance<MediaParser>().FindAndPersistPastedTempImages(html, mediaParentFolder, userId);
}
}

View File

@@ -0,0 +1,61 @@
using System.Text.RegularExpressions;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
namespace Umbraco.Web.Templates
{
public sealed class UrlParser
{
private readonly IContentSection _contentSection;
private readonly IProfilingLogger _logger;
private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
public UrlParser(IContentSection contentSection, IProfilingLogger logger)
{
_contentSection = contentSection;
_logger = logger;
}
/// <summary>
/// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl to replace the tilde with the application path.
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
/// <remarks>
/// When used with a Virtual-Directory set-up, this would resolve all URLs correctly.
/// The recommendation is that the "ResolveUrlsFromTextString" option (in umbracoSettings.config) is set to false for non-Virtual-Directory installs.
/// </remarks>
public string EnsureUrls(string text)
{
if (_contentSection.ResolveUrlsFromTextString == false) return text;
using (var timer = _logger.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete"))
{
// find all relative urls (ie. urls that contain ~)
var tags = ResolveUrlPattern.Matches(text);
_logger.Debug(typeof(IOHelper), "After regex: {Duration} matched: {TagsCount}", timer.Stopwatch.ElapsedMilliseconds, tags.Count);
foreach (Match tag in tags)
{
var url = "";
if (tag.Groups[1].Success)
url = tag.Groups[1].Value;
// The richtext editor inserts a slash in front of the url. That's why we need this little fix
// if (url.StartsWith("/"))
// text = text.Replace(url, ResolveUrl(url.Substring(1)));
// else
if (string.IsNullOrEmpty(url) == false)
{
var resolvedUrl = (url.Substring(0, 1) == "/") ? IOHelper.ResolveUrl(url.Substring(1)) : IOHelper.ResolveUrl(url);
text = text.Replace(url, resolvedUrl);
}
}
}
return text;
}
}
}

View File

@@ -248,6 +248,8 @@
<Compile Include="Models\LinkType.cs" />
<Compile Include="Models\TemplateQuery\OperatorFactory.cs" />
<Compile Include="Templates\InternalLinkParser.cs" />
<Compile Include="Templates\MediaParser.cs" />
<Compile Include="Templates\UrlParser.cs" />
<Compile Include="UmbracoContextFactory.cs" />
<Compile Include="UmbracoContextReference.cs" />
<Compile Include="ViewDataExtensions.cs" />

View File

@@ -159,7 +159,7 @@ namespace Umbraco.Web
_umbracoContextAccessor.UmbracoContext.HttpContext.Response.ContentType = contentType;
//Now, we need to ensure that local links are parsed
html = _internalLinkParser.ParseInternalLinks(output.ToString());
html = _internalLinkParser.EnsureInternalLinks(output.ToString());
}
}