Manifest refactoring

This commit is contained in:
Stephan
2018-01-19 19:08:12 +01:00
parent 506d810cf6
commit eff2e7a65b
31 changed files with 660 additions and 1318 deletions

View File

@@ -32,7 +32,8 @@ namespace Umbraco.Core.Configuration.Grid
{
Func<List<GridEditor>> getResult = () =>
{
var parser = new ManifestParser(_logger, _appPlugins, _runtimeCache);
// fixme - should use the common one somehow! + ignoring _appPlugins here!
var parser = new ManifestParser(_runtimeCache, _logger);
var editors = new List<GridEditor>();
var gridConfig = Path.Combine(_configFolder.FullName, "grid.editors.config.js");
@@ -40,10 +41,7 @@ namespace Umbraco.Core.Configuration.Grid
{
try
{
var arr = JArray.Parse(File.ReadAllText(gridConfig));
//ensure the contents parse correctly to objects
var parsed = parser.GetGridEditors(arr);
editors.AddRange(parsed);
editors.AddRange(parser.ParseGridEditors(File.ReadAllText(gridConfig)));
}
catch (Exception ex)
{
@@ -51,16 +49,13 @@ namespace Umbraco.Core.Configuration.Grid
}
}
var builder = new ManifestBuilder(_runtimeCache, parser);
foreach (var gridEditor in builder.GridEditors)
// add manifest editors, skip duplicates
foreach (var gridEditor in parser.Manifest.GridEditors)
{
//no duplicates! (based on alias)
if (editors.Contains(gridEditor) == false)
{
editors.Add(gridEditor);
}
}
return editors;
};

View File

@@ -20,10 +20,7 @@ namespace Umbraco.Core.Configuration
private static readonly Lazy<UmbracoConfig> Lazy = new Lazy<UmbracoConfig>(() => new UmbracoConfig());
public static UmbracoConfig For
{
get { return Lazy.Value; }
}
public static UmbracoConfig For => Lazy.Value;
#endregion

View File

@@ -44,6 +44,12 @@ namespace Umbraco.Core.IO
return retval;
}
public static string ResolveVirtualUrl(string path)
{
if (string.IsNullOrWhiteSpace(path)) return path;
return path.StartsWith("~/") ? ResolveUrl(path) : path;
}
//Replaces tildes with the root dir
public static string ResolveUrl(string virtualPath)
{

View File

@@ -1,35 +0,0 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.IO;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Serialization;
namespace Umbraco.Core.Manifest
{
/// <summary>
/// Ensures that virtual paths are taken care of
/// </summary>
internal class GridEditorConverter : JsonCreationConverter<GridEditor>
{
protected override GridEditor Create(Type objectType, JObject jObject)
{
return new GridEditor();
}
protected override void Deserialize(JObject jObject, GridEditor target, JsonSerializer serializer)
{
base.Deserialize(jObject, target, serializer);
if (target.View.IsNullOrWhiteSpace() == false && target.View.StartsWith("~/"))
{
target.View = IOHelper.ResolveUrl(target.View);
}
if (target.Render.IsNullOrWhiteSpace() == false && target.Render.StartsWith("~/"))
{
target.Render = IOHelper.ResolveUrl(target.Render);
}
}
}
}

View File

@@ -1,105 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Umbraco.Core.Cache;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.PropertyEditors;
namespace Umbraco.Core.Manifest
{
/// <summary>
/// This reads in the manifests and stores some definitions in memory so we can look them on the server side
/// </summary>
internal class ManifestBuilder
{
private readonly IRuntimeCacheProvider _cache;
private readonly ManifestParser _parser;
public ManifestBuilder(IRuntimeCacheProvider cache, ManifestParser parser)
{
_cache = cache;
_parser = parser;
}
private const string GridEditorsKey = "grideditors";
private const string PropertyEditorsKey = "propertyeditors";
private const string ParameterEditorsKey = "parametereditors";
/// <summary>
/// Returns all grid editors found in the manfifests
/// </summary>
internal IEnumerable<GridEditor> GridEditors
{
get
{
return _cache.GetCacheItem<IEnumerable<GridEditor>>(
typeof (ManifestBuilder) + GridEditorsKey,
() =>
{
var editors = new List<GridEditor>();
foreach (var manifest in _parser.GetManifests())
{
if (manifest.GridEditors != null)
{
editors.AddRange(_parser.GetGridEditors(manifest.GridEditors));
}
}
return editors;
}, new TimeSpan(0, 10, 0));
}
}
/// <summary>
/// Returns all property editors found in the manfifests
/// </summary>
internal IEnumerable<PropertyEditor> PropertyEditors
{
get
{
return _cache.GetCacheItem<IEnumerable<PropertyEditor>>(
typeof(ManifestBuilder) + PropertyEditorsKey,
() =>
{
var editors = new List<PropertyEditor>();
foreach (var manifest in _parser.GetManifests())
{
if (manifest.PropertyEditors != null)
{
editors.AddRange(_parser.GetPropertyEditors(manifest.PropertyEditors));
}
}
return editors;
}, new TimeSpan(0, 10, 0));
}
}
/// <summary>
/// Returns all parameter editors found in the manfifests and all property editors that are flagged to be parameter editors
/// </summary>
internal IEnumerable<ParameterEditor> ParameterEditors
{
get
{
return _cache.GetCacheItem<IEnumerable<ParameterEditor>>(
typeof (ManifestBuilder) + ParameterEditorsKey,
() =>
{
var editors = new List<ParameterEditor>();
foreach (var manifest in _parser.GetManifests())
{
if (manifest.ParameterEditors != null)
{
editors.AddRange(_parser.GetParameterEditors(manifest.ParameterEditors));
}
}
return editors;
}, new TimeSpan(0, 10, 0));
}
}
}
}

View File

@@ -3,10 +3,9 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Cache;
using Umbraco.Core.Exceptions;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.PropertyEditors;
@@ -16,345 +15,139 @@ namespace Umbraco.Core.Manifest
/// <summary>
/// Parses the Main.js file and replaces all tokens accordingly.
/// </summary>
internal class ManifestParser
public class ManifestParser
{
private readonly IRuntimeCacheProvider _cache;
private readonly string _path;
private readonly ILogger _logger;
private readonly DirectoryInfo _pluginsDir;
private readonly IRuntimeCacheProvider _cache;
private static readonly string Utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble());
//used to strip comments
private static readonly Regex CommentsSurround = new Regex(@"/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/", RegexOptions.Compiled);
private static readonly Regex CommentsLine = new Regex(@"^\s*//.*?$", RegexOptions.Compiled | RegexOptions.Multiline);
/// <summary>
/// Initializes a new instance of the <see cref="ManifestParser"/> class.
/// </summary>
public ManifestParser(IRuntimeCacheProvider cache, ILogger logger) // fixme is LightInject going to pick that one?
: this(cache, "~/App_Plugins", logger)
{ }
public ManifestParser(ILogger logger, DirectoryInfo pluginsDir, IRuntimeCacheProvider cache)
/// <summary>
/// Initializes a new instance of the <see cref="ManifestParser"/> class.
/// </summary>
public ManifestParser(IRuntimeCacheProvider cache, string path, ILogger logger)
{
if (logger == null) throw new ArgumentNullException("logger");
if (pluginsDir == null) throw new ArgumentNullException("pluginsDir");
_pluginsDir = pluginsDir;
_logger = logger;
_cache = cache;
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullOrEmptyException(nameof(path));
_path = path.StartsWith("~/") ? IOHelper.MapPath(path) : path;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Parse the grid editors from the json array
/// </summary>
/// <param name="jsonEditors"></param>
/// <returns></returns>
internal IEnumerable<GridEditor> GetGridEditors(JArray jsonEditors)
{
return JsonConvert.DeserializeObject<IEnumerable<GridEditor>>(
jsonEditors.ToString(),
new GridEditorConverter());
}
/// <summary>
/// Parse the property editors from the json array
/// </summary>
/// <param name="jsonEditors"></param>
/// <returns></returns>
internal IEnumerable<PropertyEditor> GetPropertyEditors(JArray jsonEditors)
{
return JsonConvert.DeserializeObject<IEnumerable<PropertyEditor>>(
jsonEditors.ToString(),
new PropertyEditorConverter(_logger),
new PreValueFieldConverter());
}
/// <summary>
/// Parse the property editors from the json array
/// </summary>
/// <param name="jsonEditors"></param>
/// <returns></returns>
internal IEnumerable<ParameterEditor> GetParameterEditors(JArray jsonEditors)
{
return JsonConvert.DeserializeObject<IEnumerable<ParameterEditor>>(
jsonEditors.ToString(),
new ParameterEditorConverter());
}
/// <summary>
/// Get all registered manifests
/// Gets all manifests, merged into a single manifest object.
/// </summary>
/// <returns></returns>
/// <remarks>
/// This ensures that we only build and look for all manifests once per Web app (based on the IRuntimeCache)
/// </remarks>
public IEnumerable<PackageManifest> GetManifests()
{
return _cache.GetCacheItem<IEnumerable<PackageManifest>>(typeof (ManifestParser) + "GetManifests", () =>
public PackageManifest Manifest
=> _cache.GetCacheItem<PackageManifest>("Umbraco.Core.Manifest.ManifestParser::Manifests", () =>
{
//get all Manifest.js files in the appropriate folders
var manifestFileContents = GetAllManifestFileContents(_pluginsDir);
return CreateManifests(manifestFileContents.ToArray());
}, new TimeSpan(0, 10, 0));
}
var manifests = GetManifests();
return MergeManifests(manifests);
});
/// <summary>
/// Get the file contents from all declared manifest files
/// Gets all manifests.
/// </summary>
/// <param name="currDir"></param>
/// <returns></returns>
private IEnumerable<string> GetAllManifestFileContents(DirectoryInfo currDir)
private IEnumerable<PackageManifest> GetManifests()
{
var depth = FolderDepth(_pluginsDir, currDir);
var manifests = new List<PackageManifest>();
if (depth < 1)
foreach (var path in GetManifestFiles())
{
var result = new List<string>();
if (currDir.Exists)
{
var dirs = currDir.GetDirectories();
foreach (var d in dirs)
{
result.AddRange(GetAllManifestFileContents(d));
}
}
return result;
}
//look for files here
return currDir.GetFiles("Package.manifest")
.Select(f => File.ReadAllText(f.FullName))
.ToList();
}
/// <summary>
/// Get the folder depth compared to the base folder
/// </summary>
/// <param name="baseDir"></param>
/// <param name="currDir"></param>
/// <returns></returns>
internal int FolderDepth(DirectoryInfo baseDir, DirectoryInfo currDir)
{
var removed = currDir.FullName.Remove(0, baseDir.FullName.Length).TrimStart('\\').TrimEnd('\\');
return removed.Split(new char[] {'\\'}, StringSplitOptions.RemoveEmptyEntries).Length;
}
/// <summary>
/// Creates a list of PropertyEditorManifest from the file contents of each manifest file
/// </summary>
/// <param name="manifestFileContents"></param>
/// <returns></returns>
/// <remarks>
/// This ensures that comments are removed (but they have to be /* */ style comments
/// and ensures that virtual paths are replaced with real ones
/// </remarks>
internal IEnumerable<PackageManifest> CreateManifests(params string[] manifestFileContents)
{
var result = new List<PackageManifest>();
foreach (var m in manifestFileContents)
{
var manifestContent = m;
if (manifestContent.IsNullOrWhiteSpace()) continue;
// Strip byte object marker, JSON.NET does not like it
var preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble());
// Strangely StartsWith(preamble) would always return true
if (manifestContent.Substring(0, 1) == preamble)
manifestContent = manifestContent.Remove(0, preamble.Length);
if (manifestContent.IsNullOrWhiteSpace()) continue;
//remove any comments first
var replaced = CommentsSurround.Replace(manifestContent, match => " ");
replaced = CommentsLine.Replace(replaced, match => "");
JObject deserialized;
try
{
deserialized = JsonConvert.DeserializeObject<JObject>(replaced);
var text = File.ReadAllText(path);
text = TrimPreamble(text);
if (string.IsNullOrWhiteSpace(text))
continue;
var manifest = ParseManifest(text);
manifests.Add(manifest);
}
catch (Exception ex)
catch (Exception e)
{
_logger.Error<ManifestParser>("An error occurred parsing manifest with contents: " + m, ex);
continue;
_logger.Error<ManifestParser>($"Failed to parse manifest at \"{path}\", ignoring.", e);
}
//validate the javascript
var init = deserialized.Properties().Where(x => x.Name == "javascript").ToArray();
if (init.Length > 1)
{
throw new FormatException("The manifest is not formatted correctly contains more than one 'javascript' element");
}
//validate the css
var cssinit = deserialized.Properties().Where(x => x.Name == "css").ToArray();
if (cssinit.Length > 1)
{
throw new FormatException("The manifest is not formatted correctly contains more than one 'css' element");
}
//validate the property editors section
var propEditors = deserialized.Properties().Where(x => x.Name == "propertyEditors").ToArray();
if (propEditors.Length > 1)
{
throw new FormatException("The manifest is not formatted correctly contains more than one 'propertyEditors' element");
}
//validate the parameterEditors section
var paramEditors = deserialized.Properties().Where(x => x.Name == "parameterEditors").ToArray();
if (paramEditors.Length > 1)
{
throw new FormatException("The manifest is not formatted correctly contains more than one 'parameterEditors' element");
}
//validate the gridEditors section
var gridEditors = deserialized.Properties().Where(x => x.Name == "gridEditors").ToArray();
if (gridEditors.Length > 1)
{
throw new FormatException("The manifest is not formatted correctly contains more than one 'gridEditors' element");
}
var jConfig = init.Any() ? (JArray)deserialized["javascript"] : new JArray();
ReplaceVirtualPaths(jConfig);
var cssConfig = cssinit.Any() ? (JArray)deserialized["css"] : new JArray();
ReplaceVirtualPaths(cssConfig);
//replace virtual paths for each property editor
if (deserialized["propertyEditors"] != null)
{
foreach (JObject p in deserialized["propertyEditors"])
{
if (p["editor"] != null)
{
ReplaceVirtualPaths((JObject) p["editor"]);
}
if (p["preValues"] != null)
{
ReplaceVirtualPaths((JObject)p["preValues"]);
}
}
}
//replace virtual paths for each property editor
if (deserialized["gridEditors"] != null)
{
foreach (JObject p in deserialized["gridEditors"])
{
if (p["view"] != null)
{
ReplaceVirtualPaths(p["view"]);
}
if (p["render"] != null)
{
ReplaceVirtualPaths(p["render"]);
}
}
}
var manifest = new PackageManifest()
{
JavaScriptInitialize = jConfig,
StylesheetInitialize = cssConfig,
PropertyEditors = propEditors.Any() ? (JArray)deserialized["propertyEditors"] : new JArray(),
ParameterEditors = paramEditors.Any() ? (JArray)deserialized["parameterEditors"] : new JArray(),
GridEditors = gridEditors.Any() ? (JArray)deserialized["gridEditors"] : new JArray()
};
result.Add(manifest);
}
return result;
return manifests;
}
/// <summary>
/// Replaces any virtual paths found in properties
/// Merges all manifests into one.
/// </summary>
/// <param name="jarr"></param>
private void ReplaceVirtualPaths(JArray jarr)
private static PackageManifest MergeManifests(IEnumerable<PackageManifest> manifests)
{
foreach (var i in jarr)
var scripts = new HashSet<string>();
var stylesheets = new HashSet<string>();
var propertyEditors = new List<PropertyEditor>();
var parameterEditors = new List<ParameterEditor>();
var gridEditors = new List<GridEditor>();
foreach (var manifest in manifests)
{
ReplaceVirtualPaths(i);
if (manifest.Scripts != null) foreach (var script in manifest.Scripts) scripts.Add(script);
if (manifest.Stylesheets != null) foreach (var stylesheet in manifest.Stylesheets) stylesheets.Add(stylesheet);
if (manifest.PropertyEditors != null) propertyEditors.AddRange(manifest.PropertyEditors);
if (manifest.ParameterEditors != null) parameterEditors.AddRange(manifest.ParameterEditors);
if (manifest.GridEditors != null) gridEditors.AddRange(manifest.GridEditors);
}
return new PackageManifest
{
Scripts = scripts.ToArray(),
Stylesheets = stylesheets.ToArray(),
PropertyEditors = propertyEditors.ToArray(),
ParameterEditors = parameterEditors.ToArray(),
GridEditors = gridEditors.ToArray()
};
}
// gets all manifest files (recursively)
private IEnumerable<string> GetManifestFiles()
=> Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories);
private static string TrimPreamble(string text)
{
// strangely StartsWith(preamble) would always return true
if (text.Substring(0, 1) == Utf8Preamble)
text = text.Remove(0, Utf8Preamble.Length);
return text;
}
/// <summary>
/// Replaces any virtual paths found in properties
/// Parses a manifest.
/// </summary>
/// <param name="jToken"></param>
private void ReplaceVirtualPaths(JToken jToken)
internal PackageManifest ParseManifest(string text)
{
if (jToken.Type == JTokenType.Object)
{
//recurse
ReplaceVirtualPaths((JObject)jToken);
}
else
{
var value = jToken as JValue;
if (value != null)
{
if (value.Type == JTokenType.String)
{
if (value.Value<string>().StartsWith("~/"))
{
//replace the virtual path
value.Value = IOHelper.ResolveUrl(value.Value<string>());
}
}
}
}
if (string.IsNullOrWhiteSpace(text))
throw new ArgumentNullOrEmptyException(nameof(text));
var manifest = JsonConvert.DeserializeObject<PackageManifest>(text,
new PropertyEditorConverter(_logger),
new ParameterEditorConverter(),
new ManifestValidatorConverter());
// scripts and stylesheets are raw string, must process here
for (var i = 0; i < manifest.Scripts.Length; i++)
manifest.Scripts[i] = IOHelper.ResolveVirtualUrl(manifest.Scripts[i]);
for (var i = 0; i < manifest.Stylesheets.Length; i++)
manifest.Stylesheets[i] = IOHelper.ResolveVirtualUrl(manifest.Stylesheets[i]);
return manifest;
}
/// <summary>
/// Replaces any virtual paths found in properties
/// </summary>
/// <param name="jObj"></param>
private void ReplaceVirtualPaths(JObject jObj)
// purely for tests
internal IEnumerable<GridEditor> ParseGridEditors(string text)
{
foreach (var p in jObj.Properties().Select(x => x.Value))
{
ReplaceVirtualPaths(p);
}
return JsonConvert.DeserializeObject<IEnumerable<GridEditor>>(text);
}
/// <summary>
/// Merges two json objects together
/// </summary>
/// <param name="receiver"></param>
/// <param name="donor"></param>
/// <param name="keepOriginal">set to true if we will keep the receiver value if the proeprty already exists</param>
/// <remarks>
/// taken from
/// http://stackoverflow.com/questions/4002508/does-c-sharp-have-a-library-for-parsing-multi-level-cascading-json/4002550#4002550
/// </remarks>
internal static void MergeJObjects(JObject receiver, JObject donor, bool keepOriginal = false)
{
foreach (var property in donor)
{
var receiverValue = receiver[property.Key] as JObject;
var donorValue = property.Value as JObject;
if (receiverValue != null && donorValue != null)
{
MergeJObjects(receiverValue, donorValue);
}
else if (receiver[property.Key] == null || !keepOriginal)
{
receiver[property.Key] = property.Value;
}
}
}
/// <summary>
/// Merges the donor array values into the receiver array
/// </summary>
/// <param name="receiver"></param>
/// <param name="donor"></param>
internal static void MergeJArrays(JArray receiver, JArray donor)
{
foreach (var item in donor)
{
if (!receiver.Any(x => x.Equals(item)))
{
receiver.Add(item);
}
}
}
}
}

View File

@@ -6,13 +6,13 @@ using Umbraco.Core.Serialization;
namespace Umbraco.Core.Manifest
{
/// <summary>
/// Used when deserialing the validation collection, any serialized property editors are from a manifest and thus the
/// validators are manifest validators.
/// Implements a json read converter for <see cref="IPropertyValidator"/>.
/// </summary>
internal class ManifestValidatorConverter : JsonCreationConverter<IPropertyValidator>
internal class ManifestValidatorConverter : JsonReadConverter<IPropertyValidator>
{
protected override IPropertyValidator Create(Type objectType, JObject jObject)
{
// all validators coming from manifests are ManifestPropertyValidator instances
return new ManifestPropertyValidator();
}
}

View File

@@ -1,35 +1,27 @@
using Newtonsoft.Json.Linq;
using System;
using Newtonsoft.Json;
using Umbraco.Core.PropertyEditors;
namespace Umbraco.Core.Manifest
{
/// <summary>
/// Represents a manifest file for packages
/// Represents the content of a package manifest.
/// </summary>
internal class PackageManifest
public class PackageManifest
{
/// <summary>
/// The json array used to initialize the application with the JS dependencies required
/// </summary>
public JArray JavaScriptInitialize { get; set; }
[JsonProperty("javascript")]
public string[] Scripts { get; set; } = Array.Empty<string>();
/// <summary>
/// The json array used to initialize the application with the CSS dependencies required
/// </summary>
public JArray StylesheetInitialize { get; set; }
[JsonProperty("css")]
public string[] Stylesheets { get; set; }= Array.Empty<string>();
/// <summary>
/// The json array of property editors
/// </summary>
public JArray PropertyEditors { get; set; }
[JsonProperty("propertyEditors")]
public PropertyEditor[] PropertyEditors { get; set; } = Array.Empty<PropertyEditor>();
/// <summary>
/// The json array of parameter editors
/// </summary>
public JArray ParameterEditors { get; set; }
[JsonProperty("parameterEditors")]
public ParameterEditor[] ParameterEditors { get; set; } = Array.Empty<ParameterEditor>();
/// <summary>
/// The json array of grid editors
/// </summary>
public JArray GridEditors { get; set; }
[JsonProperty("gridEditors")]
public GridEditor[] GridEditors { get; set; } = Array.Empty<GridEditor>();
}
}

View File

@@ -7,25 +7,33 @@ using Umbraco.Core.Serialization;
namespace Umbraco.Core.Manifest
{
/// <summary>
/// Used to convert a parameter editor manifest to a property editor object
/// Implements a json read converter for <see cref="ParameterEditor"/>.
/// </summary>
internal class ParameterEditorConverter : JsonCreationConverter<ParameterEditor>
internal class ParameterEditorConverter : JsonReadConverter<ParameterEditor>
{
/// <inheritdoc />
protected override ParameterEditor Create(Type objectType, JObject jObject)
{
return new ParameterEditor();
}
protected override void Deserialize(JObject jObject, ParameterEditor target, JsonSerializer serializer)
/// <inheritdoc />
protected override void Deserialize(JObject jobject, ParameterEditor target, JsonSerializer serializer)
{
//since it's a manifest editor, we need to create it's instance.
//we need to specify the view value for the editor here otherwise we'll get an exception.
target.ManifestDefinedParameterValueEditor = new ParameterValueEditor
if (jobject.Property("view") != null)
{
View = jObject["view"].ToString()
};
// the deserializer will first try to get the property, and that would throw since
// the editor would try to create a new value editor, so we have to set a
// value editor by ourselves, which will then be populated by the deserializer.
target.ValueEditor = new ParameterValueEditor();
base.Deserialize(jObject, target, serializer);
// the 'view' property in the manifest is at top-level, and needs to be moved
// down one level to the actual value editor.
jobject["editor"] = new JObject { ["view"] = jobject["view"] };
jobject.Property("view").Remove();
}
base.Deserialize(jobject, target, serializer);
}
}
}

View File

@@ -1,18 +0,0 @@
using System;
using Newtonsoft.Json.Linq;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Serialization;
namespace Umbraco.Core.Manifest
{
/// <summary>
/// Used to convert a pre-value field manifest to a real pre value field
/// </summary>
internal class PreValueFieldConverter : JsonCreationConverter<PreValueField>
{
protected override PreValueField Create(Type objectType, JObject jObject)
{
return new PreValueField();
}
}
}

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Logging;
@@ -10,89 +8,74 @@ using Umbraco.Core.Serialization;
namespace Umbraco.Core.Manifest
{
/// <summary>
/// Used to convert a property editor manifest to a property editor object
/// Implements a json read converter for <see cref="PropertyEditor"/>.
/// </summary>
internal class PropertyEditorConverter : JsonCreationConverter<PropertyEditor>
internal class PropertyEditorConverter : JsonReadConverter<PropertyEditor>
{
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="PropertyEditorConverter"/> class.
/// </summary>
public PropertyEditorConverter(ILogger logger)
{
_logger = logger;
}
/// <inheritdoc />
protected override PropertyEditor Create(Type objectType, JObject jObject)
{
return new PropertyEditor(_logger);
}
protected override void Deserialize(JObject jObject, PropertyEditor target, JsonSerializer serializer)
/// <inheritdoc />
protected override void Deserialize(JObject jobject, PropertyEditor target, JsonSerializer serializer)
{
if (jObject["editor"] != null)
if (jobject["editor"] != null)
{
//since it's a manifest editor, we need to create it's instance.
//we need to specify the view value for the editor here otherwise we'll get an exception.
target.ManifestDefinedPropertyValueEditor = new PropertyValueEditor
{
View = jObject["editor"]["view"].ToString()
};
//the manifest JSON is a simplified json for the validators which is actually a dictionary, however, the
//c# model requires an array of validators not a dictionary so we need to change the json to an array
//to deserialize properly.
JArray converted;
if (TryConvertValidatorDictionaryToArray(jObject["editor"]["validation"] as JObject, out converted))
{
jObject["editor"]["validation"] = converted;
}
// the deserializer will first try to get the property, and that would throw since
// the editor would try to create a new value editor, so we have to set a
// value editor by ourselves, which will then be populated by the deserializer.
target.ValueEditor = new PropertyValueEditor();
// in the manifest, validators are a simple dictionary eg
// {
// required: true,
// regex: '\\d*'
// }
// and we need to turn this into a list of IPropertyValidator
// so, rewrite the json structure accordingly
if (jobject["editor"]["validation"] is JObject validation)
jobject["editor"]["validation"] = RewriteValidators(validation);
}
if (jObject["prevalues"] != null)
{
target.ManifestDefinedPreValueEditor = new PreValueEditor();
//the manifest JSON is a simplified json for the validators which is actually a dictionary, however, the
//c# model requires an array of validators not a dictionary so we need to change the json to an array
//to deserialize properly.
var fields = jObject["prevalues"]["fields"] as JArray;
if (fields != null)
// see note about validators, above - same applies to field validators
if (jobject["prevalues"]?["fields"] is JArray jarray)
{
foreach (var field in jarray)
{
foreach (var f in fields)
{
JArray converted;
if (TryConvertValidatorDictionaryToArray(f["validation"] as JObject, out converted))
{
f["validation"] = converted;
}
}
// see note above, for editor
if (field["validation"] is JObject validation)
field["validation"] = RewriteValidators(validation);
}
}
base.Deserialize(jObject, target, serializer);
base.Deserialize(jobject, target, serializer);
}
private bool TryConvertValidatorDictionaryToArray(JObject validation, out JArray result)
private static JArray RewriteValidators(JObject validation)
{
if (validation == null)
var jarray = new JArray();
foreach (var v in validation)
{
result = null;
return false;
var key = v.Key;
var val = v.Value?.Type == JTokenType.Boolean ? string.Empty : v.Value;
var jo = new JObject { { "type", key }, { "config", val } };
jarray.Add(jo);
}
result = new JArray();
foreach (var entry in validation)
{
//in a special case if the value is simply 'true' (boolean) this just indicates that the
// validator is enabled, the config should just be empty.
var formattedItem = JObject.FromObject(new { type = entry.Key, config = entry.Value });
if (entry.Value.Type == JTokenType.Boolean)
{
formattedItem["config"] = "";
}
result.Add(formattedItem);
}
return true;
return jarray;
}
}
}

View File

@@ -1,11 +1,15 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using Umbraco.Core.Configuration.Grid;
using Umbraco.Core.IO;
namespace Umbraco.Core.PropertyEditors
{
internal class GridEditor : IGridEditorConfig
public class GridEditor : IGridEditorConfig
{
private string _view;
private string _render;
public GridEditor()
{
Config = new Dictionary<string, object>();
@@ -18,10 +22,18 @@ namespace Umbraco.Core.PropertyEditors
public string Alias { get; set; }
[JsonProperty("view", Required = Required.Always)]
public string View { get; set; }
public string View
{
get => _view;
set => _view = IOHelper.ResolveVirtualUrl(value);
}
[JsonProperty("render")]
public string Render { get; set; }
public string Render
{
get => _render;
set => _render = IOHelper.ResolveVirtualUrl(value);
}
[JsonProperty("icon", Required = Required.Always)]
public string Icon { get; set; }

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Umbraco.Core.IO;
namespace Umbraco.Core.PropertyEditors
{
@@ -10,30 +9,25 @@ namespace Umbraco.Core.PropertyEditors
/// </summary>
public class ParameterEditor : IParameterEditor
{
private readonly ParameterEditorAttribute _attribute;
private ParameterValueEditor _valueEditor;
private ParameterValueEditor _valueEditorAssigned;
/// <summary>
/// The constructor will setup the property editor based on the attribute if one is found
/// </summary>
public ParameterEditor()
{
Configuration = new Dictionary<string, object>();
//assign properties based on the attribute if it is found
_attribute = GetType().GetCustomAttribute<ParameterEditorAttribute>(false);
if (_attribute != null)
{
//set the id/name from the attribute
Alias = _attribute.Alias;
Name = _attribute.Name;
}
}
/// <summary>
/// These are assigned by default normally based on parameter editor attributes or manifest definitions,
/// developers have the chance to override CreateValueEditor if they don't want to use the pre-defined instance
/// </summary>
internal ParameterValueEditor ManifestDefinedParameterValueEditor = null;
// assign properties based on the attribute, if it is found
_attribute = GetType().GetCustomAttribute<ParameterEditorAttribute>(false);
if (_attribute == null) return;
Alias = _attribute.Alias;
Name = _attribute.Name;
}
/// <summary>
/// The id of the property editor
@@ -53,17 +47,19 @@ namespace Umbraco.Core.PropertyEditors
[JsonProperty("config")]
public IDictionary<string, object> Configuration { get; set; }
[JsonIgnore]
[JsonProperty("editor")]
public ParameterValueEditor ValueEditor
{
get { return CreateValueEditor(); }
get => _valueEditor ?? (_valueEditor = CreateValueEditor());
set
{
_valueEditorAssigned = value;
_valueEditor = null;
}
}
[JsonIgnore]
IValueEditor IParameterEditor.ValueEditor
{
get { return ValueEditor; }
}
IValueEditor IParameterEditor.ValueEditor => ValueEditor; // fixme - because we must, but - bah
/// <summary>
/// Creates a value editor instance
@@ -71,25 +67,18 @@ namespace Umbraco.Core.PropertyEditors
/// <returns></returns>
protected virtual ParameterValueEditor CreateValueEditor()
{
if (ManifestDefinedParameterValueEditor != null)
{
//detect if the view is a virtual path (in most cases, yes) then convert it
if (ManifestDefinedParameterValueEditor.View.StartsWith("~/"))
{
ManifestDefinedParameterValueEditor.View = IOHelper.ResolveUrl(ManifestDefinedParameterValueEditor.View);
}
return ManifestDefinedParameterValueEditor;
}
// handle assigned editor
if (_valueEditorAssigned != null)
return _valueEditorAssigned;
//create a new editor
// create a new editor
var editor = new ParameterValueEditor();
if (_attribute.EditorView.IsNullOrWhiteSpace())
{
throw new NotImplementedException("This method must be implemented if a view is not explicitly set");
}
var view = _attribute?.EditorView;
if (string.IsNullOrWhiteSpace(view))
throw new InvalidOperationException("The editor does not specify a view.");
editor.View = view;
editor.View = _attribute.EditorView;
return editor;
}
}

View File

@@ -6,14 +6,14 @@ using Umbraco.Core.Manifest;
namespace Umbraco.Core.PropertyEditors
{
internal class ParameterEditorCollectionBuilder : LazyCollectionBuilderBase<ParameterEditorCollectionBuilder, ParameterEditorCollection, IParameterEditor>
public class ParameterEditorCollectionBuilder : LazyCollectionBuilderBase<ParameterEditorCollectionBuilder, ParameterEditorCollection, IParameterEditor>
{
private readonly ManifestBuilder _manifestBuilder;
private readonly ManifestParser _manifestParser;
public ParameterEditorCollectionBuilder(IServiceContainer container, ManifestBuilder manifestBuilder)
public ParameterEditorCollectionBuilder(IServiceContainer container, ManifestParser manifestParser)
: base(container)
{
_manifestBuilder = manifestBuilder;
_manifestParser = manifestParser;
}
protected override ParameterEditorCollectionBuilder This => this;
@@ -33,8 +33,9 @@ namespace Umbraco.Core.PropertyEditors
return base.CreateItems(args)
.Where(x => (x is PropertyEditor) == false || ((PropertyEditor) x).IsParameterEditor)
.Union(_manifestBuilder.ParameterEditors)
.Union(_manifestBuilder.PropertyEditors.Where(x => x.IsParameterEditor));
.Union(_manifestParser.Manifest.ParameterEditors)
.Union(_manifestParser.Manifest.PropertyEditors.Where(x => x.IsParameterEditor))
.ToList();
}
}
}

View File

@@ -1,4 +1,6 @@
namespace Umbraco.Core.PropertyEditors
using Umbraco.Core.IO;
namespace Umbraco.Core.PropertyEditors
{
// fixme - can we kill this and use "ValueEditor" for both macro and all?
@@ -7,6 +9,8 @@
/// </summary>
public class ParameterValueEditor : IValueEditor
{
private string _view;
/// <summary>
/// Initializes a new instance of the <see cref="ParameterValueEditor"/> class.
/// </summary>
@@ -25,6 +29,10 @@
/// <summary>
/// Gets or sets the editor view.
/// </summary>
public string View { get; set; }
public string View
{
get => _view;
set => _view = IOHelper.ResolveVirtualUrl(value);
}
}
}

View File

@@ -97,7 +97,7 @@ namespace Umbraco.Core.PropertyEditors
/// If fields are specified then the master View and Validators will be ignored
/// </remarks>
[JsonProperty("fields")]
public List<PreValueField> Fields { get; private set; }
public List<PreValueField> Fields { get; internal set; }
/// <summary>
/// A method to format the posted values from the editor to the values to be persisted

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using Umbraco.Core.IO;
using Umbraco.Core.Manifest;
namespace Umbraco.Core.PropertyEditors
@@ -9,6 +10,8 @@ namespace Umbraco.Core.PropertyEditors
/// </summary>
public class PreValueField
{
private string _view;
/// <summary>
/// Standard constructor
/// </summary>
@@ -73,7 +76,11 @@ namespace Umbraco.Core.PropertyEditors
/// * a simple view name which will map to the views/prevalueeditors/{view}.html
/// </summary>
[JsonProperty("view", Required = Required.Always)]
public string View { get; set; }
public string View
{
get => _view;
set => _view = IOHelper.ResolveVirtualUrl(value);
}
/// <summary>
/// A collection of validators for the pre value field

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Newtonsoft.Json;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
@@ -21,6 +20,11 @@ namespace Umbraco.Core.PropertyEditors
{
private readonly PropertyEditorAttribute _attribute;
private PropertyValueEditor _valueEditor;
private PropertyValueEditor _valueEditorAssigned;
private PreValueEditor _preValueEditor;
private PreValueEditor _preValueEditorAssigned;
/// <summary>
/// Initializes a new instance of the <see cref="PropertyEditor"/> class.
/// </summary>
@@ -49,23 +53,11 @@ namespace Umbraco.Core.PropertyEditors
/// </summary>
protected ILogger Logger { get; }
/// <summary>
/// These are assigned by default normally based on property editor attributes or manifest definitions,
/// developers have the chance to override CreateValueEditor if they don't want to use the pre-defined instance
/// </summary>
internal PropertyValueEditor ManifestDefinedPropertyValueEditor = null;
/// <summary>
/// These are assigned by default normally based on property editor attributes or manifest definitions,
/// developers have the chance to override CreatePreValueEditor if they don't want to use the pre-defined instance
/// </summary>
internal PreValueEditor ManifestDefinedPreValueEditor = null;
/// <summary>
/// Gets or sets a value indicating whether this editor can be used as a parameter editor.
/// </summary>
[JsonProperty("isParameterEditor")]
public bool IsParameterEditor { get; internal set; }
public bool IsParameterEditor { get; internal set; } // fixme understand + explain
/// <summary>
/// Gets or sets the unique alias of the property editor.
@@ -95,50 +87,61 @@ namespace Umbraco.Core.PropertyEditors
/// Gets or sets a value indicating whether the property editor is deprecated.
/// </summary>
[JsonIgnore]
public bool IsDeprecated { get; internal set; } // fixme kill it all in v8
public bool IsDeprecated { get; internal set; } // fixme - kill it all in v8
[JsonProperty("editor", Required = Required.Always)]
public PropertyValueEditor ValueEditor => CreateValueEditor();
public PropertyValueEditor ValueEditor
{
get => _valueEditor ?? (_valueEditor = CreateValueEditor());
set
{
_valueEditorAssigned = value;
_valueEditor = null;
}
}
[JsonIgnore]
IValueEditor IParameterEditor.ValueEditor => ValueEditor;
IValueEditor IParameterEditor.ValueEditor => ValueEditor; // fixme - because we must, but - bah
[JsonProperty("prevalues")]
public PreValueEditor PreValueEditor => CreatePreValueEditor();
public PreValueEditor PreValueEditor
{
get => _preValueEditor ?? (_preValueEditor = CreatePreValueEditor());
set
{
_preValueEditorAssigned = value;
_preValueEditor = null;
}
}
[JsonProperty("defaultConfig")]
public virtual IDictionary<string, object> DefaultPreValues { get; set; }
[JsonIgnore]
IDictionary<string, object> IParameterEditor.Configuration => DefaultPreValues;
IDictionary<string, object> IParameterEditor.Configuration => DefaultPreValues; // fixme - because we must, but - bah
/// <summary>
/// Creates a value editor instance
/// Creates a value editor instance.
/// </summary>
protected virtual PropertyValueEditor CreateValueEditor()
{
// handle manifest-defined editors
if (ManifestDefinedPropertyValueEditor != null)
{
// map view path if virtual
if (ManifestDefinedPropertyValueEditor.View.StartsWith("~/"))
ManifestDefinedPropertyValueEditor.View = IOHelper.ResolveUrl(ManifestDefinedPropertyValueEditor.View);
return ManifestDefinedPropertyValueEditor;
}
// handle assigned editor
if (_valueEditorAssigned != null)
return _valueEditorAssigned;
// create a new editor
var editor = new PropertyValueEditor();
var view = _attribute?.EditorView;
if (string.IsNullOrWhiteSpace(view))
throw new InvalidOperationException("The editor does not specify a view.");
if (view.StartsWith("~/"))
view = IOHelper.ResolveUrl(view);
editor.View = view;
editor.ValueType = _attribute.ValueType;
editor.HideLabel = _attribute.HideLabel;
return editor;
}
/// <summary>
@@ -146,17 +149,9 @@ namespace Umbraco.Core.PropertyEditors
/// </summary>
protected virtual PreValueEditor CreatePreValueEditor()
{
// handle manifest-defined editors
if (ManifestDefinedPreValueEditor != null)
{
foreach (var field in ManifestDefinedPreValueEditor.Fields)
{
// map view path if virtual
if (field.View.StartsWith("~/"))
field.View = IOHelper.ResolveUrl(field.View);
}
return ManifestDefinedPreValueEditor;
}
// handle assigned editor
if (_preValueEditorAssigned != null)
return _preValueEditorAssigned;
// else return an empty one
return new PreValueEditor();
@@ -216,17 +211,4 @@ namespace Umbraco.Core.PropertyEditors
return configuration;
}
}
// fixme clear that one! breaking everything!
//public class PropertyEditor<TConfiguration> : PropertyEditor
//{
// public PropertyEditor(ILogger logger)
// : base(logger)
// { }
// public virtual TConfiguration MapConfiguration(string configuration)
// {
// return JsonConvert.DeserializeObject<TConfiguration>(configuration);
// }
//}
}

View File

@@ -8,19 +8,19 @@ namespace Umbraco.Core.PropertyEditors
{
public class PropertyEditorCollectionBuilder : LazyCollectionBuilderBase<PropertyEditorCollectionBuilder, PropertyEditorCollection, PropertyEditor>
{
public PropertyEditorCollectionBuilder(IServiceContainer container)
: base(container)
{ }
private readonly ManifestParser _manifestParser;
// have to property-inject that one as it is internal & the builder is public
[Inject]
internal ManifestBuilder ManifestBuilder { get; set; }
public PropertyEditorCollectionBuilder(IServiceContainer container, ManifestParser manifestParser)
: base(container)
{
_manifestParser = manifestParser;
}
protected override PropertyEditorCollectionBuilder This => this;
protected override IEnumerable<PropertyEditor> CreateItems(params object[] args)
{
return base.CreateItems(args).Union(ManifestBuilder.PropertyEditors);
return base.CreateItems(args).Union(_manifestParser.Manifest.PropertyEditors);
}
}
}

View File

@@ -1,12 +1,11 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Newtonsoft.Json;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Manifest;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.Services;
@@ -18,6 +17,8 @@ namespace Umbraco.Core.PropertyEditors
/// </summary>
public class PropertyValueEditor : IValueEditor
{
private string _view;
/// <summary>
/// Initializes a new instance of the <see cref="PropertyValueEditor"/> class.
/// </summary>
@@ -61,8 +62,7 @@ namespace Umbraco.Core.PropertyEditors
/// </remarks>
public virtual void ConfigureForDisplay(PreValueCollection preValues)
{
if (preValues == null) throw new ArgumentNullException("preValues");
_preVals = preValues;
_preVals = preValues ?? throw new ArgumentNullException(nameof(preValues));
}
/// <summary>
@@ -73,7 +73,11 @@ namespace Umbraco.Core.PropertyEditors
/// folder, or (3) a view name which maps to views/propertyeditors/{view}/{view}.html.</para>
/// </remarks>
[JsonProperty("view", Required = Required.Always)]
public string View { get; set; }
public string View
{
get => _view;
set => _view = IOHelper.ResolveVirtualUrl(value);
}
/// <summary>
/// The value type which reflects how it is validated and stored in the database
@@ -84,9 +88,11 @@ namespace Umbraco.Core.PropertyEditors
/// <summary>
/// A collection of validators for the pre value editor
/// </summary>
[JsonProperty("validation", ItemConverterType = typeof(ManifestValidatorConverter))]
[JsonProperty("validation")]
public List<IPropertyValidator> Validators { get; private set; }
// fixme - need to explain and understand these two + what is "overridable pre-values"
/// <summary>
/// Returns the validator used for the required field validation which is specified on the PropertyType
/// </summary>
@@ -96,10 +102,7 @@ namespace Umbraco.Core.PropertyEditors
/// The default validator used is the RequiredValueValidator but this can be overridden by property editors
/// if they need to do some custom validation, or if the value being validated is a json object.
/// </remarks>
public virtual ManifestValueValidator RequiredValidator
{
get { return new RequiredManifestValueValidator(); }
}
public virtual ManifestValueValidator RequiredValidator => new RequiredManifestValueValidator();
/// <summary>
/// Returns the validator used for the regular expression field validation which is specified on the PropertyType
@@ -110,10 +113,7 @@ namespace Umbraco.Core.PropertyEditors
/// The default validator used is the RegexValueValidator but this can be overridden by property editors
/// if they need to do some custom validation, or if the value being validated is a json object.
/// </remarks>
public virtual ManifestValueValidator RegexValidator
{
get { return new RegexValidator(); }
}
public virtual ManifestValueValidator RegexValidator => new RegexValidator();
/// <summary>
/// Returns the true DataTypeDatabaseType from the string representation ValueType.
@@ -144,7 +144,7 @@ namespace Umbraco.Core.PropertyEditors
case PropertyEditorValueTypes.Time:
return DataTypeDatabaseType.Date;
default:
throw new ArgumentException("Not a valid value type.", "valueType");
throw new ArgumentException("Not a valid value type.", nameof(valueType));
}
}
@@ -157,10 +157,7 @@ namespace Umbraco.Core.PropertyEditors
/// <summary>
/// Set this to true if the property editor is for display purposes only
/// </summary>
public virtual bool IsReadOnly
{
get { return false; }
}
public virtual bool IsReadOnly => false;
/// <summary>
/// Used to try to convert the string value to the correct CLR type based on the DatabaseDataType specified for this value editor
@@ -170,14 +167,8 @@ namespace Umbraco.Core.PropertyEditors
internal Attempt<object> TryConvertValueToCrlType(object value)
{
//this is a custom check to avoid any errors, if it's a string and it's empty just make it null
var s = value as string;
if (s != null)
{
if (s.IsNullOrWhiteSpace())
{
value = null;
}
}
if (value is string s && string.IsNullOrWhiteSpace(s))
value = null;
Type valueType;
//convert the string to a known type

View File

@@ -55,9 +55,7 @@ namespace Umbraco.Core.Runtime
composition.Container.RegisterSingleton(factory => factory.GetInstance<FileSystems>().XsltFileSystem, Constants.Composing.FileSystems.XsltFileSystem);
// register manifest builder, will be injected in eg PropertyEditorCollectionBuilder
composition.Container.RegisterSingleton(factory
=> new ManifestParser(factory.GetInstance<ILogger>(), new DirectoryInfo(IOHelper.MapPath("~/App_Plugins")), factory.GetInstance<IRuntimeCacheProvider>()));
composition.Container.RegisterSingleton<ManifestBuilder>();
composition.Container.RegisterSingleton<ManifestParser>();
composition.Container.RegisterCollectionBuilder<PropertyEditorCollectionBuilder>()
.Add(factory => factory.GetInstance<TypeLoader>().GetPropertyEditors());

View File

@@ -1,47 +1,47 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Umbraco.Core.Serialization
{
internal abstract class JsonCreationConverter<T> : JsonConverter
{
/// <summary>
/// Create an instance of objectType, based properties in the JSON object
/// </summary>
/// <param name="objectType">type of object expected</param>
/// <param name="jObject">contents of JSON object that will be deserialized</param>
/// <returns></returns>
protected abstract T Create(Type objectType, JObject jObject);
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Load JObject from stream
var jObject = JObject.Load(reader);
// Create target object based on JObject
var target = Create(objectType, jObject);
Deserialize(jObject, target, serializer);
return target;
}
protected virtual void Deserialize(JObject jObject, T target, JsonSerializer serializer)
{
// Populate the object properties
serializer.Populate(jObject.CreateReader(), target);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
}
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Umbraco.Core.Serialization
{
internal abstract class JsonReadConverter<T> : JsonConverter
{
/// <summary>
/// Create an instance of objectType, based properties in the JSON object
/// </summary>
/// <param name="objectType">type of object expected</param>
/// <param name="jObject">contents of JSON object that will be deserialized</param>
/// <returns></returns>
protected abstract T Create(Type objectType, JObject jObject);
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Load JObject from stream
var jObject = JObject.Load(reader);
// Create target object based on JObject
var target = Create(objectType, jObject);
Deserialize(jObject, target, serializer);
return target;
}
protected virtual void Deserialize(JObject jobject, T target, JsonSerializer serializer)
{
// Populate the object properties
serializer.Populate(jobject.CreateReader(), target);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotSupportedException();
}
}
}

View File

@@ -489,14 +489,11 @@
<Compile Include="Macros\XsltExtensionCollection.cs" />
<Compile Include="Macros\XsltExtensionCollectionBuilder.cs" />
<Compile Include="MainDom.cs" />
<Compile Include="Manifest\GridEditorConverter.cs" />
<Compile Include="Manifest\ManifestBuilder.cs" />
<Compile Include="Manifest\ManifestParser.cs" />
<Compile Include="Manifest\ManifestValidatorConverter.cs" />
<Compile Include="Manifest\ManifestWatcher.cs" />
<Compile Include="Manifest\PackageManifest.cs" />
<Compile Include="Manifest\ParameterEditorConverter.cs" />
<Compile Include="Manifest\PreValueFieldConverter.cs" />
<Compile Include="Manifest\PropertyEditorConverter.cs" />
<Compile Include="Media\Exif\BitConverterEx.cs" />
<Compile Include="Media\Exif\ExifBitConverter.cs" />
@@ -1286,7 +1283,7 @@
<Compile Include="Serialization\IFormatter.cs" />
<Compile Include="Serialization\ISerializer.cs" />
<Compile Include="Serialization\IStreamedResult.cs" />
<Compile Include="Serialization\JsonCreationConverter.cs" />
<Compile Include="Serialization\JsonReadConverter.cs" />
<Compile Include="Serialization\JsonNetSerializer.cs" />
<Compile Include="Serialization\JsonToStringConverter.cs" />
<Compile Include="Serialization\KnownTypeUdiJsonConverter.cs" />

View File

@@ -1,5 +1,4 @@
using System;
using System.IO;
using System.Linq;
using Moq;
using System.Text;
@@ -7,11 +6,9 @@ using NUnit.Framework;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Cache;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Manifest;
using Umbraco.Core.PropertyEditors;
using Umbraco.Tests.TestHelpers;
namespace Umbraco.Tests.Manifest
{
@@ -24,20 +21,105 @@ namespace Umbraco.Tests.Manifest
[SetUp]
public void Setup()
{
_parser = new ManifestParser(Mock.Of<ILogger>(), new DirectoryInfo(IOHelper.MapPath("~/App_Plugins")), NullCacheProvider.Instance);
_parser = new ManifestParser(NullCacheProvider.Instance, Mock.Of<ILogger>());
}
[Test]
public void Parse_Property_Editors_With_Pre_Vals()
public void CanParseComments()
{
var a = JsonConvert.DeserializeObject<JArray>(@"[
const string json1 = @"
// this is a single-line comment
{
""x"": 2, // this is an end-of-line comment
""y"": 3, /* this is a single line comment block
/* comment */ ""z"": /* comment */ 4,
""t"": ""this is /* comment */ a string"",
""u"": ""this is // more comment in a string""
}
";
var jobject = (JObject) JsonConvert.DeserializeObject(json1);
Assert.AreEqual("2", jobject.Property("x").Value.ToString());
Assert.AreEqual("3", jobject.Property("y").Value.ToString());
Assert.AreEqual("4", jobject.Property("z").Value.ToString());
Assert.AreEqual("this is /* comment */ a string", jobject.Property("t").Value.ToString());
Assert.AreEqual("this is // more comment in a string", jobject.Property("u").Value.ToString());
}
[Test]
public void ThrowOnJsonError()
{
// invalid json, missing the final ']' on javascript
const string json = @"{
propertyEditors: []/*we have empty property editors**/,
javascript: ['~/test.js',/*** some note about stuff asd09823-4**09234*/ '~/test2.js' }";
// parsing fails
Assert.Throws<JsonReaderException>(() => _parser.ParseManifest(json));
}
[Test]
public void CanParseManifest_ScriptsAndStylesheets()
{
var json = "{}";
var manifest = _parser.ParseManifest(json);
Assert.AreEqual(0, manifest.Scripts.Length);
json = "{javascript: []}";
manifest = _parser.ParseManifest(json);
Assert.AreEqual(0, manifest.Scripts.Length);
json = "{javascript: ['~/test.js', '~/test2.js']}";
manifest = _parser.ParseManifest(json);
Assert.AreEqual(2, manifest.Scripts.Length);
json = "{propertyEditors: [], javascript: ['~/test.js', '~/test2.js']}";
manifest = _parser.ParseManifest(json);
Assert.AreEqual(2, manifest.Scripts.Length);
Assert.AreEqual("/test.js", manifest.Scripts[0]);
Assert.AreEqual("/test2.js", manifest.Scripts[1]);
// kludge is gone - must filter before parsing
json = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()) + "{propertyEditors: [], javascript: ['~/test.js', '~/test2.js']}";
Assert.Throws<JsonReaderException>(() => _parser.ParseManifest(json));
json = "{}";
manifest = _parser.ParseManifest(json);
Assert.AreEqual(0, manifest.Stylesheets.Length);
json = "{css: []}";
manifest = _parser.ParseManifest(json);
Assert.AreEqual(0, manifest.Stylesheets.Length);
json = "{css: ['~/style.css', '~/folder-name/sdsdsd/stylesheet.css']}";
manifest = _parser.ParseManifest(json);
Assert.AreEqual(2, manifest.Stylesheets.Length);
json = "{propertyEditors: [], css: ['~/stylesheet.css', '~/random-long-name.css']}";
manifest = _parser.ParseManifest(json);
Assert.AreEqual(2, manifest.Stylesheets.Length);
json = "{propertyEditors: [], javascript: ['~/test.js', '~/test2.js'], css: ['~/stylesheet.css', '~/random-long-name.css']}";
manifest = _parser.ParseManifest(json);
Assert.AreEqual(2, manifest.Scripts.Length);
Assert.AreEqual(2, manifest.Stylesheets.Length);
}
[Test]
public void CanParseManifest_PropertyEditors()
{
const string json = @"{'propertyEditors': [
{
alias: 'Test.Test1',
name: 'Test 1',
editor: {
view: '~/App_Plugins/MyPackage/PropertyEditors/MyEditor.html',
valueType: 'int',
hideLabel: true,
validation: {
'required': true,
'Regex': '\\d*'
@@ -60,147 +142,10 @@ namespace Umbraco.Tests.Manifest
}
]
}
}
]");
var parser = _parser.GetPropertyEditors(a);
Assert.AreEqual(1, parser.Count());
Assert.AreEqual(2, parser.ElementAt(0).PreValueEditor.Fields.Count);
Assert.AreEqual("key1", parser.ElementAt(0).PreValueEditor.Fields.ElementAt(0).Key);
Assert.AreEqual("Some config 1", parser.ElementAt(0).PreValueEditor.Fields.ElementAt(0).Name);
Assert.AreEqual("/App_Plugins/MyPackage/PropertyEditors/Views/pre-val1.html", parser.ElementAt(0).PreValueEditor.Fields.ElementAt(0).View);
Assert.AreEqual(1, parser.ElementAt(0).PreValueEditor.Fields.ElementAt(0).Validators.Count);
Assert.AreEqual("key2", parser.ElementAt(0).PreValueEditor.Fields.ElementAt(1).Key);
Assert.AreEqual("Some config 2", parser.ElementAt(0).PreValueEditor.Fields.ElementAt(1).Name);
Assert.AreEqual("/App_Plugins/MyPackage/PropertyEditors/Views/pre-val2.html", parser.ElementAt(0).PreValueEditor.Fields.ElementAt(1).View);
Assert.AreEqual(0, parser.ElementAt(0).PreValueEditor.Fields.ElementAt(1).Validators.Count());
}
[Test]
public void Parse_Grid_Editors()
{
var a = JsonConvert.DeserializeObject<JArray>(@"[
{
alias: 'Test.Test1',
name: 'Test 1',
view: 'blah',
icon: 'hello'
},
{
alias: 'Test.Test2',
name: 'Test 2',
config: { key1: 'some default val' },
view: '~/hello/world.cshtml',
icon: 'helloworld'
},
{
alias: 'Test.Test3',
name: 'Test 3',
config: { key1: 'some default val' },
view: '/hello/world.html',
render: '~/hello/world.cshtml',
icon: 'helloworld'
}
]");
var parser = _parser.GetGridEditors(a).ToArray();
Assert.AreEqual(3, parser.Count());
Assert.AreEqual("Test.Test1", parser.ElementAt(0).Alias);
Assert.AreEqual("Test 1", parser.ElementAt(0).Name);
Assert.AreEqual("blah", parser.ElementAt(0).View);
Assert.AreEqual("hello", parser.ElementAt(0).Icon);
Assert.IsNull(parser.ElementAt(0).Render);
Assert.AreEqual(0, parser.ElementAt(0).Config.Count);
Assert.AreEqual("Test.Test2", parser.ElementAt(1).Alias);
Assert.AreEqual("Test 2", parser.ElementAt(1).Name);
Assert.AreEqual("/hello/world.cshtml", parser.ElementAt(1).View);
Assert.AreEqual("helloworld", parser.ElementAt(1).Icon);
Assert.IsNull(parser.ElementAt(1).Render);
Assert.AreEqual(1, parser.ElementAt(1).Config.Count);
Assert.AreEqual("some default val", parser.ElementAt(1).Config["key1"]);
Assert.AreEqual("Test.Test3", parser.ElementAt(2).Alias);
Assert.AreEqual("Test 3", parser.ElementAt(2).Name);
Assert.AreEqual("/hello/world.html", parser.ElementAt(2).View);
Assert.AreEqual("helloworld", parser.ElementAt(2).Icon);
Assert.AreEqual("/hello/world.cshtml", parser.ElementAt(2).Render);
Assert.AreEqual(1, parser.ElementAt(2).Config.Count);
Assert.AreEqual("some default val", parser.ElementAt(2).Config["key1"]);
}
[Test]
public void Parse_Property_Editors()
{
var a = JsonConvert.DeserializeObject<JArray>(@"[
{
alias: 'Test.Test1',
name: 'Test 1',
icon: 'icon-war',
editor: {
view: '~/App_Plugins/MyPackage/PropertyEditors/MyEditor.html',
valueType: 'int',
validation: {
required : true,
regex : '\\d*'
}
}
},
{
alias: 'Test.Test2',
name: 'Test 2',
group: 'customgroup',
defaultConfig: { key1: 'some default pre val' },
editor: {
view: '~/App_Plugins/MyPackage/PropertyEditors/CsvEditor.html',
hideLabel: true
}
}
]");
var parser = _parser.GetPropertyEditors(a);
Assert.AreEqual(2, parser.Count());
Assert.AreEqual(false, parser.ElementAt(0).ValueEditor.HideLabel);
Assert.AreEqual("Test.Test1", parser.ElementAt(0).Alias);
Assert.AreEqual("Test 1", parser.ElementAt(0).Name);
Assert.AreEqual("/App_Plugins/MyPackage/PropertyEditors/MyEditor.html", parser.ElementAt(0).ValueEditor.View);
Assert.AreEqual("int", parser.ElementAt(0).ValueEditor.ValueType);
Assert.AreEqual(2, parser.ElementAt(0).ValueEditor.Validators.Count());
var manifestValidator1 = parser.ElementAt(0).ValueEditor.Validators.ElementAt(0) as ManifestPropertyValidator;
Assert.IsNotNull(manifestValidator1);
Assert.AreEqual("required", manifestValidator1.Type);
var manifestValidator2 = parser.ElementAt(0).ValueEditor.Validators.ElementAt(1) as ManifestPropertyValidator;
Assert.IsNotNull(manifestValidator2);
Assert.AreEqual("regex", manifestValidator2.Type);
//groups and icons
Assert.AreEqual("common", parser.ElementAt(0).Group);
Assert.AreEqual("customgroup", parser.ElementAt(1).Group);
Assert.AreEqual("icon-war", parser.ElementAt(0).Icon);
Assert.AreEqual("icon-autofill", parser.ElementAt(1).Icon);
Assert.AreEqual(true, parser.ElementAt(1).ValueEditor.HideLabel);
Assert.AreEqual("Test.Test2", parser.ElementAt(1).Alias);
Assert.AreEqual("Test 2", parser.ElementAt(1).Name);
Assert.IsTrue(parser.ElementAt(1).DefaultPreValues.ContainsKey("key1"));
Assert.AreEqual("some default pre val", parser.ElementAt(1).DefaultPreValues["key1"]);
}
[Test]
public void Property_Editors_Can_Be_Parameter_Editor()
{
var a = JsonConvert.DeserializeObject<JArray>(@"[
{
alias: 'Test.Test1',
name: 'Test 1',
isParameterEditor: true,
defaultConfig: { key1: 'some default val' },
editor: {
@@ -211,31 +156,76 @@ namespace Umbraco.Tests.Manifest
regex : '\\d*'
}
}
},
{
alias: 'Test.Test2',
name: 'Test 2',
defaultConfig: { key1: 'some default pre val' },
editor: {
view: '~/App_Plugins/MyPackage/PropertyEditors/CsvEditor.html'
}
}
]");
var parser = _parser.GetPropertyEditors(a);
]}";
Assert.AreEqual(1, parser.Count(x => x.IsParameterEditor));
var manifest = _parser.ParseManifest(json);
Assert.AreEqual(2, manifest.PropertyEditors.Length);
IParameterEditor parameterEditor = parser.First();
Assert.AreEqual(1, parameterEditor.Configuration.Count);
Assert.IsTrue(parameterEditor.Configuration.ContainsKey("key1"));
Assert.AreEqual("some default val", parameterEditor.Configuration["key1"]);
var editor = manifest.PropertyEditors[1];
Assert.IsTrue(editor.IsParameterEditor);
editor = manifest.PropertyEditors[0];
Assert.AreEqual("Test.Test1", editor.Alias);
Assert.AreEqual("Test 1", editor.Name);
Assert.IsFalse(editor.IsParameterEditor);
var valueEditor = editor.ValueEditor;
Assert.AreEqual("/App_Plugins/MyPackage/PropertyEditors/MyEditor.html", valueEditor.View);
Assert.AreEqual("int", valueEditor.ValueType);
Assert.IsTrue(valueEditor.HideLabel);
// these two don't make much sense here
// valueEditor.RegexValidator;
// valueEditor.RequiredValidator;
var validators = valueEditor.Validators;
Assert.AreEqual(2, validators.Count);
var validator = validators[0];
var v = validator as ManifestPropertyValidator;
Assert.IsNotNull(v);
Assert.AreEqual("required", v.Type);
Assert.AreEqual("", v.Config);
validator = validators[1];
v = validator as ManifestPropertyValidator;
Assert.IsNotNull(v);
Assert.AreEqual("Regex", v.Type);
Assert.AreEqual("\\d*", v.Config);
// this is not part of the manifest
var preValues = editor.DefaultPreValues;
Assert.IsNull(preValues);
var preValueEditor = editor.PreValueEditor;
Assert.IsNotNull(preValueEditor);
Assert.IsNotNull(preValueEditor.Fields);
Assert.AreEqual(2, preValueEditor.Fields.Count);
var f = preValueEditor.Fields[0];
Assert.AreEqual("key1", f.Key);
Assert.AreEqual("Some config 1", f.Name);
Assert.AreEqual("/App_Plugins/MyPackage/PropertyEditors/Views/pre-val1.html", f.View);
var fvalidators = f.Validators;
Assert.IsNotNull(fvalidators);
Assert.AreEqual(1, fvalidators.Count);
var fv = fvalidators[0] as ManifestPropertyValidator;
Assert.IsNotNull(fv);
Assert.AreEqual("required", fv.Type);
Assert.AreEqual("", fv.Config);
f = preValueEditor.Fields[1];
Assert.AreEqual("key2", f.Key);
Assert.AreEqual("Some config 2", f.Name);
Assert.AreEqual("/App_Plugins/MyPackage/PropertyEditors/Views/pre-val2.html", f.View);
fvalidators = f.Validators;
Assert.IsNotNull(fvalidators);
Assert.AreEqual(0, fvalidators.Count);
}
[Test]
public void Parse_Parameter_Editors()
public void CanParseManifest_ParameterEditors()
{
var a = JsonConvert.DeserializeObject<JArray>(@"[
const string json = @"{'parameterEditors': [
{
alias: 'parameter1',
name: 'My Parameter',
@@ -246,116 +236,47 @@ namespace Umbraco.Tests.Manifest
name: 'Another parameter',
config: { key1: 'some config val' },
view: '~/App_Plugins/MyPackage/PropertyEditors/CsvEditor.html'
},
{
alias: 'parameter3',
name: 'Yet another parameter'
}
]");
var parser = _parser.GetParameterEditors(a);
]}";
Assert.AreEqual(2, parser.Count());
Assert.AreEqual("parameter1", parser.ElementAt(0).Alias);
Assert.AreEqual("My Parameter", parser.ElementAt(0).Name);
Assert.AreEqual("/App_Plugins/MyPackage/PropertyEditors/MyEditor.html", parser.ElementAt(0).ValueEditor.View);
var manifest = _parser.ParseManifest(json);
Assert.AreEqual(3, manifest.ParameterEditors.Length);
Assert.AreEqual("parameter2", parser.ElementAt(1).Alias);
Assert.AreEqual("Another parameter", parser.ElementAt(1).Name);
Assert.IsTrue(parser.ElementAt(1).Configuration.ContainsKey("key1"));
Assert.AreEqual("some config val", parser.ElementAt(1).Configuration["key1"]);
}
var editor = manifest.ParameterEditors[1];
Assert.AreEqual("parameter2", editor.Alias);
Assert.AreEqual("Another parameter", editor.Name);
[Test]
public void Merge_JArrays()
{
var obj1 = JArray.FromObject(new[] { "test1", "test2", "test3" });
var obj2 = JArray.FromObject(new[] { "test1", "test2", "test3", "test4" });
var config = editor.Configuration;
Assert.AreEqual(1, config.Count);
Assert.IsTrue(config.ContainsKey("key1"));
Assert.AreEqual("some config val", config["key1"]);
ManifestParser.MergeJArrays(obj1, obj2);
var valueEditor = editor.ValueEditor;
Assert.AreEqual("/App_Plugins/MyPackage/PropertyEditors/CsvEditor.html", valueEditor.View);
Assert.AreEqual(4, obj1.Count());
}
[Test]
public void Merge_JObjects_Replace_Original()
{
var obj1 = JObject.FromObject(new
{
Property1 = "Value1",
Property2 = "Value2",
Property3 = "Value3"
});
var obj2 = JObject.FromObject(new
editor = manifest.ParameterEditors[2];
Assert.Throws<InvalidOperationException>(() =>
{
Property3 = "Value3/2",
Property4 = "Value4",
Property5 = "Value5"
var _ = editor.ValueEditor;
});
ManifestParser.MergeJObjects(obj1, obj2);
Assert.AreEqual(5, obj1.Properties().Count());
Assert.AreEqual("Value3/2", obj1.Properties().ElementAt(2).Value.Value<string>());
}
[Test]
public void Merge_JObjects_Keep_Original()
public void CanParseManifest_GridEditors()
{
var obj1 = JObject.FromObject(new
{
Property1 = "Value1",
Property2 = "Value2",
Property3 = "Value3"
});
var obj2 = JObject.FromObject(new
{
Property3 = "Value3/2",
Property4 = "Value4",
Property5 = "Value5"
});
ManifestParser.MergeJObjects(obj1, obj2, true);
Assert.AreEqual(5, obj1.Properties().Count());
Assert.AreEqual("Value3", obj1.Properties().ElementAt(2).Value.Value<string>());
}
[TestCase("C:\\Test", "C:\\Test\\MyFolder\\AnotherFolder", 2)]
[TestCase("C:\\Test", "C:\\Test\\MyFolder\\AnotherFolder\\YetAnother", 3)]
[TestCase("C:\\Test", "C:\\Test\\", 0)]
public void Get_Folder_Depth(string baseFolder, string currFolder, int expected)
{
Assert.AreEqual(expected,
_parser.FolderDepth(
new DirectoryInfo(baseFolder),
new DirectoryInfo(currFolder)));
}
//[Test]
//public void Parse_Property_Editor()
//{
//}
[Test]
public void Create_Manifests_Editors()
{
var package1 = @"{
propertyEditors: [],
javascript: ['~/test.js', '~/test2.js']}";
var package2 = "{css: ['~/style.css', '~/folder-name/sdsdsd/stylesheet.css']}";
var package3 = @"{
const string json = @"{
'javascript': [ ],
'css': [ ],
'gridEditors': [
{
'name': 'Small Hero',
'alias': 'small-hero',
'view': '/App_Plugins/MyPlugin/small-hero/editortemplate.html',
'render': '/Views/Partials/Grid/Editors/SmallHero.cshtml',
'view': '~/App_Plugins/MyPlugin/small-hero/editortemplate.html',
'render': '~/Views/Partials/Grid/Editors/SmallHero.cshtml',
'icon': 'icon-presentation',
'config': {
'image': {
@@ -373,144 +294,32 @@ javascript: ['~/test.js', '~/test2.js']}";
{
'name': 'Document Links By Category',
'alias': 'document-links-by-category',
'view': '/App_Plugins/MyPlugin/document-links-by-category/editortemplate.html',
'render': '/Views/Partials/Grid/Editors/DocumentLinksByCategory.cshtml',
'view': '~/App_Plugins/MyPlugin/document-links-by-category/editortemplate.html',
'render': '~/Views/Partials/Grid/Editors/DocumentLinksByCategory.cshtml',
'icon': 'icon-umb-members'
}
]
}";
var package4 = @"{'propertyEditors': [
{
alias: 'Test.Test1',
name: 'Test 1',
editor: {
view: '~/App_Plugins/MyPackage/PropertyEditors/MyEditor.html',
valueType: 'int',
validation: {
'required': true,
'Regex': '\\d*'
}
},
prevalues: {
fields: [
{
label: 'Some config 1',
key: 'key1',
view: '~/App_Plugins/MyPackage/PropertyEditors/Views/pre-val1.html',
validation: {
required: true
}
},
{
label: 'Some config 2',
key: 'key2',
view: '~/App_Plugins/MyPackage/PropertyEditors/Views/pre-val2.html'
}
]
}
}
]}";
var manifest = _parser.ParseManifest(json);
Assert.AreEqual(2, manifest.GridEditors.Length);
var package5 = @"{'parameterEditors': [
{
alias: 'parameter1',
name: 'My Parameter',
view: '~/App_Plugins/MyPackage/PropertyEditors/MyEditor.html'
},
{
alias: 'parameter2',
name: 'Another parameter',
config: { key1: 'some config val' },
view: '~/App_Plugins/MyPackage/PropertyEditors/CsvEditor.html'
}
]}";
var editor = manifest.GridEditors[0];
Assert.AreEqual("small-hero", editor.Alias);
Assert.AreEqual("Small Hero", editor.Name);
Assert.AreEqual("/App_Plugins/MyPlugin/small-hero/editortemplate.html", editor.View);
Assert.AreEqual("/Views/Partials/Grid/Editors/SmallHero.cshtml", editor.Render);
Assert.AreEqual("icon-presentation", editor.Icon);
var result = _parser.CreateManifests(package1, package2, package3, package4, package5).ToArray();
var paramEditors = result.SelectMany(x => _parser.GetParameterEditors(x.ParameterEditors)).ToArray();
var propEditors = result.SelectMany(x => _parser.GetPropertyEditors(x.PropertyEditors)).ToArray();
var gridEditors = result.SelectMany(x => _parser.GetGridEditors(x.GridEditors)).ToArray();
Assert.AreEqual(2, gridEditors.Count());
Assert.AreEqual(2, paramEditors.Count());
Assert.AreEqual(1, propEditors.Count());
var config = editor.Config;
Assert.AreEqual(2, config.Count);
Assert.IsTrue(config.ContainsKey("image"));
var c = config["image"];
Assert.IsInstanceOf<JObject>(c); // fixme - is this what we want?
Assert.IsTrue(config.ContainsKey("link"));
c = config["link"];
Assert.IsInstanceOf<JObject>(c); // fixme - is this what we want?
// fixme - should we resolveUrl in configs?
}
[Test]
public void Create_Manifest_With_Line_Comments()
{
var content4 = @"{
//here's the property editors
propertyEditors: [],
//and here's the javascript
javascript: ['~/test.js', '~/test2.js']}";
var result = _parser.CreateManifests(null, content4);
Assert.AreEqual(1, result.Count());
}
[Test]
public void Create_Manifest_With_Surround_Comments()
{
var content4 = @"{
propertyEditors: []/*we have empty property editors**/,
javascript: ['~/test.js',/*** some note about stuff asd09823-4**09234*/ '~/test2.js']}";
var result = _parser.CreateManifests(null, content4);
Assert.AreEqual(1, result.Count());
}
[Test]
public void Create_Manifest_With_Error()
{
//NOTE: This is missing the final closing ]
var content4 = @"{
propertyEditors: []/*we have empty property editors**/,
javascript: ['~/test.js',/*** some note about stuff asd09823-4**09234*/ '~/test2.js' }";
var result = _parser.CreateManifests(null, content4);
//an error has occurred and been logged but processing continues
Assert.AreEqual(0, result.Count());
}
[Test]
public void Create_Manifest_From_File_Content()
{
var content1 = "{}";
var content2 = "{javascript: []}";
var content3 = "{javascript: ['~/test.js', '~/test2.js']}";
var content4 = "{propertyEditors: [], javascript: ['~/test.js', '~/test2.js']}";
var content5 = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()) + "{propertyEditors: [], javascript: ['~/test.js', '~/test2.js']}";
var result = _parser.CreateManifests(null, content1, content2, content3, content4, content5);
Assert.AreEqual(5, result.Count());
Assert.AreEqual(0, result.ElementAt(1).JavaScriptInitialize.Count);
Assert.AreEqual(2, result.ElementAt(2).JavaScriptInitialize.Count);
Assert.AreEqual(2, result.ElementAt(3).JavaScriptInitialize.Count);
Assert.AreEqual(2, result.ElementAt(4).JavaScriptInitialize.Count);
}
[Test]
public void Parse_Stylesheet_Initialization()
{
var content1 = "{}";
var content2 = "{css: []}";
var content3 = "{css: ['~/style.css', '~/folder-name/sdsdsd/stylesheet.css']}";
var content4 = "{propertyEditors: [], css: ['~/stylesheet.css', '~/random-long-name.css']}";
var result = _parser.CreateManifests(null, content1, content2, content3, content4);
Assert.AreEqual(4, result.Count());
Assert.AreEqual(0, result.ElementAt(1).StylesheetInitialize.Count);
Assert.AreEqual(2, result.ElementAt(2).StylesheetInitialize.Count);
Assert.AreEqual(2, result.ElementAt(3).StylesheetInitialize.Count);
}
}
}

View File

@@ -21,9 +21,10 @@ namespace Umbraco.Tests.Models.Mapping
{
base.Compose();
var manifestBuilder = new ManifestBuilder(
var manifestBuilder = new ManifestParser(
CacheHelper.CreateDisabledCacheHelper().RuntimeCache,
new ManifestParser(Logger, new DirectoryInfo(TestHelper.CurrentAssemblyDirectory), CacheHelper.CreateDisabledCacheHelper().RuntimeCache));
TestHelper.CurrentAssemblyDirectory,
Logger);
Container.Register(_ => manifestBuilder);
Func<IEnumerable<Type>> typeListProducerList = Enumerable.Empty<Type>;

View File

@@ -303,10 +303,7 @@ namespace Umbraco.Tests.Testing
Container.RegisterSingleton<ISectionService, SectionService>();
// somehow property editor ends up wanting this
Container.RegisterSingleton(f => new ManifestBuilder(
f.GetInstance<IRuntimeCacheProvider>(),
new ManifestParser(f.GetInstance<ILogger>(), new DirectoryInfo(IOHelper.MapPath("~/App_Plugins")), f.GetInstance<IRuntimeCacheProvider>())
));
Container.RegisterSingleton<ManifestParser>();
// note - don't register collections, use builders
Container.RegisterCollectionBuilder<PropertyEditorCollectionBuilder>();

View File

@@ -19,7 +19,7 @@ namespace Umbraco.Tests.Web.AngularIntegration
[Test]
public void Parse_Main()
{
var result = JsInitialization.ParseMain(new[] {"[World]", "Hello" });
var result = JsInitialization.WriteScript(new[] {"[World]", "Hello" });
Assert.AreEqual(@"LazyLoad.js([World], function () {
//we need to set the legacy UmbClientMgr path

View File

@@ -32,6 +32,7 @@ using Umbraco.Core.Services;
using Umbraco.Web.Composing;
using Action = Umbraco.Web._Legacy.Actions.Action;
using Constants = Umbraco.Core.Constants;
using JArray = Newtonsoft.Json.Linq.JArray;
namespace Umbraco.Web.Editors
{
@@ -47,7 +48,7 @@ namespace Umbraco.Web.Editors
[DisableClientCache]
public class BackOfficeController : UmbracoController
{
private readonly IRuntimeState _runtime;
private readonly ManifestParser _manifestParser;
private BackOfficeUserManager<BackOfficeIdentityUser> _userManager;
private BackOfficeSignInManager _signInManager;
@@ -55,9 +56,9 @@ namespace Umbraco.Web.Editors
private const string TokenPasswordResetCode = "PasswordResetCode";
private static readonly string[] TempDataTokenNames = { TokenExternalSignInError, TokenPasswordResetCode };
public BackOfficeController(IRuntimeState runtime)
public BackOfficeController(ManifestParser manifestParser)
{
_runtime = runtime;
_manifestParser = manifestParser;
}
protected BackOfficeSignInManager SignInManager => _signInManager ?? (_signInManager = OwinContext.GetBackOfficeSignInManager());
@@ -184,12 +185,12 @@ namespace Umbraco.Web.Editors
[OutputCache(Order = 1, VaryByParam = "none", Location = OutputCacheLocation.Server, Duration = 5000)]
public JavaScriptResult Application()
{
var parser = GetManifestParser();
var parser = _manifestParser;
var initJs = new JsInitialization(parser);
var initCss = new CssInitialization(parser);
//get the legacy ActionJs file references to append as well
var legacyActionJsRef = new JArray(GetLegacyActionJs(LegacyJsActionType.JsUrl));
var legacyActionJsRef = GetLegacyActionJs(LegacyJsActionType.JsUrl);
var result = initJs.GetJavascriptInitialization(HttpContext, JsInitialization.GetDefaultInitialization(), legacyActionJsRef);
result += initCss.GetStylesheetInitialization(HttpContext);
@@ -205,24 +206,24 @@ namespace Umbraco.Web.Editors
[HttpGet]
public JsonNetResult GetManifestAssetList()
{
Func<JArray> getResult = () =>
JArray GetAssetList()
{
var parser = GetManifestParser();
var parser = _manifestParser;
var initJs = new JsInitialization(parser);
var initCss = new CssInitialization(parser);
var jsResult = initJs.GetJavascriptInitializationArray(HttpContext, new JArray());
var cssResult = initCss.GetStylesheetInitializationArray(HttpContext);
ManifestParser.MergeJArrays(jsResult, cssResult);
return jsResult;
};
var assets = new List<string>();
assets.AddRange(initJs.GetScriptFiles(HttpContext, Enumerable.Empty<string>()));
assets.AddRange(initCss.GetStylesheetFiles(HttpContext));
return new JArray(assets);
}
//cache the result if debugging is disabled
var result = HttpContext.IsDebuggingEnabled
? getResult()
? GetAssetList()
: ApplicationCache.RuntimeCache.GetCacheItem<JArray>(
typeof(BackOfficeController) + "GetManifestAssetList",
() => getResult(),
new TimeSpan(0, 10, 0));
"Umbraco.Web.Editors.BackOfficeController.GetManifestAssetList",
GetAssetList,
new TimeSpan(0, 2, 0));
return new JsonNetResult { Data = result, Formatting = Formatting.Indented };
}
@@ -333,13 +334,6 @@ namespace Umbraco.Web.Editors
return RedirectToLocal(Url.Action("Default", "BackOffice"));
}
private ManifestParser GetManifestParser()
{
var plugins = new DirectoryInfo(Server.MapPath("~/App_Plugins"));
var parser = new ManifestParser(Logger, plugins, ApplicationCache.RuntimeCache);
return parser;
}
/// <summary>
/// Used by Default and AuthorizeUpgrade to render as per normal if there's no external login info,
/// otherwise process the external login info.

View File

@@ -2,12 +2,9 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Web;
using ClientDependency.Core;
using ClientDependency.Core.Config;
using Newtonsoft.Json.Linq;
using Umbraco.Core.PropertyEditors;
using Umbraco.Web.Composing;
using Umbraco.Web.PropertyEditors;
@@ -15,88 +12,53 @@ namespace Umbraco.Web.UI.JavaScript
{
internal abstract class AssetInitialization
{
/// <summary>
/// Get all dependencies declared on property editors
/// </summary>
/// <param name="cdfType"></param>
/// <param name="httpContext"></param>
/// <returns></returns>
protected JArray ScanPropertyEditors(ClientDependencyType cdfType, HttpContextBase httpContext)
protected IEnumerable<string> ScanPropertyEditors(ClientDependencyType assetType, HttpContextBase httpContext)
{
if (httpContext == null) throw new ArgumentNullException("httpContext");
var cdfAttributes =
Current.PropertyEditors
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
var attributes = Current.PropertyEditors
.SelectMany(x => x.GetType().GetCustomAttributes<PropertyEditorAssetAttribute>(false))
.Where(x => x.AssetType == cdfType)
.Where(x => x.AssetType == assetType)
.Select(x => x.DependencyFile)
.ToList();
string jsOut;
string cssOut;
var renderer = ClientDependencySettings.Instance.MvcRendererCollection["Umbraco.DependencyPathRenderer"];
renderer.RegisterDependencies(cdfAttributes, new HashSet<IClientDependencyPath>(), out jsOut, out cssOut, httpContext);
renderer.RegisterDependencies(attributes, new HashSet<IClientDependencyPath>(), out var scripts, out var stylesheets, httpContext);
var toParse = cdfType == ClientDependencyType.Javascript ? jsOut : cssOut;
var result = new JArray();
//split the result by the delimiter and add to the array
foreach (var u in toParse.Split(new[] { DependencyPathRenderer.Delimiter }, StringSplitOptions.RemoveEmptyEntries))
{
result.Add(u);
}
return result;
var toParse = assetType == ClientDependencyType.Javascript ? scripts : stylesheets;
return toParse.Split(new[] { DependencyPathRenderer.Delimiter }, StringSplitOptions.RemoveEmptyEntries);
}
/// <summary>
/// This will use CDF to optimize the asset file collection
/// </summary>
/// <param name="fileRefs"></param>
/// <param name="cdfType"></param>
/// <param name="httpContext"></param>
/// <returns>
/// Return the asset URLs that should be loaded, if the application is in debug mode then the URLs returned will be the same as the ones
/// passed in with the CDF version query strings appended so cache busting works correctly.
/// </returns>
protected JArray OptimizeAssetCollection(JArray fileRefs, ClientDependencyType cdfType, HttpContextBase httpContext)
protected IEnumerable<string> OptimizeAssetCollection(IEnumerable<string> assets, ClientDependencyType assetType, HttpContextBase httpContext)
{
if (httpContext == null) throw new ArgumentNullException("httpContext");
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
var depenencies = fileRefs.Select(x =>
var requestUrl = httpContext.Request.Url;
if (requestUrl == null) throw new ArgumentException("HttpContext.Request.Url is null.", nameof(httpContext));
var dependencies = assets.Select(x =>
{
var asString = x.ToString();
if (asString.StartsWith("/") == false)
// most declarations with be made relative to the /umbraco folder, so things
// ike lib/blah/blah.js so we need to turn them into absolutes here
if (x.StartsWith("/") == false && Uri.IsWellFormedUriString(x, UriKind.Relative))
{
//most declarations with be made relative to the /umbraco folder, so things like lib/blah/blah.js
// so we need to turn them into absolutes here
if (Uri.IsWellFormedUriString(asString, UriKind.Relative))
{
var absolute = new Uri(httpContext.Request.Url, asString);
return (IClientDependencyFile)new BasicFile(cdfType) { FilePath = absolute.AbsolutePath };
}
return (IClientDependencyFile) new BasicFile(assetType) { FilePath = new Uri(requestUrl, x).AbsolutePath };
}
return cdfType == ClientDependencyType.Javascript
? (IClientDependencyFile)new JavascriptFile(asString)
: (IClientDependencyFile)new CssFile(asString);
}).Where(x => x != null).ToList();
//Get the output string for these registrations which will be processed by CDF correctly to stagger the output based
return assetType == ClientDependencyType.Javascript
? (IClientDependencyFile) new JavascriptFile(x)
: (IClientDependencyFile) new CssFile(x);
}).ToList();
// get the output string for these registrations which will be processed by CDF correctly to stagger the output based
// on internal vs external resources. The output will be delimited based on our custom Umbraco.Web.UI.JavaScript.DependencyPathRenderer
string jsOut;
string cssOut;
var renderer = ClientDependencySettings.Instance.MvcRendererCollection["Umbraco.DependencyPathRenderer"];
renderer.RegisterDependencies(depenencies, new HashSet<IClientDependencyPath>(), out jsOut, out cssOut, httpContext);
renderer.RegisterDependencies(dependencies, new HashSet<IClientDependencyPath>(), out var scripts, out var stylesheets, httpContext);
var urls = cdfType == ClientDependencyType.Javascript
? jsOut.Split(new string[] { DependencyPathRenderer.Delimiter }, StringSplitOptions.RemoveEmptyEntries)
: cssOut.Split(new string[] { DependencyPathRenderer.Delimiter }, StringSplitOptions.RemoveEmptyEntries);
var urls = assetType == ClientDependencyType.Javascript
? scripts.Split(new[] { DependencyPathRenderer.Delimiter }, StringSplitOptions.RemoveEmptyEntries)
: stylesheets.Split(new[] { DependencyPathRenderer.Delimiter }, StringSplitOptions.RemoveEmptyEntries);
var result = new JArray();
foreach (var u in urls)
{
result.Add(u);
}
return result;
return urls;
}
}
}

View File

@@ -1,12 +1,9 @@
using System.Web;
using ClientDependency.Core;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Core.IO;
using Umbraco.Core.Manifest;
namespace Umbraco.Web.UI.JavaScript
@@ -14,50 +11,39 @@ namespace Umbraco.Web.UI.JavaScript
internal class CssInitialization : AssetInitialization
{
private readonly ManifestParser _parser;
public CssInitialization(ManifestParser parser)
{
_parser = parser;
}
/// <summary>
/// Processes all found manifest files and outputs yepnope.injectcss calls for all css files found in all manifests
/// Processes all found manifest files, and outputs css inject calls for all css files found in all manifests.
/// </summary>
public string GetStylesheetInitialization(HttpContextBase httpContext)
{
var result = GetStylesheetInitializationArray(httpContext);
return ParseMain(result);
var files = GetStylesheetFiles(httpContext);
return WriteScript(files);
}
public JArray GetStylesheetInitializationArray(HttpContextBase httpContext)
public IEnumerable<string> GetStylesheetFiles(HttpContextBase httpContext)
{
var merged = new JArray();
foreach (var m in _parser.GetManifests())
{
ManifestParser.MergeJArrays(merged, m.StylesheetInitialize);
}
var stylesheets = new HashSet<string>();
var optimizedManifest = OptimizeAssetCollection(_parser.Manifest.Stylesheets, ClientDependencyType.Css, httpContext);
foreach (var stylesheet in optimizedManifest)
stylesheets.Add(stylesheet);
//now we can optimize if in release mode
merged = OptimizeAssetCollection(merged, ClientDependencyType.Css, httpContext);
foreach (var stylesheet in ScanPropertyEditors(ClientDependencyType.Css, httpContext))
stylesheets.Add(stylesheet);
//now we need to merge in any found cdf declarations on property editors
ManifestParser.MergeJArrays(merged, ScanPropertyEditors(ClientDependencyType.Css, httpContext));
return merged;
return stylesheets.ToArray();
}
/// <summary>
/// Parses the CssResources.Main and returns a yepnop.injectCss format
/// </summary>
/// <param name="files"></param>
/// <returns></returns>
internal static string ParseMain(JArray files)
internal static string WriteScript(IEnumerable<string> files)
{
var sb = new StringBuilder();
foreach (var file in files)
sb.AppendFormat("{0}LazyLoad.css('{1}');", Environment.NewLine, file);
return sb.ToString();
}

View File

@@ -1,17 +1,13 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Web;
using ClientDependency.Core;
using ClientDependency.Core.Config;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Core.Manifest;
using System.Linq;
using System.Text;
namespace Umbraco.Web.UI.JavaScript
{
@@ -28,65 +24,65 @@ namespace Umbraco.Web.UI.JavaScript
_parser = parser;
}
//used to strip comments
internal static readonly Regex Comments = new Regex("(/\\*.*\\*/)", RegexOptions.Compiled);
//used for dealing with js functions inside of json (which is not a supported json syntax)
// deal with javascript functions inside of json (not a supported json syntax)
private const string PrefixJavaScriptObject = "@@@@";
private static readonly Regex JsFunctionParser = new Regex(string.Format("(\"{0}(.*?)\")+", PrefixJavaScriptObject),
RegexOptions.Multiline
| RegexOptions.CultureInvariant
| RegexOptions.Compiled);
//used to replace the tokens in the js main
private static readonly Regex JsFunctionParser = new Regex($"(\"{PrefixJavaScriptObject}(.*?)\")+",
RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);
// replace tokens in the js main
private static readonly Regex Token = new Regex("(\"##\\w+?##\")", RegexOptions.Compiled);
/// <summary>
/// Processes all found manifest files and outputs the main.js file containing all plugin manifests
/// </summary>
public string GetJavascriptInitialization(HttpContextBase httpContext, JArray umbracoInit, JArray additionalJsFiles = null)
public string GetJavascriptInitialization(HttpContextBase httpContext, IEnumerable<string> umbracoInit, IEnumerable<string> additionalJsFiles = null)
{
var result = GetJavascriptInitializationArray(httpContext, umbracoInit, additionalJsFiles);
var files = GetScriptFiles(httpContext, umbracoInit, additionalJsFiles);
return ParseMain(
result.ToString(),
IOHelper.ResolveUrl(SystemDirectories.Umbraco));
var jarray = new StringBuilder();
jarray.AppendLine("[");
var first = true;
foreach (var file in files)
{
if (first) first = false;
else jarray.AppendLine(",");
jarray.Append("\"");
jarray.Append(file);
jarray.Append("\"");
}
jarray.Append("]");
return WriteScript(jarray.ToString(), IOHelper.ResolveUrl(SystemDirectories.Umbraco));
}
public JArray GetJavascriptInitializationArray(HttpContextBase httpContext, JArray umbracoInit, JArray additionalJsFiles = null)
public IEnumerable<string> GetScriptFiles(HttpContextBase httpContext, IEnumerable<string> umbracoInit, IEnumerable<string> additionalJsFiles = null)
{
foreach (var m in _parser.GetManifests())
{
ManifestParser.MergeJArrays(umbracoInit, m.JavaScriptInitialize);
}
//merge in the additional ones specified if there are any
var scripts = new HashSet<string>();
foreach (var script in umbracoInit)
scripts.Add(script);
foreach (var script in _parser.Manifest.Scripts)
scripts.Add(script);
if (additionalJsFiles != null)
{
ManifestParser.MergeJArrays(umbracoInit, additionalJsFiles);
}
foreach (var script in additionalJsFiles)
scripts.Add(script);
//now we can optimize if in release mode
umbracoInit = OptimizeAssetCollection(umbracoInit, ClientDependencyType.Javascript, httpContext);
scripts = new HashSet<string>(OptimizeAssetCollection(scripts, ClientDependencyType.Javascript, httpContext));
//now we need to merge in any found cdf declarations on property editors
ManifestParser.MergeJArrays(umbracoInit, ScanPropertyEditors(ClientDependencyType.Javascript, httpContext));
foreach (var script in ScanPropertyEditors(ClientDependencyType.Javascript, httpContext))
scripts.Add(script);
return umbracoInit;
return scripts.ToArray();
}
/// <summary>
/// Returns the default config as a JArray
/// </summary>
/// <returns></returns>
internal static JArray GetDefaultInitialization()
internal static IEnumerable<string> GetDefaultInitialization()
{
var init = Resources.JsInitialize;
var deserialized = JsonConvert.DeserializeObject<JArray>(init);
var result = new JArray();
foreach (var j in deserialized.Where(j => j.Type == JTokenType.String))
{
result.Add(j);
}
return result;
var resources = JsonConvert.DeserializeObject<JArray>(Resources.JsInitialize);
return resources.Where(x => x.Type == JTokenType.String).Select(x => x.ToString());
}
/// <summary>
@@ -94,22 +90,18 @@ namespace Umbraco.Web.UI.JavaScript
/// </summary>
/// <param name="replacements"></param>
/// <returns></returns>
internal static string ParseMain(params string[] replacements)
internal static string WriteScript(params string[] replacements)
{
var count = 0;
// replace, catering for the special syntax when we have
// js function() objects contained in the json
return Token.Replace(Resources.Main, match =>
{
var replaced = replacements[count];
//we need to cater for the special syntax when we have js function() objects contained in the json
var jsFunctionParsed = JsFunctionParser.Replace(replaced, "$2");
count++;
return jsFunctionParsed;
var replacement = replacements[count++];
return JsFunctionParser.Replace(replacement, "$2");
});
}
}
}