mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-08 00:14:31 +08:00
Implemented resource file hash-based cache busting. (#7564)
This commit is contained in:
@@ -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(); }
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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); }
|
||||
|
@@ -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; }
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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(); }
|
||||
|
@@ -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(); }
|
||||
|
@@ -12,7 +12,7 @@
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
|
||||
.row {
|
||||
display: block;
|
||||
margin: 0 0 20px 0;
|
||||
|
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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" />
|
||||
|
@@ -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; }
|
||||
|
5
src/Orchard/UI/Resources/IResourceFileHashProvider.cs
Normal file
5
src/Orchard/UI/Resources/IResourceFileHashProvider.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace Orchard.UI.Resources {
|
||||
public interface IResourceFileHashProvider : ISingletonDependency {
|
||||
string GetResourceFileHash(string physicalPath);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
39
src/Orchard/UI/Resources/ResourceFileHashProvider.cs
Normal file
39
src/Orchard/UI/Resources/ResourceFileHashProvider.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user