Files
Umbraco-CMS/src/Umbraco.Core/PublishedContentHelper.cs

162 lines
6.6 KiB
C#
Raw Normal View History

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Umbraco.Core.Configuration;
using Umbraco.Core.Dynamics;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
namespace Umbraco.Core
{
/// <summary>
/// Utility class for dealing with data types and value conversions
/// </summary>
/// <remarks>
/// TODO: The logic for the GetDataType + cache should probably be moved to a service, no ?
///
/// We inherit from ApplicationEventHandler so we can bind to the ContentTypeService events to ensure that our local cache
/// object gets cleared when content types change.
/// </remarks>
internal class PublishedContentHelper : ApplicationEventHandler
{
#region event handlers to ensure that the cache is cleared when content types change
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
ContentTypeService.SavedContentType += ContentTypeServiceSavedContentType;
ContentTypeService.SavedMediaType += ContentTypeServiceSavedMediaType;
ContentTypeService.DeletedContentType += ContentTypeServiceDeletedContentType;
ContentTypeService.DeletedMediaType += ContentTypeServiceDeletedMediaType;
}
static void ContentTypeServiceDeletedMediaType(IContentTypeService sender, Events.DeleteEventArgs<Models.IMediaType> e)
{
PropertyTypeCache.Clear();
}
static void ContentTypeServiceDeletedContentType(IContentTypeService sender, Events.DeleteEventArgs<Models.IContentType> e)
{
PropertyTypeCache.Clear();
}
static void ContentTypeServiceSavedMediaType(IContentTypeService sender, Events.SaveEventArgs<Models.IMediaType> e)
{
PropertyTypeCache.Clear();
}
static void ContentTypeServiceSavedContentType(IContentTypeService sender, Events.SaveEventArgs<Models.IContentType> e)
{
PropertyTypeCache.Clear();
}
#endregion
/// <summary>
/// This callback is used only for unit tests which enables us to return any data we want and not rely on having the data in a database
/// </summary>
internal static Func<string, string, Guid> GetDataTypeCallback = null;
private static readonly ConcurrentDictionary<Tuple<string, string>, Guid> PropertyTypeCache = new ConcurrentDictionary<Tuple<string, string>, Guid>();
/// <summary>
/// Return the GUID Id for the data type assigned to the document type with the property alias
/// </summary>
/// <param name="applicationContext"></param>
/// <param name="docTypeAlias"></param>
/// <param name="propertyAlias"></param>
/// <returns></returns>
internal static Guid GetDataType(ApplicationContext applicationContext, string docTypeAlias, string propertyAlias)
{
if (GetDataTypeCallback != null)
return GetDataTypeCallback(docTypeAlias, propertyAlias);
var key = new Tuple<string, string>(docTypeAlias, propertyAlias);
return PropertyTypeCache.GetOrAdd(key, tuple =>
{
var result = applicationContext.Services.ContentTypeService.GetContentType(docTypeAlias);
if (result == null) return Guid.Empty;
var property = result.PropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyAlias));
if (property == null) return Guid.Empty;
return property.DataTypeId;
});
}
/// <summary>
/// Converts the currentValue to a correctly typed value based on known registered converters, then based on known standards.
/// </summary>
/// <param name="currentValue"></param>
/// <param name="dataType"></param>
/// <param name="docTypeAlias"></param>
/// <param name="propertyTypeAlias"></param>
/// <returns></returns>
internal static Attempt<object> ConvertPropertyValue(object currentValue, Guid dataType, string docTypeAlias, string propertyTypeAlias)
{
if (currentValue == null) return Attempt<object>.False;
//First lets check all registered converters for this data type.
var converters = PropertyEditorValueConvertersResolver.Current.Converters
.Where(x => x.IsConverterFor(dataType, docTypeAlias, propertyTypeAlias))
.ToArray();
//try to convert the value with any of the converters:
foreach (var converted in converters
.Select(p => p.ConvertPropertyValue(currentValue))
.Where(converted => converted.Success))
{
return new Attempt<object>(true, converted.Result);
}
//if none of the converters worked, then we'll process this from what we know
var sResult = Convert.ToString(currentValue).Trim();
//this will eat csv strings, so only do it if the decimal also includes a decimal seperator (according to the current culture)
if (sResult.Contains(System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator))
{
decimal dResult;
if (decimal.TryParse(sResult, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.CurrentCulture, out dResult))
{
return new Attempt<object>(true, dResult);
}
}
//process string booleans as booleans
if (sResult.InvariantEquals("true"))
{
return new Attempt<object>(true, true);
}
if (sResult.InvariantEquals("false"))
{
return new Attempt<object>(true, false);
}
//a really rough check to see if this may be valid xml
//TODO: This is legacy code, I'm sure there's a better and nicer way
if (sResult.StartsWith("<") && sResult.EndsWith(">") && sResult.Contains("/"))
{
try
{
var e = XElement.Parse(sResult, LoadOptions.None);
//check that the document element is not one of the disallowed elements
//allows RTE to still return as html if it's valid xhtml
var documentElement = e.Name.LocalName;
//TODO: See note against this setting, pretty sure we don't need this
if (!UmbracoSettings.NotDynamicXmlDocumentElements.Any(
tag => string.Equals(tag, documentElement, StringComparison.CurrentCultureIgnoreCase)))
{
return new Attempt<object>(true, new DynamicXml(e));
}
return Attempt<object>.False;
}
catch (Exception)
{
return Attempt<object>.False;
}
}
return Attempt<object>.False;
}
}
}