diff --git a/src/Orchard.Tests/DisplayManagement/Descriptors/StylesheetBindingStrategyTests.cs b/src/Orchard.Tests/DisplayManagement/Descriptors/StylesheetBindingStrategyTests.cs new file mode 100644 index 000000000..fa10824a2 --- /dev/null +++ b/src/Orchard.Tests/DisplayManagement/Descriptors/StylesheetBindingStrategyTests.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Autofac; +using Moq; +using NUnit.Framework; +using Orchard.DisplayManagement.Descriptors; +using Orchard.DisplayManagement.Descriptors.ResourceBindingStrategy; +using Orchard.DisplayManagement.Descriptors.ShapeTemplateStrategy; +using Orchard.Environment.Descriptor.Models; +using Orchard.Environment.Extensions; +using Orchard.Environment.Extensions.Models; +using Orchard.FileSystems.VirtualPath; + +namespace Orchard.Tests.DisplayManagement.Descriptors { + [TestFixture] + public class StylesheetBindingStrategyTests : ContainerTestBase { + private ShellDescriptor _descriptor; + private IList _features; + private TestViewEngine _testViewEngine; + private TestVirtualPathProvider _testVirtualPathProvider; + + + protected override void Register(Autofac.ContainerBuilder builder) { + _descriptor = new ShellDescriptor { }; + _testViewEngine = new TestViewEngine(); + _testVirtualPathProvider = new TestVirtualPathProvider(); + + builder.Register(ctx => _descriptor); + builder.RegisterType().As(); + builder.RegisterInstance(_testViewEngine).As(); + builder.RegisterInstance(_testVirtualPathProvider).As(); + + var extensionManager = new Mock(); + builder.Register(ctx => extensionManager); + builder.Register(ctx => extensionManager.Object); + } + + public class TestViewEngine : Dictionary, IShapeTemplateViewEngine { + public IEnumerable DetectTemplateFileNames(IEnumerable fileNames) { + return fileNames; + } + } + + public class TestVirtualPathProvider : IVirtualPathProvider { + public string Combine(params string[] paths) { + throw new NotImplementedException(); + } + + public string ToAppRelative(string virtualPath) { + throw new NotImplementedException(); + } + + public string MapPath(string virtualPath) { + throw new NotImplementedException(); + } + + public bool FileExists(string virtualPath) { + throw new NotImplementedException(); + } + + public Stream OpenFile(string virtualPath) { + throw new NotImplementedException(); + } + + public StreamWriter CreateText(string virtualPath) { + throw new NotImplementedException(); + } + + public Stream CreateFile(string virtualPath) { + throw new NotImplementedException(); + } + + public DateTime GetFileLastWriteTimeUtc(string virtualPath) { + throw new NotImplementedException(); + } + + public bool DirectoryExists(string virtualPath) { + throw new NotImplementedException(); + } + + public void CreateDirectory(string virtualPath) { + throw new NotImplementedException(); + } + + public string GetDirectoryName(string virtualPath) { + throw new NotImplementedException(); + } + + public IEnumerable ListFiles(string path) { + return new List {"~/Modules/Alpha/Styles/AlphaStyle.css"}; + } + + public IEnumerable ListDirectories(string path) { + throw new NotImplementedException(); + } + } + + protected override void Resolve(ILifetimeScope container) { + _features = new List(); + + container.Resolve>() + .Setup(em => em.AvailableFeatures()) + .Returns(_features); + } + + void AddFeature(string name, params string[] dependencies) { + var featureDescriptor = new FeatureDescriptor { + Id = name, + Dependencies = dependencies, + Extension = new ExtensionDescriptor { + Id = name, + Location = "~/Modules" + } + }; + featureDescriptor.Extension.Features = new[] { featureDescriptor }; + + _features.Add(featureDescriptor); + } + + void AddEnabledFeature(string name, params string[] dependencies) { + AddFeature(name, dependencies); + _descriptor.Features = _descriptor.Features.Concat(new[] { new ShellFeature { Name = name } }); + } + + [Test] + public void TemplateResolutionWorks() { + AddEnabledFeature("Alpha"); + + _testViewEngine.Add("~/Modules/Alpha/Styles/AlphaShape.css", null); + var strategy = _container.Resolve(); + + IList alterationBuilders = new List(); + var builder = new ShapeTableBuilder(alterationBuilders,null); + strategy.Discover(builder); + var alterations = alterationBuilders.Select(alterationBuilder=>alterationBuilder.Build()); + + Assert.That(alterations.Any(alteration => alteration.ShapeType == "Style")); + } + + } +} diff --git a/src/Orchard.Tests/Orchard.Framework.Tests.csproj b/src/Orchard.Tests/Orchard.Framework.Tests.csproj index 5815c2241..c1537c8af 100644 --- a/src/Orchard.Tests/Orchard.Framework.Tests.csproj +++ b/src/Orchard.Tests/Orchard.Framework.Tests.csproj @@ -212,6 +212,7 @@ + diff --git a/src/Orchard.Web/Core/Shapes/CoreShapes.cs b/src/Orchard.Web/Core/Shapes/CoreShapes.cs index 72efafe06..f292be49b 100644 --- a/src/Orchard.Web/Core/Shapes/CoreShapes.cs +++ b/src/Orchard.Web/Core/Shapes/CoreShapes.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; @@ -8,6 +9,8 @@ using System.Web.Mvc; using System.Web.Mvc.Html; using Orchard.DisplayManagement; using Orchard.DisplayManagement.Descriptors; +using Orchard.DisplayManagement.Descriptors.ResourceBindingStrategy; +using Orchard.Mvc; using Orchard.Settings; using Orchard.UI; using Orchard.UI.Resources; @@ -19,11 +22,20 @@ using Orchard.Utility.Extensions; namespace Orchard.Core.Shapes { public class CoreShapes : IShapeTableProvider { private readonly IWorkContextAccessor _workContextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; - public CoreShapes(IWorkContextAccessor workContextAccessor) { + public CoreShapes(IWorkContextAccessor workContextAccessor, IHttpContextAccessor httpContextAccessor) { // needed to get CurrentSite. // note that injecting ISiteService here causes a stack overflow in AutoFac! _workContextAccessor = workContextAccessor; + _httpContextAccessor = httpContextAccessor; + } + + // not injected the usual way because this component is a 'static' dependency and RM is per-request + private IResourceManager ResourceManager { + get { + return _workContextAccessor.GetContext(_httpContextAccessor.Current()).Resolve(); + } } public void Discover(ShapeTableBuilder builder) { @@ -82,6 +94,26 @@ namespace Orchard.Core.Shapes { list.ItemClasses = new List(); list.ItemAttributes = new Dictionary(); }); + + builder.Describe("Style") + .OnDisplaying(displaying => { + var resource = displaying.Shape; + string url = resource.Url; + string fileName = StylesheetBindingStrategy.GetAlternateShapeNameFromFileName(url); + if (!string.IsNullOrEmpty(fileName)) { + resource.Metadata.Alternates.Add("Style__" + fileName); + } + }); + + builder.Describe("Resource") + .OnDisplaying(displaying => { + var resource = displaying.Shape; + string url = resource.Url; + string fileName = StylesheetBindingStrategy.GetAlternateShapeNameFromFileName(url); + if (!string.IsNullOrEmpty(fileName)) { + resource.Metadata.Alternates.Add("Resource__" + fileName); + } + }); } @@ -159,51 +191,58 @@ namespace Orchard.Core.Shapes { #endregion [Shape] - public void HeadScripts(HtmlHelper Html, IResourceManager ResourceManager) { - WriteResources(Html, _workContextAccessor.GetContext(Html.ViewContext).CurrentSite, - ResourceManager, "script", ResourceLocation.Head, null); - WriteLiteralScripts(Html, ResourceManager.GetRegisteredHeadScripts()); + public void HeadScripts(dynamic Display, TextWriter Output) { + WriteResources(Display, Output, "script", ResourceLocation.Head, null); + WriteLiteralScripts(Output, ResourceManager.GetRegisteredHeadScripts()); } [Shape] - public void FootScripts(HtmlHelper Html, IResourceManager ResourceManager) { - WriteResources(Html, _workContextAccessor.GetContext(Html.ViewContext).CurrentSite, - ResourceManager, "script", null, ResourceLocation.Head); - WriteLiteralScripts(Html, ResourceManager.GetRegisteredFootScripts()); + public void FootScripts(dynamic Display, TextWriter Output) { + WriteResources(Display, Output, "script", null, ResourceLocation.Head); + WriteLiteralScripts(Output, ResourceManager.GetRegisteredFootScripts()); } [Shape] - public void Metas(HtmlHelper Html, IResourceManager ResourceManager) { + public void Metas(TextWriter Output) { foreach (var meta in ResourceManager.GetRegisteredMetas()) { - Html.ViewContext.Writer.WriteLine(meta.GetTag()); + Output.WriteLine(meta.GetTag()); } } [Shape] - public void HeadLinks(HtmlHelper Html, IResourceManager ResourceManager) { + public void HeadLinks(TextWriter Output) { foreach (var link in ResourceManager.GetRegisteredLinks()) { - Html.ViewContext.Writer.WriteLine(link.GetTag()); + Output.WriteLine(link.GetTag()); } } [Shape] - public void StylesheetLinks(HtmlHelper Html, IResourceManager ResourceManager) { - WriteResources(Html, _workContextAccessor.GetContext(Html.ViewContext).CurrentSite, - ResourceManager, "stylesheet", null, null); + public void StylesheetLinks(dynamic Display, TextWriter Output) { + WriteResources(Display, Output, "stylesheet", null, null); } - private static void WriteLiteralScripts(HtmlHelper html, IEnumerable scripts) { + [Shape] + public void Style(TextWriter Output, ResourceDefinition Resource, string Url, string Condition) { + UI.Resources.ResourceManager.WriteResource(Output, Resource, Url, Condition); + } + + [Shape] + public void Resource(TextWriter Output, ResourceDefinition Resource, string Url, string Condition) { + UI.Resources.ResourceManager.WriteResource(Output, Resource, Url, Condition); + } + + private static void WriteLiteralScripts(TextWriter output, IEnumerable scripts) { if (scripts == null) { return; } - var writer = html.ViewContext.Writer; foreach (string script in scripts) { - writer.WriteLine(script); + output.WriteLine(script); } } - private static void WriteResources(HtmlHelper html, ISite site, IResourceManager rm, string resourceType, ResourceLocation? includeLocation, ResourceLocation? excludeLocation) { + private void WriteResources(dynamic Display, TextWriter Output, string resourceType, ResourceLocation? includeLocation, ResourceLocation? excludeLocation) { bool debugMode; + var site = _workContextAccessor.GetContext(_httpContextAccessor.Current()).CurrentSite; switch (site.ResourceDebugMode) { case ResourceDebugMode.Enabled: debugMode = true; @@ -213,26 +252,29 @@ namespace Orchard.Core.Shapes { break; default: Debug.Assert(site.ResourceDebugMode == ResourceDebugMode.FromAppSetting, "Unknown ResourceDebugMode value."); - debugMode = html.ViewContext.HttpContext.IsDebuggingEnabled; + debugMode = _httpContextAccessor.Current().IsDebuggingEnabled; break; } var defaultSettings = new RequireSettings { DebugMode = debugMode, Culture = CultureInfo.CurrentUICulture.Name, }; - var requiredResources = rm.BuildRequiredResources(resourceType); - var appPath = html.ViewContext.HttpContext.Request.ApplicationPath; + var requiredResources = ResourceManager.BuildRequiredResources(resourceType); + var appPath = _httpContextAccessor.Current().Request.ApplicationPath; foreach (var context in requiredResources.Where(r => (includeLocation.HasValue ? r.Settings.Location == includeLocation.Value : true) && (excludeLocation.HasValue ? r.Settings.Location != excludeLocation.Value : true))) { + + var path = context.GetResourceUrl(defaultSettings, appPath); var condition = context.Settings.Condition; - if (!string.IsNullOrEmpty(condition)) { - html.ViewContext.Writer.WriteLine(""); + else { + result = Display.Resource(Url: path, Condition: condition, Resource: context.Resource); } + Output.Write(result); } } diff --git a/src/Orchard/DisplayManagement/Descriptors/ResourceBindingStrategy/StylesheetBindingStrategy.cs b/src/Orchard/DisplayManagement/Descriptors/ResourceBindingStrategy/StylesheetBindingStrategy.cs new file mode 100644 index 000000000..910364b13 --- /dev/null +++ b/src/Orchard/DisplayManagement/Descriptors/ResourceBindingStrategy/StylesheetBindingStrategy.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Orchard.Environment.Descriptor.Models; +using Orchard.Environment.Extensions; +using Orchard.Environment.Extensions.Models; +using Orchard.FileSystems.VirtualPath; +using Orchard.UI.Resources; + +namespace Orchard.DisplayManagement.Descriptors.ResourceBindingStrategy { + // discovers .css files and turns them into Style__ shapes. + public class StylesheetBindingStrategy : IShapeTableProvider { + private readonly IExtensionManager _extensionManager; + private readonly ShellDescriptor _shellDescriptor; + private readonly IVirtualPathProvider _virtualPathProvider; + private static readonly Regex _safeName = new Regex(@"[/:?#\[\]@!&'()*+,;=\s\""<>\.\-_]+", RegexOptions.Compiled); + + public StylesheetBindingStrategy(IExtensionManager extensionManager, ShellDescriptor shellDescriptor, IVirtualPathProvider virtualPathProvider) { + _extensionManager = extensionManager; + _shellDescriptor = shellDescriptor; + _virtualPathProvider = virtualPathProvider; + } + + private static string SafeName(string name) { + if (string.IsNullOrWhiteSpace(name)) + return String.Empty; + return _safeName.Replace(name, String.Empty); + } + + public static string GetAlternateShapeNameFromFileName(string fileName) { + if (fileName == null) { + throw new ArgumentNullException("fileName"); + } + string shapeName; + if (Uri.IsWellFormedUriString(fileName, UriKind.Absolute)) { + var uri = new Uri(fileName); + shapeName = uri.Authority + "$" + uri.AbsolutePath + "$" + uri.Query; + } + else { + shapeName = Path.GetFileNameWithoutExtension(fileName); + } + return SafeName(shapeName); + } + + private static IEnumerable Once(IEnumerable featureDescriptors) { + var once = new ConcurrentDictionary(); + return featureDescriptors.Select(fd => fd.Extension).Where(ed => once.TryAdd(ed.Id, null)).ToList(); + } + + public void Discover(ShapeTableBuilder builder) { + var availableFeatures = _extensionManager.AvailableFeatures(); + var activeFeatures = availableFeatures.Where(FeatureIsEnabled); + var activeExtensions = Once(activeFeatures); + + var hits = activeExtensions.SelectMany(extensionDescriptor => { + var basePath = Path.Combine(extensionDescriptor.Location, extensionDescriptor.Id).Replace(Path.DirectorySeparatorChar, '/'); + var virtualPath = Path.Combine(basePath, "Styles").Replace(Path.DirectorySeparatorChar, '/'); + var shapes = _virtualPathProvider.ListFiles(virtualPath) + .Select(Path.GetFileName) + .Where(fileName => string.Equals(Path.GetExtension(fileName), ".css", System.StringComparison.OrdinalIgnoreCase)) + .Select(cssFileName => new { + fileName = Path.GetFileNameWithoutExtension(cssFileName), + fileVirtualPath = Path.Combine(virtualPath, cssFileName).Replace(Path.DirectorySeparatorChar, '/'), + shapeType = "Style__" + GetAlternateShapeNameFromFileName(cssFileName), + extensionDescriptor + }); + return shapes; + }); + + foreach (var iter in hits) { + var hit = iter; + var featureDescriptors = hit.extensionDescriptor.Features.Where(fd => fd.Id == hit.extensionDescriptor.Id); + foreach (var featureDescriptor in featureDescriptors) { + builder.Describe(iter.shapeType) + .From(new Feature {Descriptor = featureDescriptor}) + .BoundAs( + hit.fileVirtualPath, + shapeDescriptor => displayContext => { + var shape = ((dynamic) displayContext.Value); + var output = displayContext.ViewContext.Writer; + ResourceDefinition resource = shape.Resource; + string condition = shape.Condition; + ResourceManager.WriteResource(output, resource, hit.fileVirtualPath, condition); + return null; + }); + } + } + } + + private bool FeatureIsEnabled(FeatureDescriptor fd) { + return (DefaultExtensionTypes.IsTheme(fd.Extension.ExtensionType) && (fd.Id == "TheAdmin" || fd.Id == "SafeMode")) || + _shellDescriptor.Features.Any(sf => sf.Name == fd.Id); + } + } +} diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 40d72d553..d89393040 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -142,6 +142,7 @@ + diff --git a/src/Orchard/UI/Resources/ResourceFilter.cs b/src/Orchard/UI/Resources/ResourceFilter.cs index 990184f1b..fa291fe08 100644 --- a/src/Orchard/UI/Resources/ResourceFilter.cs +++ b/src/Orchard/UI/Resources/ResourceFilter.cs @@ -4,20 +4,16 @@ using Orchard.Mvc.Filters; namespace Orchard.UI.Resources { public class ResourceFilter : FilterProvider, IResultFilter { - private readonly IResourceManager _resourceManager; private readonly IWorkContextAccessor _workContextAccessor; private readonly dynamic _shapeFactory; public ResourceFilter( - IResourceManager resourceManager, IWorkContextAccessor workContextAccessor, IShapeFactory shapeFactory) { - _resourceManager = resourceManager; _workContextAccessor = workContextAccessor; _shapeFactory = shapeFactory; } - public void OnResultExecuting(ResultExecutingContext filterContext) { // should only run on a full view rendering result if (!(filterContext.Result is ViewResult)) @@ -26,11 +22,11 @@ namespace Orchard.UI.Resources { var ctx = _workContextAccessor.GetContext(); var head = ctx.Layout.Head; var tail = ctx.Layout.Tail; - head.Add(_shapeFactory.Metas().ResourceManager(_resourceManager)); - head.Add(_shapeFactory.HeadLinks().ResourceManager(_resourceManager)); - head.Add(_shapeFactory.StylesheetLinks().ResourceManager(_resourceManager)); - head.Add(_shapeFactory.HeadScripts().ResourceManager(_resourceManager)); - tail.Add(_shapeFactory.FootScripts().ResourceManager(_resourceManager)); + head.Add(_shapeFactory.Metas()); + head.Add(_shapeFactory.HeadLinks()); + head.Add(_shapeFactory.StylesheetLinks()); + head.Add(_shapeFactory.HeadScripts()); + tail.Add(_shapeFactory.FootScripts()); } public void OnResultExecuted(ResultExecutedContext filterContext) { diff --git a/src/Orchard/UI/Resources/ResourceManager.cs b/src/Orchard/UI/Resources/ResourceManager.cs index e5f36502b..429332006 100644 --- a/src/Orchard/UI/Resources/ResourceManager.cs +++ b/src/Orchard/UI/Resources/ResourceManager.cs @@ -3,8 +3,10 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; +using System.IO; using System.Linq; using System.Web; +using System.Web.Mvc; using Autofac.Features.Metadata; using Orchard.Environment.Extensions.Models; @@ -40,6 +42,30 @@ namespace Orchard.UI.Resources { return resourcePath; } + private static TagBuilder GetTagBuilder(ResourceDefinition resource, string url) { + var tagBuilder = new TagBuilder(resource.TagName); + tagBuilder.MergeAttributes(resource.TagBuilder.Attributes); + if (!String.IsNullOrEmpty(resource.FilePathAttributeName)) { + if (!String.IsNullOrEmpty(url)) { + if (VirtualPathUtility.IsAppRelative(url)) { + url = VirtualPathUtility.ToAbsolute(url); + } + tagBuilder.MergeAttribute(resource.FilePathAttributeName, url, true); + } + } + return tagBuilder; + } + + public static void WriteResource(TextWriter writer, ResourceDefinition resource, string url, string condition) { + if (!string.IsNullOrEmpty(condition)) { + writer.WriteLine(""); + } + } + public ResourceManager(IEnumerable> resourceProviders) { _providers = resourceProviders; } diff --git a/src/Orchard/UI/Resources/ResourceRequiredContext.cs b/src/Orchard/UI/Resources/ResourceRequiredContext.cs index 1cbe31f31..8475613ad 100644 --- a/src/Orchard/UI/Resources/ResourceRequiredContext.cs +++ b/src/Orchard/UI/Resources/ResourceRequiredContext.cs @@ -6,11 +6,15 @@ namespace Orchard.UI.Resources { public ResourceDefinition Resource { get; set; } public RequireSettings Settings { get; set; } + public string GetResourceUrl(RequireSettings baseSettings, string appPath) { + return Resource.ResolveUrl(baseSettings == null ? Settings : baseSettings.Combine(Settings), appPath); + } + public TagBuilder GetTagBuilder(RequireSettings baseSettings, string appPath) { var tagBuilder = new TagBuilder(Resource.TagName); tagBuilder.MergeAttributes(Resource.TagBuilder.Attributes); if (!String.IsNullOrEmpty(Resource.FilePathAttributeName)) { - var resolvedUrl = Resource.ResolveUrl(baseSettings == null ? Settings : baseSettings.Combine(Settings), appPath); + var resolvedUrl = GetResourceUrl(baseSettings, appPath); if (!String.IsNullOrEmpty(resolvedUrl)) { tagBuilder.MergeAttribute(Resource.FilePathAttributeName, resolvedUrl, true); }