Implemented resource file hash-based cache busting. (#7564)

This commit is contained in:
Daniel Stolt
2017-03-07 21:49:06 +01:00
committed by GitHub
parent 332a2676dd
commit 514e4fa0d7
20 changed files with 263 additions and 70 deletions

View File

@@ -66,7 +66,7 @@ namespace Orchard.Tests.Stubs {
set { throw new NotImplementedException(); }
}
public string SiteCalendar {
public string SiteCalendar {
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
@@ -81,6 +81,11 @@ namespace Orchard.Tests.Stubs {
set { throw new NotImplementedException(); }
}
public bool UseFileHash {
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public int PageSize {
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }

View File

@@ -13,6 +13,7 @@ namespace Orchard.Tests.UI.Resources {
public class ResourceManagerTests {
private IContainer _container;
private IResourceManager _resourceManager;
private IResourceFileHashProvider _resourceFileHashProvider;
private TestManifestProvider _testManifest;
private string _appPath = "/AppPath/";
@@ -37,7 +38,7 @@ namespace Orchard.Tests.UI.Resources {
private void VerifyPaths(string resourceType, RequireSettings defaultSettings, string expectedPaths, bool ssl) {
defaultSettings = defaultSettings ?? new RequireSettings();
var requiredResources = _resourceManager.BuildRequiredResources(resourceType);
var renderedResources = string.Join(",", requiredResources.Select(context => context.GetResourceUrl(defaultSettings, _appPath, ssl)).ToArray());
var renderedResources = string.Join(",", requiredResources.Select(context => context.GetResourceUrl(defaultSettings, _appPath, ssl, _resourceFileHashProvider)).ToArray());
Assert.That(renderedResources, Is.EqualTo(expectedPaths));
}
@@ -45,9 +46,11 @@ namespace Orchard.Tests.UI.Resources {
public void Init() {
var builder = new ContainerBuilder();
builder.RegisterType<ResourceManager>().As<IResourceManager>();
builder.RegisterType<ResourceFileHashProvider>().As<IResourceFileHashProvider>();
builder.RegisterType<TestManifestProvider>().As<IResourceManifestProvider>().SingleInstance();
_container = builder.Build();
_resourceManager = _container.Resolve<IResourceManager>();
_resourceFileHashProvider = _container.Resolve<IResourceFileHashProvider>();
_testManifest = _container.Resolve<IResourceManifestProvider>() as TestManifestProvider;
}

View File

@@ -15,6 +15,7 @@ namespace Orchard.Core.Settings.Handlers {
siteSettingsPart.SiteName = "My Orchard Project Application";
siteSettingsPart.PageTitleSeparator = " - ";
siteSettingsPart.SiteTimeZone = TimeZoneInfo.Local.Id;
siteSettingsPart.UseFileHash = true;
}
}
}

View File

@@ -35,7 +35,7 @@ namespace Orchard.Core.Settings.Models {
set { this.Store(x => x.SiteCulture, value); }
}
public string SiteCalendar {
public string SiteCalendar {
get { return this.Retrieve(x => x.SiteCalendar); }
set { this.Store(x => x.SiteCalendar, value); }
}
@@ -50,6 +50,11 @@ namespace Orchard.Core.Settings.Models {
set { this.Store(x=> x.UseCdn, value); }
}
public bool UseFileHash {
get { return this.Retrieve(x => x.UseFileHash); }
set { this.Store(x => x.UseFileHash, value); }
}
public int PageSize {
get { return this.Retrieve(x => x.PageSize, DefaultPageSize); }
set { this.Store(x => x.PageSize, value); }

View File

@@ -8,7 +8,7 @@ namespace Orchard.Core.Settings.ViewModels {
public class SiteSettingsPartViewModel {
public SiteSettingsPart Site { get; set; }
public IEnumerable<string> SiteCultures { get; set; }
public IEnumerable<string> SiteCalendars { get; set; }
public IEnumerable<string> SiteCalendars { get; set; }
public IEnumerable<TimeZoneInfo> TimeZones { get; set; }
[HiddenInput(DisplayValue = false)]
@@ -51,6 +51,11 @@ namespace Orchard.Core.Settings.ViewModels {
set { Site.UseCdn = value; }
}
public bool UseFileHash {
get { return Site.UseFileHash; }
set { Site.UseFileHash = value; }
}
public int PageSize {
get { return Site.PageSize; }
set { Site.PageSize = value; }

View File

@@ -23,20 +23,20 @@
<span class="hint">@T("Enter the fully qualified base URL of the web site.")</span>
<span class="hint">@T("e.g., http://localhost:30320/orchardlocal, http://www.yourdomain.com")</span>
</div>
<div>
<label for="SiteCulture">@T("Default Site Culture")</label>
@Html.DropDownList("SiteCulture", new SelectList(Model.SiteCultures, Model.SiteCulture))
@Html.ValidationMessage("SiteCulture", "*")
<span class="hint">@T("Determines the default culture used to localize strings and to format and parse numbers, date and times.")</span>
<p>@Html.ActionLink(T("Add or remove supported cultures for the site").ToString(), "Culture")</p>
</div>
<div>
<label for="SiteCalendar">@T("Default Site Calendar")</label>
@Html.DropDownList("SiteCalendar", new[] { new SelectListItem { Text = T("Culture calendar").Text, Value = "" } }.Union(new SelectList(Model.SiteCalendars, Model.SiteCalendar)))
@Html.ValidationMessage("SiteCalendar", "*")
<span class="hint">@T("Determines the default calendar used when displaying and editing dates and times.")</span>
<span class="hint">@T("The 'Culture calendar' option means the default calendar for the culture of the current request will be used (not necessarily the configured default site culture).")</span>
</div>
<div>
<label for="SiteCulture">@T("Default Site Culture")</label>
@Html.DropDownList("SiteCulture", new SelectList(Model.SiteCultures, Model.SiteCulture))
@Html.ValidationMessage("SiteCulture", "*")
<span class="hint">@T("Determines the default culture used to localize strings and to format and parse numbers, date and times.")</span>
<p>@Html.ActionLink(T("Add or remove supported cultures for the site").ToString(), "Culture")</p>
</div>
<div>
<label for="SiteCalendar">@T("Default Site Calendar")</label>
@Html.DropDownList("SiteCalendar", new[] { new SelectListItem { Text = T("Culture calendar").Text, Value = "" } }.Union(new SelectList(Model.SiteCalendars, Model.SiteCalendar)))
@Html.ValidationMessage("SiteCalendar", "*")
<span class="hint">@T("Determines the default calendar used when displaying and editing dates and times.")</span>
<span class="hint">@T("The 'Culture calendar' option means the default calendar for the culture of the current request will be used (not necessarily the configured default site culture).")</span>
</div>
<div>
<label for="TimeZone">@T("Default Time Zone")</label>
@Html.DropDownList("TimeZone", new[] { new SelectListItem { Text = T("Local to server").Text, Value = "" } }.Union(new SelectList(Model.TimeZones, "Id", "", Model.TimeZone)))
@@ -65,7 +65,12 @@
<div>
@Html.CheckBoxFor(m => m.UseCdn)
@Html.LabelFor(m => m.UseCdn, T("Use CDN").Text, new { @class = "forcheckbox" })
<span class="hint">@T("Determines whether the defined CDN value is used for scripts and stylesheets, or their local version")</span>
<span class="hint">@T("Determines whether the defined CDN value is used for scripts and stylesheets, or their local version")</span>
</div>
<div>
@Html.CheckBoxFor(m => m.UseFileHash)
@Html.LabelFor(m => m.UseFileHash, T("Use Resource File Hashing").Text, new { @class = "forcheckbox" })
<span class="hint">@T("Determines whether MD5 file hashes are appended to the URLs for scripts and stylesheets (provides automatic cache busting).")</span>
</div>
<div>
<label for="DefaultPageSize">@T("Default number of items per page")</label>

View File

@@ -28,17 +28,20 @@ namespace Orchard.Core.Shapes {
private readonly Work<IResourceManager> _resourceManager;
private readonly Work<IHttpContextAccessor> _httpContextAccessor;
private readonly Work<IShapeFactory> _shapeFactory;
private readonly IResourceFileHashProvider _resourceFileHashProvider;
public CoreShapes(
Work<WorkContext> workContext,
Work<IResourceManager> resourceManager,
Work<IHttpContextAccessor> httpContextAccessor,
Work<IShapeFactory> shapeFactory
Work<IShapeFactory> shapeFactory,
IResourceFileHashProvider resourceHashProvider
) {
_workContext = workContext;
_resourceManager = resourceManager;
_httpContextAccessor = httpContextAccessor;
_shapeFactory = shapeFactory;
_resourceFileHashProvider = resourceHashProvider;
T = NullLocalizer.Instance;
}
@@ -448,6 +451,7 @@ namespace Orchard.Core.Shapes {
var defaultSettings = new RequireSettings {
DebugMode = debugMode,
CdnMode = site.UseCdn,
FileHashMode = site.UseFileHash,
Culture = _workContext.Value.CurrentCulture,
};
var requiredResources = _resourceManager.Value.BuildRequiredResources(resourceType);
@@ -460,18 +464,18 @@ namespace Orchard.Core.Shapes {
(includeLocation.HasValue ? r.Settings.Location == includeLocation.Value : true) &&
(excludeLocation.HasValue ? r.Settings.Location != excludeLocation.Value : true))) {
var path = context.GetResourceUrl(defaultSettings, appPath, ssl);
var url = context.GetResourceUrl(defaultSettings, appPath, ssl, _resourceFileHashProvider);
var condition = context.Settings.Condition;
var attributes = context.Settings.HasAttributes ? context.Settings.Attributes : null;
IHtmlString result;
if (resourceType == "stylesheet") {
result = Display.Style(Url: path, Condition: condition, Resource: context.Resource, TagAttributes: attributes);
result = Display.Style(Url: url, Condition: condition, Resource: context.Resource, TagAttributes: attributes);
}
else if (resourceType == "script") {
result = Display.Script(Url: path, Condition: condition, Resource: context.Resource, TagAttributes: attributes);
result = Display.Script(Url: url, Condition: condition, Resource: context.Resource, TagAttributes: attributes);
}
else {
result = Display.Resource(Url: path, Condition: condition, Resource: context.Resource, TagAttributes: attributes);
result = Display.Resource(Url: url, Condition: condition, Resource: context.Resource, TagAttributes: attributes);
}
Output.Write(result);
}

View File

@@ -192,6 +192,11 @@ namespace Orchard.Setup {
set { throw new NotImplementedException(); }
}
public bool UseFileHash {
get { return false; }
set { throw new NotImplementedException(); }
}
public int PageSize {
get { return SiteSettingsPart.DefaultPageSize; }
set { throw new NotImplementedException(); }

View File

@@ -86,6 +86,11 @@ namespace Orchard.Tokens.Tests {
set { throw new NotImplementedException(); }
}
public bool UseFileHash {
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public int PageSize {
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }

View File

@@ -12,7 +12,7 @@
margin-left: auto;
margin-right: auto;
}
.row {
display: block;
margin: 0 0 20px 0;

View File

@@ -85,18 +85,19 @@ namespace Orchard.DisplayManagement.Descriptors.ResourceBindingStrategy {
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})
.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;
Dictionary<string, string> attributes = shape.TagAttributes;
ResourceManager.WriteResource(output, resource, hit.fileVirtualPath, condition, attributes);
return null;
});
var shape = ((dynamic)displayContext.Value);
var output = displayContext.ViewContext.Writer;
ResourceDefinition resource = shape.Resource;
string url = shape.Url;
string condition = shape.Condition;
Dictionary<string, string> attributes = shape.TagAttributes;
ResourceManager.WriteResource(output, resource, url ?? hit.fileVirtualPath, condition, attributes);
return null;
});
}
}
}

View File

@@ -12,13 +12,13 @@ using Orchard.Caching;
using Orchard.Data;
using Orchard.Environment.AutofacUtil;
using Orchard.Environment.Configuration;
using Orchard.Environment.Descriptor;
using Orchard.Environment.Extensions;
using Orchard.Environment.Extensions.Compilers;
using Orchard.Environment.Extensions.Folders;
using Orchard.Environment.Extensions.Loaders;
using Orchard.Environment.ShellBuilders;
using Orchard.Environment.State;
using Orchard.Environment.Descriptor;
using Orchard.Events;
using Orchard.Exceptions;
using Orchard.FileSystems.AppData;
@@ -33,10 +33,9 @@ using Orchard.Mvc.Filters;
using Orchard.Mvc.ViewEngines.Razor;
using Orchard.Mvc.ViewEngines.ThemeAwareness;
using Orchard.Services;
using Orchard.UI.Resources;
using Orchard.WebApi;
using Orchard.WebApi.Filters;
using System.Linq;
using System.Web.Configuration;
namespace Orchard.Environment {
public static class OrchardStarter {
@@ -74,6 +73,7 @@ namespace Orchard.Environment {
builder.RegisterType<ViewsBackgroundCompilation>().As<IViewsBackgroundCompilation>().SingleInstance();
builder.RegisterType<DefaultExceptionPolicy>().As<IExceptionPolicy>().SingleInstance();
builder.RegisterType<DefaultCriticalErrorProvider>().As<ICriticalErrorProvider>().SingleInstance();
builder.RegisterType<ResourceFileHashProvider>().As<IResourceFileHashProvider>().SingleInstance();
//builder.RegisterType<RazorTemplateCache>().As<IRazorTemplateProvider>().SingleInstance();
RegisterVolatileProvider<WebSiteFolder, IWebSiteFolder>(builder);

View File

@@ -127,6 +127,7 @@
<Reference Include="System.Runtime.Serialization">
<RequiredTargetFramework>3.0</RequiredTargetFramework>
</Reference>
<Reference Include="System.Security" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.ServiceModel.Activation" />
<Reference Include="System.Transactions" />
@@ -450,7 +451,9 @@
<Compile Include="UI\Navigation\NavigationHelper.cs" />
<Compile Include="UI\Navigation\Pager.cs" />
<Compile Include="UI\Navigation\PagerParameters.cs" />
<Compile Include="UI\Resources\IResourceFileHashProvider.cs" />
<Compile Include="UI\Resources\IResourceManifestProvider.cs" />
<Compile Include="UI\Resources\ResourceFileHashProvider.cs" />
<Compile Include="UI\Resources\ResourceManifestBuilder.cs" />
<Compile Include="UI\Zones\LayoutWorkContext.cs" />
<Compile Include="UI\Zones\ZoneHoldingBehavior.cs" />

View File

@@ -14,6 +14,7 @@ namespace Orchard.Settings {
string SiteCalendar { get; set; }
ResourceDebugMode ResourceDebugMode { get; set; }
bool UseCdn { get; set; }
bool UseFileHash { get; set; }
int PageSize { get; set; }
int MaxPageSize { get; set; }
int MaxPagedCount { get; set; }

View File

@@ -0,0 +1,5 @@
namespace Orchard.UI.Resources {
public interface IResourceFileHashProvider : ISingletonDependency {
string GetResourceFileHash(string physicalPath);
}
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Web.Mvc;
using System.Linq;
namespace Orchard.UI.Resources {
public class RequireSettings {
@@ -13,6 +12,7 @@ namespace Orchard.UI.Resources {
public string Culture { get; set; }
public bool DebugMode { get; set; }
public bool CdnMode { get; set; }
public bool FileHashMode { get; set; }
public ResourceLocation Location { get; set; }
public string Condition { get; set; }
public Action<ResourceDefinition> InlineDefinition { get; set; }
@@ -69,8 +69,17 @@ namespace Orchard.UI.Resources {
return UseCdn(true);
}
public RequireSettings UseCdn(bool cdn) {
CdnMode |= cdn;
public RequireSettings UseCdn(bool cdnMode) {
CdnMode |= cdnMode;
return this;
}
public RequireSettings UseFileHash() {
return UseFileHash(true);
}
public RequireSettings UseFileHash(bool fileHashMode) {
FileHashMode |= fileHashMode;
return this;
}
@@ -132,6 +141,7 @@ namespace Orchard.UI.Resources {
.WithBasePath(BasePath).WithBasePath(other.BasePath)
.UseCdn(CdnMode).UseCdn(other.CdnMode)
.UseDebugMode(DebugMode).UseDebugMode(other.DebugMode)
.UseFileHash(FileHashMode).UseFileHash(other.FileHashMode)
.UseCulture(Culture).UseCulture(other.Culture)
.UseCondition(Condition).UseCondition(other.Condition)
.Define(InlineDefinition).Define(other.InlineDefinition);

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Web;
using System.Web.Hosting;
using System.Web.Mvc;
namespace Orchard.UI.Resources {
@@ -29,7 +30,8 @@ namespace Orchard.UI.Resources {
};
private string _basePath;
private readonly Dictionary<RequireSettings, string> _urlResolveCache = new Dictionary<RequireSettings, string>();
private string _physicalPath;
private string _physicalPathDebug;
public ResourceDefinition(ResourceManifest manifest, string type, string name) {
Manifest = manifest;
@@ -63,7 +65,7 @@ namespace Orchard.UI.Resources {
_resourceTypeDirectories.TryGetValue(resourceType, out path);
return path ?? "";
}
private static string Coalesce(params string[] strings) {
foreach (var str in strings) {
if (!String.IsNullOrEmpty(str)) {
@@ -81,6 +83,11 @@ namespace Orchard.UI.Resources {
public string Name { get; private set; }
public string Type { get; private set; }
public string Version { get; private set; }
public string Url { get; private set; }
public string UrlDebug { get; private set; }
public string UrlCdn { get; private set; }
public string UrlCdnDebug { get; private set; }
public string BasePath {
get {
if (!String.IsNullOrEmpty(_basePath)) {
@@ -93,10 +100,25 @@ namespace Orchard.UI.Resources {
return basePath ?? "";
}
}
public string Url { get; private set; }
public string UrlDebug { get; private set; }
public string UrlCdn { get; private set; }
public string UrlCdnDebug { get; private set; }
public string PhysicalPath {
get {
if (!String.IsNullOrEmpty(_physicalPath)) {
return _physicalPath;
}
return GetPhysicalPath(Url);
}
}
public string PhysicalPathDebug {
get {
if (!String.IsNullOrEmpty(_physicalPathDebug)) {
return _physicalPathDebug;
}
return GetPhysicalPath(UrlDebug);
}
}
public string[] Cultures { get; private set; }
public bool CdnSupportsSsl { get; private set; }
public IEnumerable<string> Dependencies { get; private set; }
@@ -158,6 +180,21 @@ namespace Orchard.UI.Resources {
return this;
}
public ResourceDefinition SetPhysicalPath(string physicalPath) {
return SetPhysicalPath(physicalPath, null);
}
public ResourceDefinition SetPhysicalPath(string physicalPath, string physicalPathDebug) {
if (String.IsNullOrEmpty(physicalPath)) {
throw new ArgumentNullException("physicalPath");
}
_physicalPath = physicalPath;
if (physicalPathDebug != null) {
_physicalPathDebug = physicalPathDebug;
}
return this;
}
/// <summary>
/// Sets the version of the resource.
/// </summary>
@@ -177,15 +214,13 @@ namespace Orchard.UI.Resources {
return this;
}
public string ResolveUrl(RequireSettings settings, string applicationPath) {
return ResolveUrl(settings, applicationPath, false);
public string ResolveUrl(RequireSettings settings, string applicationPath, IResourceFileHashProvider resourceFileHashProvider) {
return ResolveUrl(settings, applicationPath, false, resourceFileHashProvider);
}
public string ResolveUrl(RequireSettings settings, string applicationPath, bool ssl) {
public string ResolveUrl(RequireSettings settings, string applicationPath, bool ssl, IResourceFileHashProvider resourceFileHashProvider) {
string url;
if (_urlResolveCache.TryGetValue(settings, out url)) {
return url;
}
string physicalPath = null;
// Url priority:
if (!ssl || (ssl && CdnSupportsSsl)) { //Not ssl or ssl and cdn supports it
if (settings.DebugMode) {
@@ -204,6 +239,12 @@ namespace Orchard.UI.Resources {
? Coalesce(UrlDebug, Url)
: Coalesce(Url, UrlDebug);
}
if (url == UrlDebug) {
physicalPath = PhysicalPathDebug;
}
else if (url == Url) {
physicalPath = PhysicalPath;
}
if (String.IsNullOrEmpty(url)) {
return null;
}
@@ -222,11 +263,13 @@ namespace Orchard.UI.Resources {
? VirtualPathUtility.ToAbsolute(url, applicationPath)
: VirtualPathUtility.ToAbsolute(url);
}
_urlResolveCache[settings] = url;
if (settings.FileHashMode && !String.IsNullOrEmpty(physicalPath) && File.Exists(physicalPath)) {
url = AddQueryStringValue(url, "fileHash", resourceFileHashProvider.GetResourceFileHash(physicalPath));
}
return url;
}
public string FindNearestCulture(string culture) {
private string FindNearestCulture(string culture) {
// go for an exact match
if (Cultures == null) {
return null;
@@ -261,5 +304,34 @@ namespace Orchard.UI.Resources {
return (Name ?? "").GetHashCode() ^ (Type ?? "").GetHashCode();
}
private string GetPhysicalPath(string url) {
if (!String.IsNullOrEmpty(url) && !Uri.IsWellFormedUriString(url, UriKind.Absolute) && !url.StartsWith("//")) {
if (VirtualPathUtility.IsAbsolute(url) || VirtualPathUtility.IsAppRelative(url)) {
return HostingEnvironment.MapPath(url);
}
if (!String.IsNullOrEmpty(BasePath)) {
return HostingEnvironment.MapPath(VirtualPathUtility.Combine(BasePath, url));
}
}
return null;
}
private string AddQueryStringValue(string url, string name, string value) {
if (String.IsNullOrEmpty(url)) {
return null;
}
var encodedValue = HttpUtility.UrlEncode(value);
if (url.Contains("?")) {
if (url.EndsWith("&")) {
return String.Format("{0}{1}={2}", url, name, encodedValue);
}
else {
return String.Format("{0}&{1}={2}", url, name, encodedValue);
}
}
else {
return String.Format("{0}?{1}={2}", url, name, encodedValue);
}
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Security.Cryptography;
namespace Orchard.UI.Resources {
public class ResourceFileHashProvider : IResourceFileHashProvider {
private ConcurrentDictionary<string, HashInfo> _hashInfoCache = new ConcurrentDictionary<string, HashInfo>();
public string GetResourceFileHash(string filePath) {
if (!File.Exists(filePath))
throw new ArgumentException(String.Format("File with path '{0}' could not be found.", filePath), "physicalPath");
var lastWriteTime = File.GetLastWriteTimeUtc(filePath);
var hashInfo =
_hashInfoCache.AddOrUpdate(filePath,
addValueFactory: (key) => new HashInfo(lastWriteTime, ComputeHash(filePath)),
updateValueFactory: (key, oldHashInfo) => oldHashInfo.LastWriteTime >= lastWriteTime ? oldHashInfo : new HashInfo(lastWriteTime, ComputeHash(filePath)));
return hashInfo.Hash;
}
private string ComputeHash(string filePath) {
using (var md5 = MD5.Create()) {
using (var fileStream = File.OpenRead(filePath)) {
var hashBytes = md5.ComputeHash(fileStream);
return Convert.ToBase64String(hashBytes);
}
}
}
private class HashInfo {
public HashInfo(DateTime lastWriteTime, string hash) {
LastWriteTime = lastWriteTime;
Hash = hash;
}
public readonly DateTime LastWriteTime;
public readonly string Hash;
}
}
}

View File

@@ -6,6 +6,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Hosting;
using System.Web.Mvc;
using Autofac.Features.Metadata;
using Orchard.Environment.Extensions.Models;
@@ -27,14 +28,14 @@ namespace Orchard.UI.Resources {
private const string NotIE = "!IE";
private static string ToAppRelativePath(string resourcePath) {
if (!String.IsNullOrEmpty(resourcePath) && !Uri.IsWellFormedUriString(resourcePath, UriKind.Absolute)) {
if (!String.IsNullOrEmpty(resourcePath) && !Uri.IsWellFormedUriString(resourcePath, UriKind.Absolute) && !resourcePath.StartsWith("//")) {
resourcePath = VirtualPathUtility.ToAppRelative(resourcePath);
}
return resourcePath;
}
private static string FixPath(string resourcePath, string relativeFromPath) {
if (!String.IsNullOrEmpty(resourcePath) && !VirtualPathUtility.IsAbsolute(resourcePath) && !Uri.IsWellFormedUriString(resourcePath, UriKind.Absolute)) {
private static string ToAbsolutePath(string resourcePath, string relativeFromPath) {
if (!String.IsNullOrEmpty(resourcePath) && !VirtualPathUtility.IsAbsolute(resourcePath) && !Uri.IsWellFormedUriString(resourcePath, UriKind.Absolute) && !resourcePath.StartsWith("//")) {
// appears to be a relative path (e.g. 'foo.js' or '../foo.js', not "/foo.js" or "http://..")
if (String.IsNullOrEmpty(relativeFromPath)) {
throw new InvalidOperationException("ResourcePath cannot be relative unless a base relative path is also provided.");
@@ -44,6 +45,13 @@ namespace Orchard.UI.Resources {
return resourcePath;
}
private static string ToPhysicalPath(string resourcePath) {
if (!String.IsNullOrEmpty(resourcePath) && (VirtualPathUtility.IsAppRelative(resourcePath) || VirtualPathUtility.IsAbsolute(resourcePath)) && !Uri.IsWellFormedUriString(resourcePath, UriKind.Absolute) && !resourcePath.StartsWith("//")) {
return HostingEnvironment.MapPath(resourcePath);
}
return null;
}
private static TagBuilder GetTagBuilder(ResourceDefinition resource, string url) {
var tagBuilder = new TagBuilder(resource.TagName);
tagBuilder.MergeAttributes(resource.TagBuilder.Attributes);
@@ -142,17 +150,27 @@ namespace Orchard.UI.Resources {
throw new ArgumentNullException("resourcePath");
}
// ~/ ==> convert to absolute path (e.g. /orchard/..)
// Convert app-relative paths (~/) to absolute paths (e.g. /orchard/..)
if (VirtualPathUtility.IsAppRelative(resourcePath)) {
resourcePath = VirtualPathUtility.ToAbsolute(resourcePath);
}
if (resourceDebugPath != null && VirtualPathUtility.IsAppRelative(resourceDebugPath)) {
resourceDebugPath = VirtualPathUtility.ToAbsolute(resourceDebugPath);
}
// Convert relative paths (e.g. dir/file.css) to absolute paths.
resourcePath = ToAbsolutePath(resourcePath, relativeFromPath);
resourceDebugPath = ToAbsolutePath(resourceDebugPath, relativeFromPath);
resourcePath = FixPath(resourcePath, relativeFromPath);
resourceDebugPath = FixPath(resourceDebugPath, relativeFromPath);
return Require(resourceType, ToAppRelativePath(resourcePath)).Define(d => d.SetUrl(resourcePath, resourceDebugPath));
// Resolve absolute paths (not full URLs) to physical file paths.
var resourcePhysicalPath = ToPhysicalPath(resourcePath);
var resourceDebugPhysicalPath = ToPhysicalPath(resourceDebugPath);
return Require(resourceType, ToAppRelativePath(resourcePath)).Define(d => {
d.SetUrl(resourcePath, resourceDebugPath);
if (resourcePhysicalPath != null)
d.SetPhysicalPath(resourcePhysicalPath, resourceDebugPhysicalPath);
});
}
public virtual void RegisterHeadScript(string script) {
@@ -266,8 +284,13 @@ namespace Orchard.UI.Resources {
}
ExpandDependencies(resource, settings, allResources);
}
requiredResources = (from DictionaryEntry entry in allResources
select new ResourceRequiredContext { Resource = (ResourceDefinition)entry.Key, Settings = (RequireSettings)entry.Value }).ToList();
requiredResources = (
from DictionaryEntry entry in allResources
select new ResourceRequiredContext() {
Resource = (ResourceDefinition)entry.Key,
Settings = (RequireSettings)entry.Value
}
).ToList();
_builtResources[resourceType] = requiredResources;
return requiredResources;
}
@@ -285,8 +308,9 @@ namespace Orchard.UI.Resources {
? ((RequireSettings)allResources[resource]).Combine(settings)
: new RequireSettings { Type = resource.Type, Name = resource.Name }.Combine(settings);
if (resource.Dependencies != null) {
var dependencies = from d in resource.Dependencies
select FindResource(new RequireSettings { Type = resource.Type, Name = d });
var dependencies =
from d in resource.Dependencies
select FindResource(new RequireSettings { Type = resource.Type, Name = d });
foreach (var dependency in dependencies) {
if (dependency == null) {
continue;

View File

@@ -6,15 +6,15 @@ namespace Orchard.UI.Resources {
public ResourceDefinition Resource { get; set; }
public RequireSettings Settings { get; set; }
public string GetResourceUrl(RequireSettings baseSettings, string appPath, bool ssl) {
return Resource.ResolveUrl(baseSettings == null ? Settings : baseSettings.Combine(Settings), appPath, ssl);
public string GetResourceUrl(RequireSettings baseSettings, string appPath, bool ssl, IResourceFileHashProvider resourceFileHashProvider) {
return Resource.ResolveUrl(baseSettings == null ? Settings : baseSettings.Combine(Settings), appPath, ssl, resourceFileHashProvider);
}
public TagBuilder GetTagBuilder(RequireSettings baseSettings, string appPath) {
public TagBuilder GetTagBuilder(RequireSettings baseSettings, string appPath, IResourceFileHashProvider resourceFileHashProvider) {
var tagBuilder = new TagBuilder(Resource.TagName);
tagBuilder.MergeAttributes(Resource.TagBuilder.Attributes);
if (!String.IsNullOrEmpty(Resource.FilePathAttributeName)) {
var resolvedUrl = GetResourceUrl(baseSettings, appPath, false);
var resolvedUrl = GetResourceUrl(baseSettings, appPath, false, resourceFileHashProvider);
if (!String.IsNullOrEmpty(resolvedUrl)) {
tagBuilder.MergeAttribute(Resource.FilePathAttributeName, resolvedUrl, true);
}