diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Controllers/AdminController.cs new file mode 100644 index 000000000..08fe6e561 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Controllers/AdminController.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using System.Web.Mvc; +using Orchard.Themes; + +namespace Orchard.Tokens.Controllers { + public class AdminController : Controller { + private readonly ITokenManager _tokenManager; + + public AdminController(ITokenManager tokenManager) { + _tokenManager = tokenManager; + } + + [Themed(false)] + public ActionResult Tokens() { + var tokenTypes = _tokenManager.Describe(Enumerable.Empty()); + var results = new List(); + + foreach (var tokenType in tokenTypes.OrderBy(d => d.Name.ToString())) + { + results.Add(new { + label = tokenType.Name.Text, + desc = tokenType.Description.Text, + value = string.Empty + }); + + foreach(var token in tokenType.Tokens) { + results.Add(new { + label = token.Name.Text, + desc = token.Description.Text, + value = "{" + token.Target + "." + token.Token + "}" + }); + } + } + + return Json(results, JsonRequestBehavior.AllowGet); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/DescribeContext.cs b/src/Orchard.Web/Modules/Orchard.Tokens/DescribeContext.cs new file mode 100644 index 000000000..5ca3f0443 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/DescribeContext.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Orchard.Localization; + +namespace Orchard.Tokens { + public abstract class DescribeContext { + public abstract IEnumerable Describe(params string[] targets); + public abstract DescribeFor For(string target); + public abstract DescribeFor For(string target, LocalizedString name, LocalizedString description); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/DescribeFor.cs b/src/Orchard.Web/Modules/Orchard.Tokens/DescribeFor.cs new file mode 100644 index 000000000..cae639f39 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/DescribeFor.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Orchard.Localization; + +namespace Orchard.Tokens { + public abstract class DescribeFor { + public abstract IEnumerable Tokens { get; } + public abstract LocalizedString Name { get; } + public abstract LocalizedString Description { get; } + public abstract DescribeFor Token(string token, LocalizedString name, LocalizedString description); + public abstract DescribeFor Token(string token, LocalizedString name, LocalizedString description, string chainTarget); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/EvaluateContext.cs b/src/Orchard.Web/Modules/Orchard.Tokens/EvaluateContext.cs new file mode 100644 index 000000000..51d2165d4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/EvaluateContext.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace Orchard.Tokens { + public abstract class EvaluateContext { + public abstract string Target { get; } + public abstract IDictionary Tokens { get; } + public abstract IDictionary Data { get; } + public abstract IDictionary Values { get; } + + public abstract EvaluateFor For(string target); + public abstract EvaluateFor For(string target, TData defaultData); + public abstract EvaluateFor For(string target, Func defaultData); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/EvaluateFor.cs b/src/Orchard.Web/Modules/Orchard.Tokens/EvaluateFor.cs new file mode 100644 index 000000000..39bda24bc --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/EvaluateFor.cs @@ -0,0 +1,11 @@ +using System; + +namespace Orchard.Tokens { + public abstract class EvaluateFor { + public abstract TData Data { get; } + public abstract EvaluateFor Token(string token, Func tokenValue); + public abstract EvaluateFor Chain(string token, string chainTarget, Func chainValue); + public abstract EvaluateFor Token(Func tokenValue); + public abstract EvaluateFor Token(Func filter, Func tokenValue); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/ITokenManager.cs b/src/Orchard.Web/Modules/Orchard.Tokens/ITokenManager.cs new file mode 100644 index 000000000..f6b77e133 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/ITokenManager.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Orchard.Tokens { + public interface ITokenManager : IDependency { + IEnumerable Describe(IEnumerable targets); + IDictionary Evaluate(string target, IDictionary tokens, IDictionary data); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/ITokenProvider.cs b/src/Orchard.Web/Modules/Orchard.Tokens/ITokenProvider.cs new file mode 100644 index 000000000..6b3344a7c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/ITokenProvider.cs @@ -0,0 +1,8 @@ +using Orchard.Events; + +namespace Orchard.Tokens { + public interface ITokenProvider : IEventHandler { + void Describe(DescribeContext context); + void Evaluate(EvaluateContext context); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/ITokenizer.cs b/src/Orchard.Web/Modules/Orchard.Tokens/ITokenizer.cs new file mode 100644 index 000000000..0167109ee --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/ITokenizer.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Orchard.Events; + +namespace Orchard.Tokens { + public interface ITokenizer : IDependency { + IDictionary Evaluate(IEnumerable tokens, object data); + IDictionary Evaluate(IEnumerable tokens, IDictionary data); + string Replace(string text, object data); + string Replace(string text, object data, ReplaceOptions options); + string Replace(string text, IDictionary data); + string Replace(string text, IDictionary data, ReplaceOptions options); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Implementation/TokenManager.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Implementation/TokenManager.cs new file mode 100644 index 000000000..b87d6f353 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Implementation/TokenManager.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Orchard.Localization; + +namespace Orchard.Tokens.Implementation { + public class TokenManager : ITokenManager { + private readonly IEnumerable _providers; + + public TokenManager(IEnumerable providers) { + _providers = providers; + } + + public IEnumerable Describe(IEnumerable targets) { + var context = new DescribeContextImpl(); + foreach (var provider in _providers) { + provider.Describe(context); + } + return context.Describe((targets ?? Enumerable.Empty()).ToArray()); + } + + public IDictionary Evaluate(string target, IDictionary tokens, IDictionary data) { + var context = new EvaluateContextImpl(target, tokens, data, this); + foreach (var provider in _providers) { + provider.Evaluate(context); + } + return context.Produce(); + } + + private class EvaluateContextImpl : EvaluateContext { + private readonly string _target; + private readonly IDictionary _tokens; + private readonly IDictionary _data; + private readonly ITokenManager _manager; + private readonly IDictionary _values = new Dictionary(); + + public EvaluateContextImpl(string target, IDictionary tokens, IDictionary data, ITokenManager manager) { + _target = target; + _tokens = tokens; + _data = data; + _manager = manager; + } + + public IDictionary Produce() { + return _values; + } + + public override string Target { + get { return _target; } + } + + public override IDictionary Tokens { + get { return _tokens; } + } + + public override IDictionary Data { + get { return _data; } + } + + public override IDictionary Values { + get { return _values; } + } + + public override EvaluateFor For(string target) { + if (_data != null && string.Equals(target, _target, StringComparison.InvariantCulture)) { + object value; + if (_data.TryGetValue(target, out value)) { + return new EvaluateForImpl(this, (TData)value); + } + } + + return new EvaluateForSilent(); + } + + public override EvaluateFor For(string target, TData defaultData) { + return For(target, () => defaultData); + } + + public override EvaluateFor For(string target, Func defaultData) { + if (string.Equals(target, _target, StringComparison.InvariantCulture)) { + var data = default(TData); + object value; + if (_data != null && _data.TryGetValue(target, out value)) { + data = (TData)value; + } + else if (defaultData != null) { + data = defaultData(); + } + + return new EvaluateForImpl(this, data); + } + + return new EvaluateForSilent(); + } + + private class EvaluateForImpl : EvaluateFor { + private readonly EvaluateContextImpl _context; + private readonly TData _data; + + public EvaluateForImpl(EvaluateContextImpl context, TData data) { + _context = context; + _data = data; + } + + public override TData Data { + get { return _data; } + } + + public override EvaluateFor Token(string token, Func tokenValue) { + string originalToken; + if (_context.Tokens.TryGetValue(token, out originalToken)) { + try { + _context.Values[originalToken] = tokenValue(_data); + } + catch (NullReferenceException) { + _context.Values[originalToken] = null; + } + } + return this; + } + + public override EvaluateFor Token(Func tokenValue) { + return Token(null, tokenValue); + } + + public override EvaluateFor Token(Func filter, Func tokenValue) { + foreach (var token in _context.Tokens) { + var tokenName = token.Key; + if (filter != null) { + tokenName = filter(token.Key); + if (tokenName == null) + continue; + } + var value = tokenValue(tokenName, _data); + if (value != null) { + _context.Values[token.Value] = value; + } + } + return this; + } + + + public override EvaluateFor Chain(string token, string chainTarget, Func chainValue) { + var subTokens = _context.Tokens + .Where(kv => kv.Key.StartsWith(token + ".")) + .ToDictionary(kv => kv.Key.Substring(token.Length + 1), kv => kv.Value); + if (!subTokens.Any()) { + return this; + } + var subValues = _context._manager.Evaluate(chainTarget, subTokens, new Dictionary { { chainTarget, chainValue(_data) } }); + foreach (var subValue in subValues) { + _context.Values[subValue.Key] = subValue.Value; + } + return this; + } + } + + private class EvaluateForSilent : EvaluateFor { + public override TData Data { + get { return default(TData); } + } + + public override EvaluateFor Token(string token, Func tokenValue) { + return this; + } + + public override EvaluateFor Token(Func tokenValue) { + return this; + } + + public override EvaluateFor Token(Func filter, Func tokenValue) { + return this; + } + + public override EvaluateFor Chain(string token, string chainTarget, Func chainValue) { + return this; + } + } + } + + private class DescribeContextImpl : DescribeContext { + private readonly Dictionary _describes = new Dictionary(); + + public override IEnumerable Describe(params string[] targets) { + return _describes + .Where(kp => targets == null || targets.Length == 0 || targets.Contains(kp.Key)) + .Select(kp => new TokenTypeDescriptor { + Target = kp.Key, + Name = kp.Value.Name, + Description = kp.Value.Description, + Tokens = kp.Value.Tokens + }); + } + + public override DescribeFor For(string target) { + return For(target, null, null); + } + + public override DescribeFor For(string target, LocalizedString name, LocalizedString description) { + DescribeFor describeFor; + if (!_describes.TryGetValue(target, out describeFor)) { + describeFor = new DescribeForImpl(target, name, description); + _describes[target] = describeFor; + } + return describeFor; + } + } + + private class DescribeForImpl : DescribeFor { + private readonly LocalizedString _name; + private readonly LocalizedString _description; + private readonly string _target; + private readonly List _tokens = new List(); + + public DescribeForImpl(string target, LocalizedString name, LocalizedString description) { + _target = target; + _name = name; + _description = description; + } + + public override LocalizedString Name { + get { + return _name; + } + } + public override LocalizedString Description { + get { + return _description; + } + } + + public override IEnumerable Tokens { + get { return _tokens; } + } + + public override DescribeFor Token(string token, LocalizedString name, LocalizedString description) { + return Token(token, name, description, null); + } + + public override DescribeFor Token(string token, LocalizedString name, LocalizedString description, string chainTarget) { + _tokens.Add(new TokenDescriptor { Token = token, Name = name, Description = description, Target = _target, ChainTarget = chainTarget }); + return this; + } + + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Implementation/Tokenizer.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Implementation/Tokenizer.cs new file mode 100644 index 000000000..de648cdb7 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Implementation/Tokenizer.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Routing; + +namespace Orchard.Tokens.Implementation { + public class Tokenizer : ITokenizer { + private readonly ITokenManager _tokenManager; + + public Tokenizer(ITokenManager tokenManager) { + _tokenManager = tokenManager; + } + + public IDictionary Evaluate(IEnumerable tokens, object data) { + return Evaluate(tokens, new RouteValueDictionary(data)); + } + + public IDictionary Evaluate(IEnumerable tokens, IDictionary data) { + var distinctTokens = tokens.Distinct().ToList(); + var replacements = distinctTokens.ToDictionary(s => s, s => (object)null); + return distinctTokens + .Select(Split) + .GroupBy(item => item.Item1) + .SelectMany(grouping => _tokenManager.Evaluate(grouping.Key, grouping.ToDictionary(item => item.Item2, item => item.Item3), data)) + .Aggregate(replacements, (agg, kv) => { + agg[kv.Key] = kv.Value; + return agg; + }); + } + + public string Replace(string text, object data) { + return Replace(text, data, ReplaceOptions.Default); + } + + public string Replace(string text, object data, ReplaceOptions options) { + return Replace(text, new RouteValueDictionary(data), options); + } + + public string Replace(string text, IDictionary data) { + return Replace(text, data, ReplaceOptions.Default); + } + + public string Replace(string text, IDictionary data, ReplaceOptions options) { + var tokenset = Parse(text); + var tokens = tokenset.Item2; + var replacements = Evaluate(options.Predicate == null ? tokens : tokens.Where(options.Predicate), data); + + return replacements.Aggregate(tokenset.Item1, + (current, replacement) => current.Replace("{" + replacement.Key + "}", (options.Encoding ?? ReplaceOptions.NoEncode)(replacement.Key, replacement.Value))); + } + + private static Tuple> Parse(string text) { + var tokens = new List(); + if (!string.IsNullOrEmpty(text)) { + var inToken = false; + var tokenStart = 0; + for (var i = 0; i < text.Length; i++) { + var c = text[i]; + if (c == '{') { + if (i + 1 < text.Length && text[i + 1] == '{') { + text = text.Substring(0, i) + text.Substring(i + 1); + continue; + } + } + else if (c == '}') { + if (i + 1 < text.Length && text[i + 1] == '}') { + text = text.Substring(0, i) + text.Substring(i + 1); + continue; + } + } + + if (inToken) { + if (c == '}') { + inToken = false; + var token = text.Substring(tokenStart + 1, i - tokenStart - 1); + tokens.Add(token); + } + } + else if (c == '{') { + inToken = true; + tokenStart = i; + } + } + } + return new Tuple>(text, tokens); + } + + private static Tuple Split(string token) { + var dotIndex = token.IndexOf('.'); + if (dotIndex != -1) { + return Tuple.Create(token.Substring(0, dotIndex), token.Substring(dotIndex + 1), token); + } + return Tuple.Create(token, "", token); + } + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Module.txt b/src/Orchard.Web/Modules/Orchard.Tokens/Module.txt new file mode 100644 index 000000000..58548912c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Module.txt @@ -0,0 +1,12 @@ +Name: Tokens +AntiForgery: enabled +Author: The Orchard Team +Website: http://orchardtokens.codeplex.com +Version: 1.5 +OrchardVersion: 1.4 +Description: Provides a system for performing string replacements with common site values. +Features: + Orchard.Tokens: + Name: Tokens + Description: Provides a system for performing string replacements with common site values. + Category: Content \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Orchard.Tokens.csproj b/src/Orchard.Web/Modules/Orchard.Tokens/Orchard.Tokens.csproj new file mode 100644 index 000000000..8d89d4f02 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Orchard.Tokens.csproj @@ -0,0 +1,161 @@ + + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {6F759635-13D7-4E94-BCC9-80445D63F117} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + Orchard.Tokens + Orchard.Tokens + v4.0 + false + + + 4.0 + + + + false + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + + + pdbonly + true + bin\ + TRACE + prompt + 4 + AllRules.ruleset + + + + + + + 3.5 + + + + + False + ..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll + + + + + + + + + + + + + + + + + + + + + {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} + Orchard.Framework + + + {9916839C-39FC-4CEB-A5AF-89CA7E87119F} + Orchard.Core + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + $(ProjectDir)\..\Manifests + + + + + + + + + + + + False + True + 45979 + / + + + False + True + http://orchard.codeplex.com + False + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..f791b6746 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Orchard.Tokens")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyProduct("Orchard")] +[assembly: AssemblyCopyright("Copyright © Outercurve Foundation 2009")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("03d51a53-fc12-4a54-9c02-4f233d35c057")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.5")] +[assembly: AssemblyFileVersion("1.5")] + diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Providers/ContentTokens.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Providers/ContentTokens.cs new file mode 100644 index 000000000..48fba8b44 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Providers/ContentTokens.cs @@ -0,0 +1,155 @@ +using System.Linq; +using System.Web; +using System.Web.Mvc; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Aspects; +using Orchard.ContentManagement.MetaData.Models; +using Orchard.Core.Common.Fields; +using Orchard.Localization; +using Orchard.ContentManagement.FieldStorage; +using Orchard.Mvc.Extensions; + +namespace Orchard.Tokens.Providers { + public class ContentTokens : ITokenProvider { + private readonly IContentManager _contentManager; + private readonly IWorkContextAccessor _workContextAccessor; + + public ContentTokens(IContentManager contentManager, IWorkContextAccessor workContextAccessor) { + _contentManager = contentManager; + _workContextAccessor = workContextAccessor; + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public void Describe(DescribeContext context) { + context.For("Content", T("Content Items"), T("Content Items")) + .Token("Id", T("Content Id"), T("Numeric primary key value of content.")) + .Token("Author", T("Content Author"), T("Person in charge of the content."), "User") + .Token("Date", T("Content Date"), T("Date the content was created."), "DateTime") + .Token("Identity", T("Identity"), T("Identity of the content.")) + .Token("ContentType", T("Content Type"), T("The name of the item Content Type."), "TypeDefinition") + .Token("DisplayText", T("Display Text"), T("Title of the content."),"Text") + .Token("DisplayUrl", T("Display Url"), T("Url to display the content."), "Url") + .Token("EditUrl", T("Edit Url"), T("Url to edit the content."), "Url") + .Token("Container", T("Container"), T("The container Content Item."), "Content") + ; + + // Token descriptors for fields + foreach(var typeDefinition in _contentManager.GetContentTypeDefinitions()) { + foreach (var typePart in typeDefinition.Parts) { + + if(!typePart.PartDefinition.Fields.Any()) { + continue; + } + + var partContext = context.For("Content"); + foreach (var partField in typePart.PartDefinition.Fields) { + var field = partField; + var tokenName = "Fields." + typePart.PartDefinition.Name + "." + field.Name; + + // the token is chained with the technical name + partContext.Token(tokenName, T("{0} {1}", typePart.PartDefinition.Name, field.Name), T("The content of the {0} field.", partField.DisplayName), field.Name); + } + } + } + + context.For("TextField", T("Text Field"), T("Tokens for Text Fields")) + .Token("Length", T("Length"), T("The length of the field.")); + + context.For("Url", T("Url"), T("Tokens for Urls")) + .Token("Absolute", T("Absolute"), T("Absolute url.")); + + context.For("TypeDefinition", T("Type Definition"), T("Tokens for Content Types")) + .Token("Name", T("Name"), T("Name of the content type.")) + .Token("DisplayName", T("Display Name"), T("Display name of the content type."), "Text") + .Token("Parts", T("Parts"), T("List of the attached part names.")) + .Token("Fields", T("Fields"), T("Fields for each of the attached parts. For example, Fields.Page.Approved.")); + } + + public void Evaluate(EvaluateContext context) { + context.For("Content") + .Token("Id", content => content.Id) + .Token("Author", AuthorName) + .Chain("Author", "User", content => content.As().Owner) + .Token("Date", content => content.As().CreatedUtc) + .Chain("Date", "Date", content => content.As().CreatedUtc) + .Token("Identity", content => _contentManager.GetItemMetadata(content).Identity.ToString()) + .Token("ContentType", content => content.ContentItem.TypeDefinition.DisplayName) + .Chain("ContentType", "TypeDefinition", content => content.ContentItem.TypeDefinition) + .Token("DisplayText", content => _contentManager.GetItemMetadata(content).DisplayText) + .Chain("DisplayText", "Text", content => _contentManager.GetItemMetadata(content).DisplayText) + .Token("DisplayUrl", content => new UrlHelper(_workContextAccessor.GetContext().HttpContext.Request.RequestContext).RouteUrl(_contentManager.GetItemMetadata(content).DisplayRouteValues)) + .Chain("DisplayUrl", "Url", content => new UrlHelper(_workContextAccessor.GetContext().HttpContext.Request.RequestContext).RouteUrl(_contentManager.GetItemMetadata(content).DisplayRouteValues)) + .Token("EditUrl", content => new UrlHelper(_workContextAccessor.GetContext().HttpContext.Request.RequestContext).RouteUrl(_contentManager.GetItemMetadata(content).EditorRouteValues)) + .Chain("EditUrl", "Url", content => new UrlHelper(_workContextAccessor.GetContext().HttpContext.Request.RequestContext).RouteUrl(_contentManager.GetItemMetadata(content).EditorRouteValues)) + .Token("Container", content => { + var container = Container(content); + if(container == null) { + return string.Empty; + } + return _contentManager.GetItemMetadata(container).DisplayText; + }) + .Chain("Container", "Content", content => Container(content)) + ; + + if (context.Target == "Content") { + var forContent = context.For("Content"); + // is there a content available in the context ? + if (forContent != null && forContent.Data != null && forContent.Data.ContentItem != null) { + foreach (var typePart in forContent.Data.ContentItem.TypeDefinition.Parts) { + var part = typePart; + foreach (var partField in typePart.PartDefinition.Fields) { + var field = partField; + var tokenName = "Fields." + typePart.PartDefinition.Name + "." + partField.Name; + forContent.Token( + tokenName, + content => LookupField(content, part.PartDefinition.Name, field.Name).Storage.Get()); + forContent.Chain( + tokenName, + partField.FieldDefinition.Name, + content => LookupField(content, part.PartDefinition.Name, field.Name)); + } + } + } + } + + context.For("Url") + .Token("Absolute", url => new UrlHelper(_workContextAccessor.GetContext().HttpContext.Request.RequestContext).MakeAbsolute(url)); + + context.For("TextField") + .Token("Length", field => (field.Value ?? "").Length); + + context.For("TypeDefinition") + .Token("Name", def => def.Name) + .Token("DisplayName", def => def.DisplayName) + .Chain("DisplayName", "Text", def => def.DisplayName) + .Token("Parts", def => string.Join(", ", def.Parts.Select(x => x.PartDefinition.Name).ToArray())) + .Token("Fields", def => string.Join(", ", def.Parts.SelectMany(x => x.PartDefinition.Fields.Select(x2 => x2.FieldDefinition.Name + " " + x.PartDefinition.Name + "." + x2.Name)).ToArray())); + } + + private IHtmlString AuthorName(IContent content) { + var commonPart = content.As(); + var author = commonPart != null ? commonPart.Owner : null; + // todo: encoding should be done at a higher level automatically and should be configurable via an options param + // so it can be disabled + return author == null ? (IHtmlString)T("Anonymous") : new HtmlString(HttpUtility.HtmlEncode(author.UserName)); + } + + private static ContentField LookupField(IContent content, string partName, string fieldName) { + return content.ContentItem.Parts + .Where(part => part.PartDefinition.Name == partName) + .SelectMany(part => part.Fields.Where(field => field.Name == fieldName)) + .SingleOrDefault(); + } + + private IContent Container(IContent content) { + var commonPart = content.As(); + if(commonPart == null) { + return null; + } + + return commonPart.Container; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Providers/DateTokens.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Providers/DateTokens.cs new file mode 100644 index 000000000..e96e6faf2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Providers/DateTokens.cs @@ -0,0 +1,83 @@ +using System; +using Orchard.Core.Shapes.Localization; +using Orchard.Localization; +using Orchard.Mvc.Html; +using Orchard.Services; +using System.Globalization; + +namespace Orchard.Tokens.Providers { + public class DateTokens : ITokenProvider { + private readonly IClock _clock; + private readonly IDateTimeLocalization _dateTimeLocalization; + private readonly IWorkContextAccessor _workContextAccessor; + private readonly Lazy _cultureInfo; + private readonly Lazy _timeZone; + + public DateTokens( + IClock clock, + IDateTimeLocalization dateTimeLocalization, + IWorkContextAccessor workContextAccessor) { + _clock = clock; + _dateTimeLocalization = dateTimeLocalization; + _workContextAccessor = workContextAccessor; + + _cultureInfo = new Lazy(() => CultureInfo.GetCultureInfo(_workContextAccessor.GetContext().CurrentCulture)); + _timeZone = new Lazy(() => _workContextAccessor.GetContext().CurrentTimeZone); + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public void Describe(DescribeContext context) { + context.For("Date", T("Date/time"), T("Current date/time tokens")) + .Token("Since", T("Since"), T("Relative to the current date/time.")) + .Token("Local", T("Local"), T("Based on the configured time zone.")) + .Token("ShortDate", T("Short Date"), T("Short date format.")) + .Token("ShortTime", T("Short Time"), T("Short time format.")) + .Token("Long", T("Long Date and Time"), T("Long date and time format.")) + .Token("Format:*", T("Format:"), T("Optional format specifier (e.g. yyyy/MM/dd). See format strings at Standard Formats and Custom Formats"), "DateTime"); + } + + public void Evaluate(EvaluateContext context) { + context.For("Date", () => _clock.UtcNow) + // {Date.Since} + .Token("Since", DateTimeRelative) + .Chain("Since", "Date", DateTimeRelative) + // {Date.Local} + .Token("Local", d => TimeZoneInfo.ConvertTimeFromUtc(d, _timeZone.Value)) + .Chain("Local", "Date", d => TimeZoneInfo.ConvertTimeFromUtc(d, _timeZone.Value)) + // {Date.ShortDate} + .Token("ShortDate", d => d.ToString(_dateTimeLocalization.ShortDateFormat.Text, _cultureInfo.Value)) + // {Date.ShortTime} + .Token("ShortTime", d => d.ToString(_dateTimeLocalization.ShortTimeFormat.Text, _cultureInfo.Value)) + // {Date.Long} + .Token("Long", d => d.ToString(_dateTimeLocalization.LongDateTimeFormat.Text, _cultureInfo.Value)) + // {Date} + .Token( + token => token == String.Empty ? String.Empty : null, + (token, d) => d.ToString(_dateTimeLocalization.ShortDateFormat.Text + " " + _dateTimeLocalization.ShortTimeFormat.Text, _cultureInfo.Value)) + // {Date.Format:} + .Token( + token => token.StartsWith("Format:", StringComparison.OrdinalIgnoreCase) ? token.Substring("Format:".Length) : null, + (token, d) => d.ToString(token, _cultureInfo.Value)); + } + + private string DateTimeRelative(DateTime dateTimeUtc) { + var time = _clock.UtcNow - dateTimeUtc.ToUniversalTime(); + + if (time.TotalDays > 7) + return dateTimeUtc.ToString(T("'on' MMM d yyyy 'at' h:mm tt").ToString(), _cultureInfo.Value); + if (time.TotalHours > 24) + return T.Plural("1 day ago", "{0} days ago", time.Days).ToString(); + if (time.TotalMinutes > 60) + return T.Plural("1 hour ago", "{0} hours ago", time.Hours).ToString(); + if (time.TotalSeconds > 60) + return T.Plural("1 minute ago", "{0} minutes ago", time.Minutes).ToString(); + if (time.TotalSeconds > 10) + return T.Plural("1 second ago", "{0} seconds ago", time.Seconds).ToString(); + + return T("a moment ago").ToString(); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Providers/RequestTokens.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Providers/RequestTokens.cs new file mode 100644 index 000000000..24f5a34c3 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Providers/RequestTokens.cs @@ -0,0 +1,38 @@ +using System; +using Orchard.Localization; + +namespace Orchard.Tokens.Providers { + public class RequestTokens : ITokenProvider { + private readonly IWorkContextAccessor _workContextAccessor; + + public RequestTokens(IWorkContextAccessor workContextAccessor) { + _workContextAccessor = workContextAccessor; + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public void Describe(DescribeContext context) { + context.For("Request", T("Http Request"), T("Current Http Request tokens.")) + .Token("QueryString:*", T("QueryString:"), T("The Query String value for the specified element.")) + .Token("Form:*", T("Form:"), T("The Form value for the specified element.")) + ; + } + + public void Evaluate(EvaluateContext context) { + if (_workContextAccessor.GetContext().HttpContext == null) { + return; + } + + context.For("Request", _workContextAccessor.GetContext().HttpContext.Request) + .Token( + token => token.StartsWith("QueryString:", StringComparison.OrdinalIgnoreCase) ? token.Substring("QueryString:".Length) : null, + (token, request) => request.QueryString.Get(token) + ) + .Token( + token => token.StartsWith("Form:", StringComparison.OrdinalIgnoreCase) ? token.Substring("Form:".Length) : null, + (token, request) => request.Form.Get(token) + ); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Providers/TextTokens.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Providers/TextTokens.cs new file mode 100644 index 000000000..243986552 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Providers/TextTokens.cs @@ -0,0 +1,70 @@ +using System; +using System.Web; +using Orchard.Localization; + +namespace Orchard.Tokens.Providers { + public class TextTokens : ITokenProvider { + public TextTokens() { + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public void Describe(DescribeContext context) { + context.For("Text", T("Text"), T("Tokens for text strings")) + .Token("Limit:*", T("Limit:[,]"), T("Limit text to specified length and append an optional ellipsis text.")) + .Token("Format:*", T("Format:"), T("Optional format specifier (e.g. foo{0}bar). See format strings at Standard Formats and Custom Formats"), "DateTime") + .Token("UrlEncode", T("Url Encode"), T("Encodes a URL string."), "Text") + .Token("HtmlEncode", T("Html Encode"), T("Encodes an HTML string."), "Text") + .Token("LineEncode", T("Line Encode"), T("Replaces new lines with
tags.")) + ; + } + + public void Evaluate(EvaluateContext context) { + context.For("Text", () => "") + .Token( // {Text} + token => token == String.Empty ? String.Empty : null, + (token, d) => d.ToString()) + .Token( // {Text.Limit:[,]} + token => { + if (token.StartsWith("Limit:", StringComparison.OrdinalIgnoreCase)) { + var param = token.Substring("Limit:".Length); + return param; + } + return null; + }, + (token, t) => Limit(t, token)) + // {Text.Format:} + .Token( + token => token.StartsWith("Format:", StringComparison.OrdinalIgnoreCase) ? token.Substring("Format:".Length) : null, + (token, d) => String.Format(d,token)) + .Token("UrlEncode", HttpUtility.UrlEncode) + .Token("HtmlEncode", HttpUtility.HtmlEncode) + .Token("LineEncode", text => text.Replace(System.Environment.NewLine, "
")) + ; + + } + + private string Limit(string token, string param) { + if(String.IsNullOrEmpty(token)) { + return String.Empty; + } + + var index = param.IndexOf(','); + + // no ellipsis + if (index == -1) { + var limit = Int32.Parse(param); + token = token.Substring(0, limit); + } + else { + var limit = Int32.Parse(param.Substring(0, index)); + var ellipsis = param.Substring(index + 1); + token = token.Substring(0, limit) + ellipsis; + } + + return token; + } + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Providers/UserTokens.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Providers/UserTokens.cs new file mode 100644 index 000000000..cffb4641b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Providers/UserTokens.cs @@ -0,0 +1,54 @@ +using Orchard.Localization; +using Orchard.Security; + +namespace Orchard.Tokens.Providers { + public class UserTokens : ITokenProvider { + private readonly IOrchardServices _orchardServices; + private static readonly IUser _anonymousUser = new AnonymousUser(); + + public UserTokens(IOrchardServices orchardServices) { + _orchardServices = orchardServices; + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public void Describe(DescribeContext context) { + context.For("User", T("User"), T("User tokens")) + .Token("Name", T("Name"), T("Username")) + .Token("Email", T("Email"), T("Email Address")) + .Token("Id", T("Id"), T("User Id")) + .Token("Content", T("Content"), T("The user's content item")); + } + + public void Evaluate(EvaluateContext context) { + context.For("User", () => _orchardServices.WorkContext.CurrentUser ?? _anonymousUser) + .Token("Name", u => u.UserName) + .Token("Email", u => u.Email) + .Token("Id", u => u.Id) + .Chain("Content", "Content", u => u.ContentItem); + // todo: cross-module dependency -- should be provided by the User module? + //.Token("Roles", user => string.Join(", ", user.As().Roles.ToArray())); + } + + public class AnonymousUser : IUser { + public string UserName { + get { return "Anonymous"; } + } + + public string Email { + get { return string.Empty; } + } + + public ContentManagement.ContentItem ContentItem { + get { return null; } + } + + public int Id { + get { return -1; } + } + } + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/ReplaceOptions.cs b/src/Orchard.Web/Modules/Orchard.Tokens/ReplaceOptions.cs new file mode 100644 index 000000000..ca3527249 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/ReplaceOptions.cs @@ -0,0 +1,28 @@ +using System; +using System.Web; + +namespace Orchard.Tokens { + public class ReplaceOptions { + public Func Predicate { get; set; } + public Func Encoding { get; set; } + + public static string HtmlEncode(string token, object value) { + return HttpUtility.HtmlEncode(value); + } + + public static string NoEncode(string token, object value) { + return Convert.ToString(value); + } + + public static string UrlEncode(string token, object value) { + return HttpUtility.UrlEncode(value.ToString()); + } + + public static ReplaceOptions Default { + get { + return new ReplaceOptions { Encoding = HtmlEncode }; + } + } + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Scripts/Web.config b/src/Orchard.Web/Modules/Orchard.Tokens/Scripts/Web.config new file mode 100644 index 000000000..770adfab5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Scripts/Web.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Scripts/orchard-tokens-admin.js b/src/Orchard.Web/Modules/Orchard.Tokens/Scripts/orchard-tokens-admin.js new file mode 100644 index 000000000..c7c5410d6 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Scripts/orchard-tokens-admin.js @@ -0,0 +1,67 @@ +jQuery.fn.extend({ + insertAtCaret: function (myValue) { + return this.each(function(i) { + if (document.selection) { + //For browsers like Internet Explorer + this.focus(); + sel = document.selection.createRange(); + sel.text = myValue; + this.focus(); + } else if (this.selectionStart || this.selectionStart == '0') { + //For browsers like Firefox and Webkit based + var startPos = this.selectionStart; + var endPos = this.selectionEnd; + var scrollTop = this.scrollTop; + this.value = this.value.substring(0, startPos) + myValue + this.value.substring(endPos, this.value.length); + this.focus(); + this.selectionStart = startPos + myValue.length; + this.selectionEnd = startPos + myValue.length; + this.scrollTop = scrollTop; + } else { + this.value += myValue; + this.focus(); + } + }); + } +}); + +jQuery(function ($) { + + // provide autocomplete behavior to tokenized inputs + // tokensUrl is initialized from the view + $.get(tokensUrl, function (data) { + $('.tokenized') + .autocomplete({ + minLength: 0, + source: data, + select: function (event, ui) { + $(this).insertAtCaret(ui.item.value); + return false; + } + }).each(function () { + $(this).data('autocomplete')._renderItem = function (ul, item) { + var result = item.value == '' ? $('
  • ') : $("
  • "); + + var desc = item.desc.length > 50 ? item.desc.substring(0, 50) + "..." : item.desc; + + return result + .data("item.autocomplete", item) + .append('' + item.label + ' ' + item.value + ' ' + desc + "") + .appendTo(ul); + }; + }); + }); + + // add an icon to tokenized inputs + $('.tokenized').wrap(''); + + $('.token-wrapper').prepend('
     
    '); + + // show the full list of tokens when the icon is clicked + $('.tokenized-popup').click(function () { + var input = $(this).parent().next(); + // pass empty string as value to search for, displaying all results + input.autocomplete("search", ""); + input.focus(); + }); +}); diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Styles/Images/closeButtons.png b/src/Orchard.Web/Modules/Orchard.Tokens/Styles/Images/closeButtons.png new file mode 100644 index 000000000..37313a5f1 Binary files /dev/null and b/src/Orchard.Web/Modules/Orchard.Tokens/Styles/Images/closeButtons.png differ diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Styles/Images/tokensPopup.gif b/src/Orchard.Web/Modules/Orchard.Tokens/Styles/Images/tokensPopup.gif new file mode 100644 index 000000000..3b151e859 Binary files /dev/null and b/src/Orchard.Web/Modules/Orchard.Tokens/Styles/Images/tokensPopup.gif differ diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Styles/Web.config b/src/Orchard.Web/Modules/Orchard.Tokens/Styles/Web.config new file mode 100644 index 000000000..56fa41e3e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Styles/Web.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Styles/orchard-tokens-admin.css b/src/Orchard.Web/Modules/Orchard.Tokens/Styles/orchard-tokens-admin.css new file mode 100644 index 000000000..17f72764c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Styles/orchard-tokens-admin.css @@ -0,0 +1,64 @@ +.tokenized-popup +{ + position:absolute; + background: url('images/tokensPopup.gif'); + background-repeat: no-repeat; + width:10px; + height:10px; + cursor: pointer; + + padding-right:8px; + margin-top:10px; + right:100%; +} + +.token-wrapper { + position:relative; +} + +.token-wrapper > div { + float:left; + left:100%; + position: absolute; + margin-bottom: 0px; +} + +.ui-autocomplete +{ + max-height: 12em; + background: white; + border: 1px solid #BDBCBC; + width: 700px; + overflow :auto; +} + +.ui-autocomplete .acdesc +{ + padding-right: 5px; + float: right; +} + +.ui-autocomplete .acvalue +{ + left: 150px; + position: absolute; +} + +.ui-autocomplete .accategory +{ + font-weight: bold; +} + +.ui-autocomplete li a { + + padding-left: 5px; + background-color: transparent; + color: #333; +} + +.ui-autocomplete li:hover { + background-color: #eee; + color: white; + cursor: pointer; +} + diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Tests/DateTokenTests.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/DateTokenTests.cs new file mode 100644 index 000000000..fde7de969 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/DateTokenTests.cs @@ -0,0 +1,55 @@ +using System; +using System.Globalization; +using Autofac; +using NUnit.Framework; +using Orchard.Core.Shapes.Localization; +using Orchard.Services; +using Orchard.Tokens.Implementation; +using Orchard.Tokens.Providers; + +namespace Orchard.Tokens.Tests { + [TestFixture] + public class DateTokenTests { + private IContainer _container; + private ITokenizer _tokenizer; + private IClock _clock; + + [SetUp] + public void Init() { + var builder = new ContainerBuilder(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + _container = builder.Build(); + _tokenizer = _container.Resolve(); + _clock = _container.Resolve(); + } + + [Test] + public void TestDateTokens() { + var dateTimeLocalization = _container.Resolve(); + var culture = CultureInfo.GetCultureInfo(_container.Resolve().WorkContext.CurrentCulture); + + var dateTimeFormat = dateTimeLocalization.ShortDateFormat.Text + " " + dateTimeLocalization.ShortTimeFormat.Text; + + Assert.That(_tokenizer.Replace("{Date}", null), Is.EqualTo(_clock.UtcNow.ToString(dateTimeFormat, culture))); + Assert.That(_tokenizer.Replace("{Date}", new { Date = new DateTime(1978, 11, 15, 0, 0, 0, DateTimeKind.Utc) }), Is.EqualTo(new DateTime(1978, 11, 15, 0, 0, 0, DateTimeKind.Utc).ToString(dateTimeFormat, culture))); + } + + [Test] + public void TestFormat() { + Assert.That(_tokenizer.Replace("{Date.Format:yyyy}", null), Is.EqualTo(_clock.UtcNow.ToString("yyyy"))); + } + + [Test] + public void TestSince() { + var date = _clock.UtcNow.Subtract(TimeSpan.FromHours(25)); + Assert.That(_tokenizer.Replace("{Date.Since}", new { Date = date }), Is.EqualTo("1 day ago")); + } + + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Tests/Orchard.Tokens.Tests.csproj b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/Orchard.Tokens.Tests.csproj new file mode 100644 index 000000000..54ed84b01 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/Orchard.Tokens.Tests.csproj @@ -0,0 +1,83 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {E07AFA7E-7B36-44C3-A537-AFCCAA93EA7A} + Library + Properties + Orchard.Tokens.Tests + Orchard.Tokens.Tests + v4.0 + 512 + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + False + ..\..\..\..\..\lib\autofac\Autofac.dll + + + ..\..\..\..\..\lib\nunit\nunit.framework.dll + + + + + + + + + + + + + + + + + + + + + + + + {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} + Orchard.Framework + + + {9916839C-39FC-4CEB-A5AF-89CA7E87119F} + Orchard.Core + + + {6F759635-13D7-4E94-BCC9-80445D63F117} + Orchard.Tokens + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Tests/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..5a40825fa --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Orchard.Tokens.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Orchard.Tokens.Tests")] +[assembly: AssemblyCopyright("Copyright © 2011")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("965bf918-72ab-4333-a0dc-2e435260978b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.4.1")] +[assembly: AssemblyFileVersion("1.4.1")] diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Tests/StubClock.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/StubClock.cs new file mode 100644 index 000000000..890303d0e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/StubClock.cs @@ -0,0 +1,30 @@ +using System; +using Orchard.Caching; +using Orchard.Services; + +namespace Orchard.Tokens.Tests { + public class StubClock : IClock { + public StubClock() { + UtcNow = new DateTime(2009, 10, 14, 12, 34, 56, DateTimeKind.Utc); + } + + public DateTime UtcNow { get; private set; } + + public void Advance(TimeSpan span) { + UtcNow = UtcNow.Add(span); + } + + public DateTime FutureMoment(TimeSpan span) { + return UtcNow.Add(span); + } + + + public IVolatileToken When(TimeSpan duration) { + return new Clock.AbsoluteExpirationToken(this, duration); + } + + public IVolatileToken WhenUtc(DateTime absoluteUtc) { + return new Clock.AbsoluteExpirationToken(this, absoluteUtc); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Tests/StubOrchardServices.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/StubOrchardServices.cs new file mode 100644 index 000000000..4e1d4048c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/StubOrchardServices.cs @@ -0,0 +1,49 @@ +using System; +using Autofac; +using Orchard.ContentManagement; +using Orchard.Data; +using Orchard.Security; +using Orchard.UI.Notify; + +namespace Orchard.Tokens.Tests { + public class StubOrchardServices : IOrchardServices { + private readonly ILifetimeScope _lifetimeScope; + + public StubOrchardServices() { } + + public StubOrchardServices(ILifetimeScope lifetimeScope) { + _lifetimeScope = lifetimeScope; + } + + public IContentManager ContentManager { + get { throw new NotImplementedException(); } + } + + public ITransactionManager TransactionManager { + get { throw new NotImplementedException(); } + } + + public IAuthorizer Authorizer { + get { throw new NotImplementedException(); } + } + + public INotifier Notifier { + get { throw new NotImplementedException(); } + } + + public dynamic New { + get { throw new NotImplementedException(); } + } + + private WorkContext _workContext; + public WorkContext WorkContext { + get { + if (_workContext == null) { + _workContext = new StubWorkContextAccessor(_lifetimeScope).GetContext(); + } + + return _workContext; + } + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Tests/StubWorkContextAccessor.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/StubWorkContextAccessor.cs new file mode 100644 index 000000000..92497e43f --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/StubWorkContextAccessor.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Web; +using Autofac; +using Orchard.ContentManagement; +using Orchard.Security; +using Orchard.Settings; +using System.Globalization; + +namespace Orchard.Tokens.Tests { + public class StubWorkContextAccessor : IWorkContextAccessor { + private readonly ILifetimeScope _lifetimeScope; + private readonly WorkContext _workContext; + + public StubWorkContextAccessor(ILifetimeScope lifetimeScope) { + _lifetimeScope = lifetimeScope; + _workContext = new WorkContextImpl(_lifetimeScope); + } + + public class WorkContextImpl : WorkContext { + private readonly ILifetimeScope _lifetimeScope; + private readonly Dictionary _contextDictonary; + + public delegate void MyInitMethod(WorkContextImpl workContextImpl); + + public static MyInitMethod _initMethod; + + public WorkContextImpl(ILifetimeScope lifetimeScope) { + _contextDictonary = new Dictionary(); + CurrentUser = new StubUser(); + var ci = new ContentItem(); + ci.Weld(new StubSite()); + CurrentSite = ci.As(); + _lifetimeScope = lifetimeScope; + + if (_initMethod != null) { + _initMethod(this); + } + + _contextDictonary["CurrentTimeZone"] = TimeZoneInfo.Local; + _contextDictonary["CurrentCulture"] = "en-US"; + } + + public class StubSite : ContentPart, ISite { + public static string DefaultSuperUser; + + public string PageTitleSeparator { + get { throw new NotImplementedException(); } + } + + public string SiteName { + get { throw new NotImplementedException(); } + } + + public string SiteSalt { + get { throw new NotImplementedException(); } + } + + public string SuperUser { + get { return DefaultSuperUser; } + } + + public string HomePage { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + public string SiteCulture { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + public ResourceDebugMode ResourceDebugMode { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + public int PageSize { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + public string BaseUrl { get; set; } + + public string SiteTimeZone { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + } + + public class StubUser : IUser { + public ContentItem ContentItem { + get { throw new NotImplementedException(); } + } + + public int Id { + get { return 5; } + } + + public string UserName { + get { return "Fake"; } + } + + public string Email { + get { return "Fake@fake.com"; } + } + } + + public override T Resolve() { + return _lifetimeScope.Resolve(); + } + + public override bool TryResolve(out T service) { + return _lifetimeScope.TryResolve(out service); + } + + public override T GetState(string name) { + return (T)_contextDictonary[name]; + } + + public override void SetState(string name, T value) { + _contextDictonary[name] = value; + } + } + + public WorkContext GetContext(HttpContextBase httpContext) { + return _workContext; + } + + public IWorkContextScope CreateWorkContextScope(HttpContextBase httpContext) { + throw new NotSupportedException(); + } + + public WorkContext GetContext() { + return _workContext; + } + + public IWorkContextScope CreateWorkContextScope() { + throw new NotSupportedException(); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Tests/TestTokenProvider.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/TestTokenProvider.cs new file mode 100644 index 000000000..105f705d4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/TestTokenProvider.cs @@ -0,0 +1,58 @@ +using System; +using Orchard.ContentManagement; +using Orchard.Localization; +using Orchard.Security; + +namespace Orchard.Tokens.Tests { + public class TestTokenProvider : ITokenProvider { + public TestTokenProvider() { + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public void Describe(DescribeContext context) { + context.For("Site") + .Token("Global1", T("Global1"), T("description of token1")) + .Token("Global2", T("Global2"), T("description of token2")) + .Token("Global3", T("Global3"), T("description of token3")) + .Token("CurrentUser", T("Current User"), T("The current user"), "User"); + + context.For("User") + .Token("Name", T("Name"), T("Their user name")) + .Token("Birthdate", T("Birthdate"), T("Date of birth"), "DateTime"); + + context.For("Date") + .Token("Now", T("Now"), T("Current system date in short date format. You can chain a .NET DateTime format string to customize.")); + } + + public void Evaluate(EvaluateContext context) { + context.For("Site", null) + .Token("Global1", o => "[global1]") + .Token("Global2", o => "[global2]") + .Token("Global3", o => "[global3]") + .Token("CurrentUser", o => new TestUser { UserName = "CurrentUser" }) + .Chain("CurrentUser", "User", o => new TestUser { UserName = "CurrentUser" }); + + context.For("User", () => new TestUser { UserName = "CurrentUser" }) + .Token("Name", u => u.UserName) + .Token("Birthdate", u => "Nov 15") + .Chain("Birthdate", "DateTime", u => new DateTime(1978, 11, 15)); + + context.For("Date", null) + .Token("Now", o => DateTime.Now.ToShortDateString()) + .Chain("Now", "DateTime", o => DateTime.Now); + + context.For("DateTime") + .Token((token, value) => value.ToString(token)); + } + + } + + public class TestUser : IUser { + public string UserName { get; set; } + public string Email { get; set; } + public ContentItem ContentItem { get; set; } + public int Id { get; set; } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Tests/TokenManagerTests.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/TokenManagerTests.cs new file mode 100644 index 000000000..341e5ed37 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/TokenManagerTests.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Linq; +using Autofac; +using NUnit.Framework; +using Orchard.Tokens.Implementation; + +namespace Orchard.Tokens.Tests { + [TestFixture] + public class TokenManagerTests { + private IContainer _container; + private ITokenManager _tokenManager; + + [SetUp] + public void Init() { + var builder = new ContainerBuilder(); + builder.RegisterType().As(); + builder.RegisterType().As(); + _container = builder.Build(); + _tokenManager = _container.Resolve(); + } + + [Test] + public void TestEvaluate() { + var tokens = _tokenManager.Evaluate("Site", new Dictionary { { "Global1", "Site.Global1" }, { "Global2", "Site.Global2" } }, null); + Assert.That(tokens["Site.Global1"], Is.EqualTo("[global1]")); + Assert.That(tokens["Site.Global2"], Is.EqualTo("[global2]")); + } + + [Test] + public void TestDescribe() { + var allTokens = _tokenManager.Describe(null); + Assert.That(allTokens.Count(), Is.EqualTo(3)); + Assert.That(allTokens.Any(d => d.Target == "Site")); + Assert.That(allTokens.Any(d => d.Target == "User")); + Assert.That(allTokens.Any(d => d.Target == "Date")); + + var tokens = allTokens.Single(d => d.Target == "Site").Tokens; + Assert.That(string.Join(",", tokens.Select(td => td.Target)), Is.EqualTo("Site,Site,Site,Site")); + Assert.That(string.Join(",", tokens.Select(td => td.Token)), Is.EqualTo("Global1,Global2,Global3,CurrentUser")); + Assert.That(string.Join(",", tokens.Select(td => td.Name.ToString())), Is.EqualTo("Global1,Global2,Global3,Current User")); + Assert.That(string.Join(",", tokens.Select(td => td.Description.ToString())), Is.EqualTo("description of token1,description of token2,description of token3,The current user")); + Assert.That(string.Join(",", tokens.Select(td => td.ChainTarget ?? "")), Is.EqualTo(",,,User")); + + tokens = allTokens.Single(d => d.Target == "User").Tokens; + Assert.That(string.Join(",", tokens.Select(td => td.Target)), Is.EqualTo("User,User")); + Assert.That(string.Join(",", tokens.Select(td => td.Token)), Is.EqualTo("Name,Birthdate")); + Assert.That(string.Join(",", tokens.Select(td => td.Name.ToString())), Is.EqualTo("Name,Birthdate")); + Assert.That(string.Join(",", tokens.Select(td => td.Description.ToString())), Is.EqualTo("Their user name,Date of birth")); + Assert.That(string.Join(",", tokens.Select(td => td.ChainTarget ?? "")), Is.EqualTo(",DateTime")); + + tokens = allTokens.Single(d => d.Target == "Date").Tokens; + Assert.That(string.Join(",", tokens.Select(td => td.Target)), Is.EqualTo("Date")); + Assert.That(string.Join(",", tokens.Select(td => td.Token)), Is.EqualTo("Now")); + Assert.That(string.Join(",", tokens.Select(td => td.Name.ToString())), Is.EqualTo("Now")); + Assert.That(string.Join(",", tokens.Select(td => td.Description.ToString())), Is.EqualTo("Current system date in short date format. You can chain a .NET DateTime format string to customize.")); + Assert.That(string.Join(",", tokens.Select(td => td.ChainTarget ?? "")), Is.EqualTo("")); + } + + [Test] + public void TestDescribeFilter() { + var tokenDescriptors = _tokenManager.Describe(null); + Assert.That(tokenDescriptors.Count(), Is.EqualTo(3)); + tokenDescriptors = _tokenManager.Describe(new[] { "Site" }); + Assert.That(tokenDescriptors.Count(), Is.EqualTo(1)); + Assert.That(tokenDescriptors.First().Target, Is.EqualTo("Site")); + tokenDescriptors = _tokenManager.Describe(new[] { "Site", "User" }); + Assert.That(tokenDescriptors.Count(), Is.EqualTo(2)); + Assert.That(tokenDescriptors.Any(d => d.Target == "Site")); + Assert.That(tokenDescriptors.Any(d => d.Target == "User")); + } + + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Tests/TokenizerTests.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/TokenizerTests.cs new file mode 100644 index 000000000..0b73be53c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/TokenizerTests.cs @@ -0,0 +1,80 @@ +using System; +using Autofac; +using NUnit.Framework; +using Orchard.Tokens.Implementation; + +namespace Orchard.Tokens.Tests { + [TestFixture] + public class TokenizerTests { + private IContainer _container; + private ITokenizer _tokenizer; + + [SetUp] + public void Init() { + var builder = new ContainerBuilder(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + _container = builder.Build(); + _tokenizer = _container.Resolve(); + } + + [Test] + public void TestGlobalTokens() { + Assert.That(_tokenizer.Replace("{Site.Global1}", null), Is.EqualTo("[global1]")); + Assert.That(_tokenizer.Replace("{Site.Global2}", null), Is.EqualTo("[global2]")); + Assert.That(_tokenizer.Replace("{Site.Global1}{Site.Global2}{Site.Global1}{Site.Global2}", null), Is.EqualTo("[global1][global2][global1][global2]")); + } + + [Test] + public void TestContextTokens() { + Assert.That(_tokenizer.Replace("{User.Name}", null), Is.EqualTo("CurrentUser")); + Assert.That(_tokenizer.Replace("{User.Name}", new { User = new TestUser { UserName = "LocalUser" } }), Is.EqualTo("LocalUser")); + } + + [Test] + public void TestChainedTokens() { + Assert.That(_tokenizer.Replace("{Site.CurrentUser.Name}", null), Is.EqualTo("CurrentUser")); + Assert.That(_tokenizer.Replace("{Site.CurrentUser.Name}", new { User = new TestUser { UserName = "ShouldStillUseParentValue" } }), Is.EqualTo("CurrentUser")); + Assert.That(_tokenizer.Replace("{Site.CurrentUser.Birthdate}", null), Is.EqualTo("Nov 15")); + Assert.That(_tokenizer.Replace("{Site.CurrentUser.Birthdate.yyyy}", null), Is.EqualTo("1978")); + } + + [Test] + public void TestMissingTokens() { + Assert.That(_tokenizer.Replace("[{Site.NotAToken}]", null), Is.EqualTo("[]")); + Assert.That(_tokenizer.Replace("[{NotATokenType.Foo}]", null), Is.EqualTo("[]")); + Assert.That(_tokenizer.Replace("[{Site.CurrentUser.NotASubToken}]", null), Is.EqualTo("[]")); + Assert.That(_tokenizer.Replace("[{Site}]", null), Is.EqualTo("[]")); + Assert.That(_tokenizer.Replace("[{NotATokenType}]", null), Is.EqualTo("[]")); + } + + [Test] + public void TestTokenCaseSensitivity() { + Assert.That(_tokenizer.Replace("{Site.Global1}", null), Is.EqualTo("[global1]")); + Assert.That(_tokenizer.Replace("{site.Global1}", null), Is.EqualTo("")); + Assert.That(_tokenizer.Replace("{Site.global1}", null), Is.EqualTo("")); + } + + [Test] + public void TestTokenEscapeSequences() { + Assert.That(_tokenizer.Replace("{{escaped}} {Site.Global1} }}{{ {{{{ }}}}", null), Is.EqualTo("{escaped} [global1] }{ {{ }}")); + Assert.That(_tokenizer.Replace("{Date.Now.{{yyyy}}}", null), Is.EqualTo(DateTime.UtcNow.ToString("{yyyy}"))); + } + + [Test] + public void TestHtmlEncodedByDefault() { + Assert.That(_tokenizer.Replace("{Date.Now.<>}", null), Is.EqualTo("<>")); + } + + [Test] + public void TestNoEncode() { + Assert.That(_tokenizer.Replace("{Date.Now.<>}", null, new ReplaceOptions { Encoding = ReplaceOptions.NoEncode }), Is.EqualTo("<>")); + } + + [Test] + public void TestPredicate() { + Assert.That(_tokenizer.Replace("{Site.Global1}{Site.Global2}", null, new ReplaceOptions { Predicate = token => token == "Site.Global2" }), Is.EqualTo("{Site.Global1}[global2]")); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Tests/UserTokenTests.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/UserTokenTests.cs new file mode 100644 index 000000000..300ccc759 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Tests/UserTokenTests.cs @@ -0,0 +1,41 @@ +using Autofac; +using NUnit.Framework; +using Orchard.Tokens.Implementation; +using Orchard.Tokens.Providers; + +namespace Orchard.Tokens.Tests { + [TestFixture] + public class UserTokenTests { + private IContainer _container; + private ITokenizer _tokenizer; + + [SetUp] + public void Init() { + var builder = new ContainerBuilder(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + _container = builder.Build(); + _tokenizer = _container.Resolve(); + } + + [Test] + public void TestUserTokens() { + var str = _tokenizer.Replace("{User.Name},{User.Email},{User.Id}", new { User = new TestUser { UserName = "Joe", Email = "test@test.com", Id = 88 } }); + Assert.That(str, Is.EqualTo("Joe,test@test.com,88")); + str = _tokenizer.Replace("{User.Name},{User.Email},{User.Id}", null); + Assert.That(str, Is.EqualTo("Fake,Fake@fake.com,5")); + } + + [Test] + public void AnonymousUserShouldReturnEmpty() { + var result = _tokenizer.Replace("{User.Name}", new { User = default(TestUser) }); + Assert.That(result, Is.Empty); + + result = _tokenizer.Replace("{User}", new { User = default(TestUser) }); + Assert.That(result, Is.Empty); + } + + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/TokenDescriptor.cs b/src/Orchard.Web/Modules/Orchard.Tokens/TokenDescriptor.cs new file mode 100644 index 000000000..b67e83bea --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/TokenDescriptor.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Orchard.Localization; + +namespace Orchard.Tokens { + public class TokenDescriptor { + public string Target { get; set; } + public string Token { get; set; } + public string ChainTarget { get; set; } + public LocalizedString Name { get; set; } + public LocalizedString Description { get; set; } + } + + public class TokenTypeDescriptor { + public string Target { get; set; } + public LocalizedString Name { get; set; } + public LocalizedString Description { get; set; } + public IEnumerable Tokens { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Views/TokenHint.cshtml b/src/Orchard.Web/Modules/Orchard.Tokens/Views/TokenHint.cshtml new file mode 100644 index 000000000..c0cb2cee3 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Views/TokenHint.cshtml @@ -0,0 +1,13 @@ +@{ + Script.Require("jQueryUI_Autocomplete"); + Script.Include("orchard-tokens-admin.js").AtFoot(); + Style.Include("orchard-tokens-admin.css"); +} + +@using(Script.Head()) { + +} diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Views/Web.config b/src/Orchard.Web/Modules/Orchard.Tokens/Views/Web.config new file mode 100644 index 000000000..b7d215131 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Views/Web.config @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Web.config b/src/Orchard.Web/Modules/Orchard.Tokens/Web.config new file mode 100644 index 000000000..eb8acf5f7 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Web.config @@ -0,0 +1,41 @@ + + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +