diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/Endpoints/GetSdkEndpoints.cs b/src/OrchardCore.Modules/OrchardCore.Facebook/Endpoints/GetSdkEndpoints.cs new file mode 100644 index 00000000000..20d8b3aed6e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/Endpoints/GetSdkEndpoints.cs @@ -0,0 +1,139 @@ +using System.IO.Hashing; +using System.Text; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Caching.Memory; +using OrchardCore.Facebook.Settings; +using OrchardCore.Settings; + +#nullable enable + +namespace OrchardCore.Facebook.Endpoints; + +public static class GetSdkEndpoints +{ + public static IEndpointRouteBuilder AddSdkEndpoints(this IEndpointRouteBuilder builder) + { + builder.MapGet("/OrchardCore.Facebook/sdk/init.js", GetInitScriptEndpoint.HandleRequestAsync) + .AllowAnonymous() + .DisableAntiforgery(); + + builder.MapGet("/OrchardCore.Facebook/sdk/sdk.{culture:length(2,6)}.js", GetFetchScriptEndpoint.HandleRequestAsync) + .AllowAnonymous() + .DisableAntiforgery(); + + return builder; + } + + public static class GetInitScriptEndpoint + { + // Update this version when the script changes to invalidate client caches + private static readonly int ScriptVersion = 1; + + private static readonly string CacheKey = "Facebook.GetInitScriptEndpoint"; + + public static ulong HashCacheBustingValues(FacebookSettings settings) + { + var hash = new XxHash3(ScriptVersion); + hash.Append(Encoding.UTF8.GetBytes(settings.AppId ?? "")); + hash.Append(Encoding.UTF8.GetBytes(settings.Version ?? "")); + hash.Append(Encoding.UTF8.GetBytes(settings.FBInitParams ?? "")); + return hash.GetCurrentHashAsUInt64(); + } + + public static async Task HandleRequestAsync(HttpContext context, ISiteService siteService, IMemoryCache cache) + { + byte[] scriptBytes; + var settings = await siteService.GetSettingsAsync(); + // Regenerate hash: Don't trust url hash because it could cause cache issues + var expectedHash = HashCacheBustingValues(settings); + + var cachedScript = cache.Get(CacheKey) as KeyValuePair?; + if (cachedScript == null || cachedScript.Value.Key != expectedHash) + { + // Note: Update ScriptVersion constant when the script changes + // Note: All injected values except those in url must be used in HashCacheBustingValues + scriptBytes = Encoding.UTF8.GetBytes($@" + window.fbAsyncInit = function() {{ + FB.init({{ + appId:'{settings.AppId}', + version:'{settings.Version}', + {settings.FBInitParams} + }}); + }};"); + + cache.Set(CacheKey, + new KeyValuePair (expectedHash, scriptBytes), + new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromHours(1))); + } + else + { + scriptBytes = cachedScript.Value.Value; + } + + // Set the cache timeout to the maximum allowed length of one year + // max-age is needed because immutable is not widely supported + context.Response.Headers.CacheControl = "public, max-age=31536000, immutable"; + + return Results.Bytes(scriptBytes, "application/javascript"); + } + } + + public static class GetFetchScriptEndpoint + { + // Update this version when the script changes to invalidate client caches + private static readonly int ScriptVersion = 1; + + private static readonly string CacheKey = "Facebook.GetFetchScriptEndpoint"; + + // Scraped from facebook.com + public static readonly string[] ValidFacebookCultures = { "en-US", "es-LA", "pt-BR", "fr-FR", "de-DE", "so-SO", "af-ZA", "az-AZ", "id-ID", "ms-MY", "jv-ID", "cx-PH", "bs-BA", "br-FR", "ca-ES", "cs-CZ", "co-FR", "cy-GB", "da-DK", "de-DE", "et-EE", "en-GB", "en-US", "es-LA", "es-ES", "eo-EO", "eu-ES", "tl-PH", "fo-FO", "fr-CA", "fr-FR", "fy-NL", "ff-NG", "fn-IT", "ga-IE", "gl-ES", "gn-PY", "ha-NG", "hr-HR", "rw-RW", "iu-CA", "ik-US", "is-IS", "it-IT", "sw-KE", "ht-HT", "ku-TR", "lv-LV", "lt-LT", "hu-HU", "mg-MG", "mt-MT", "nl-NL", "nb-NO", "nn-NO", "uz-UZ", "pl-PL", "pt-BR", "pt-PT", "ro-RO", "sc-IT", "sn-ZW", "sq-AL", "sz-PL", "sk-SK", "sl-SI", "fi-FI", "sv-SE", "vi-VN", "tr-TR", "nl-BE", "zz-TR", "el-GR", "be-BY", "bg-BG", "ky-KG", "kk-KZ", "mk-MK", "mn-MN", "ru-RU", "sr-RS", "tt-RU", "tg-TJ", "uk-UA", "ka-GE", "hy-AM", "he-IL", "ur-PK", "ar-AR", "ps-AF", "fa-IR", "cb-IQ", "sy-SY", "tz-MA", "am-ET", "ne-NP", "mr-IN", "hi-IN", "as-IN", "bn-IN", "pa-IN", "gu-IN", "or-IN", "ta-IN", "te-IN", "kn-IN", "ml-IN", "si-LK", "th-TH", "lo-LA", "my-MM", "km-KH", "ko-KR", "zh-TW", "zh-CN", "zh-HK", "ja-JP", "ja-KS" }; + + public static ulong HashCacheBustingValues(FacebookSettings settings) + { + var hash = new XxHash3(ScriptVersion); + hash.Append(Encoding.UTF8.GetBytes(settings.SdkJs ?? "")); + return hash.GetCurrentHashAsUInt64(); + } + + public static async Task HandleRequestAsync(string culture, HttpContext context, IMemoryCache cache, UrlEncoder urlEncoder, ISiteService siteService) + { + byte[] scriptBytes; + var settings = await siteService.GetSettingsAsync(); + // Regenerate hash: Don't trust url hash because it could cause cache issues + var expectedHash = HashCacheBustingValues(settings); + + var cachedScript = cache.Get(CacheKey) as KeyValuePair?; + if (cachedScript == null || cachedScript.Value.Key != expectedHash) + { + var encodedCulture = urlEncoder.Encode(culture.Replace('-', '_')); + + // Note: If a culture is not found, facebook will use en_US + // Note: Update ScriptVersion constant when the script changes + // Note: All injected values except those in url must be used in HashCacheBustingValues + scriptBytes = Encoding.UTF8.GetBytes($@"(function(d){{ + var js, id = 'facebook-jssdk'; if (d.getElementById(id)) {{ return; }} + js = d.createElement('script'); js.id = id; js.async = true; + js.src = ""https://connect.facebook.net/{encodedCulture}/{settings.SdkJs}""; + d.getElementsByTagName('head')[0].appendChild(js); + }} (document));"); + + cache.Set(CacheKey, + new KeyValuePair (expectedHash, scriptBytes), + new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromHours(1))); + } + else + { + scriptBytes = cachedScript.Value.Value; + } + + // Set the cache timeout to the maximum allowed length of one year + // max-age is needed because immutable is not widely supported + context.Response.Headers.CacheControl = "public, max-age=31536000, immutable"; + + return Results.Bytes(scriptBytes, "application/javascript"); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/Filters/FBInitFilter.cs b/src/OrchardCore.Modules/OrchardCore.Facebook/Filters/FBInitFilter.cs index 97cf4bf1520..357bb245fad 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/Filters/FBInitFilter.cs +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/Filters/FBInitFilter.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Microsoft.AspNetCore.Mvc.Filters; using OrchardCore.Admin; using OrchardCore.Facebook.Settings; @@ -31,6 +32,7 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE if (settings.FBInit) { var setting = _resourceManager.RegisterResource("script", "fb"); + setting.Culture = CultureInfo.CurrentUICulture.Name; setting.AtLocation(ResourceLocation.Foot); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/OrchardCore.Facebook.csproj b/src/OrchardCore.Modules/OrchardCore.Facebook/OrchardCore.Facebook.csproj index c99a2c010bd..188788f0dc7 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/OrchardCore.Facebook.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/OrchardCore.Facebook.csproj @@ -31,6 +31,7 @@ + diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/ResourceManagementOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Facebook/ResourceManagementOptionsConfiguration.cs index beecba19c8d..b5eee1c5594 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/ResourceManagementOptionsConfiguration.cs +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/ResourceManagementOptionsConfiguration.cs @@ -1,25 +1,36 @@ using Microsoft.Extensions.Options; +using OrchardCore.Facebook.Endpoints; +using OrchardCore.Facebook.Settings; using OrchardCore.ResourceManagement; +using OrchardCore.Settings; namespace OrchardCore.Facebook; public sealed class ResourceManagementOptionsConfiguration : IConfigureOptions { - private static readonly ResourceManifest _manifest; + private readonly ISiteService _siteService; - static ResourceManagementOptionsConfiguration() + public ResourceManagementOptionsConfiguration(ISiteService siteService) { - _manifest = new ResourceManifest(); + _siteService = siteService; + } + + public async void Configure(ResourceManagementOptions options) + { + var settings = await _siteService.GetSettingsAsync(); - _manifest + var manifest = new ResourceManifest(); + + manifest .DefineScript("fb") .SetDependencies("fbsdk") - .SetUrl("~/OrchardCore.Facebook/sdk/fb.js"); + .SetUrl($"~/OrchardCore.Facebook/sdk/init.js?v={GetSdkEndpoints.GetInitScriptEndpoint.HashCacheBustingValues(settings)}"); - _manifest + manifest .DefineScript("fbsdk") - .SetUrl("~/OrchardCore.Facebook/sdk/fbsdk.js"); - } + .SetCultures(GetSdkEndpoints.GetFetchScriptEndpoint.ValidFacebookCultures) + .SetUrl($"~/OrchardCore.Facebook/sdk/sdk.js?v={GetSdkEndpoints.GetInitScriptEndpoint.HashCacheBustingValues(settings)}"); - public void Configure(ResourceManagementOptions options) => options.ResourceManifests.Add(_manifest); + options.ResourceManifests.Add(manifest); + } } diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/ScriptsMiddleware.cs b/src/OrchardCore.Modules/OrchardCore.Facebook/ScriptsMiddleware.cs deleted file mode 100644 index e038f63e7b7..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/ScriptsMiddleware.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Globalization; -using System.Text; -using Microsoft.AspNetCore.Http; -using OrchardCore.Facebook.Settings; -using OrchardCore.Settings; - -namespace OrchardCore.Facebook; - -public class ScriptsMiddleware -{ - private readonly RequestDelegate _next; - private readonly ISiteService _siteService; - - public ScriptsMiddleware(RequestDelegate next, ISiteService siteService) - { - _next = next; - _siteService = siteService; - } - - public async Task Invoke(HttpContext httpContext) - { - ArgumentNullException.ThrowIfNull(httpContext); - - if (httpContext.Request.Path.StartsWithSegments("/OrchardCore.Facebook/sdk", StringComparison.OrdinalIgnoreCase)) - { - var script = default(string); - var settings = await _siteService.GetSettingsAsync(); - - if (Path.GetFileName(httpContext.Request.Path.Value) == "fbsdk.js") - { - var locale = CultureInfo.CurrentUICulture.Name.Replace('-', '_'); - script = $@"(function(d){{ - var js, id = 'facebook-jssdk'; if (d.getElementById(id)) {{ return; }} - js = d.createElement('script'); js.id = id; js.async = true; - js.src = ""https://connect.facebook.net/{locale}/{settings.SdkJs}""; - d.getElementsByTagName('head')[0].appendChild(js); - }} (document));"; - } - else if (Path.GetFileName(httpContext.Request.Path.Value) == "fb.js") - { - if (!string.IsNullOrWhiteSpace(settings?.AppId)) - { - var options = $"{{ appId:'{settings.AppId}',version:'{settings.Version}'"; - options = string.IsNullOrWhiteSpace(settings.FBInitParams) - ? string.Concat(options, "}") - : string.Concat(options, ",", settings.FBInitParams, "}"); - - script = $"window.fbAsyncInit = function(){{ FB.init({options});}};"; - } - } - - if (script != null) - { - var bytes = Encoding.UTF8.GetBytes(script); - httpContext.Response.ContentType = "text/javascript"; - await httpContext.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(script).AsMemory(0, bytes.Length), httpContext.RequestAborted); - - return; - } - } - - await _next.Invoke(httpContext); - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/Settings/FacebookSettings.cs b/src/OrchardCore.Modules/OrchardCore.Facebook/Settings/FacebookSettings.cs index af104d06ff2..ef153be1271 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/Settings/FacebookSettings.cs +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/Settings/FacebookSettings.cs @@ -6,9 +6,11 @@ public class FacebookSettings public string AppSecret { get; set; } public bool FBInit { get; set; } - public string FBInitParams { get; set; } = @"status:true, -xfbml:true, -autoLogAppEvents:true"; + public string FBInitParams { get; set; } = """ + status: true, + xfbml: true, + autoLogAppEvents: true + """; public string SdkJs { get; set; } = "sdk.js"; public string Version { get; set; } = "v3.2"; diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Facebook/Startup.cs index ee85a468a2c..a299daae8ee 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/Startup.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Facebook.Endpoints; using OrchardCore.Facebook.Drivers; using OrchardCore.Facebook.Filters; using OrchardCore.Facebook.Recipes; @@ -20,7 +21,7 @@ public sealed class Startup : StartupBase { public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) { - builder.UseMiddleware(); + routes.AddSdkEndpoints(); } public override void ConfigureServices(IServiceCollection services) diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookPluginPart.Summary.cshtml b/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookPluginPart.Summary.cshtml index dad761f16b4..029e3be2c64 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookPluginPart.Summary.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookPluginPart.Summary.cshtml @@ -1,3 +1,4 @@ +@using System.Globalization @using OrchardCore.Entities @using OrchardCore.Settings @using OrchardCore.Facebook.Settings @@ -5,11 +6,12 @@ @inject ISiteService SiteService @{ var fbInit = (await SiteService.GetSettingsAsync()).FBInit; + var culture = CultureInfo.CurrentUICulture.Name; } @Html.Raw(Model.Html) @if (!fbInit) { - + } diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookPluginPart.cshtml b/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookPluginPart.cshtml index dad761f16b4..029e3be2c64 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookPluginPart.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookPluginPart.cshtml @@ -1,3 +1,4 @@ +@using System.Globalization @using OrchardCore.Entities @using OrchardCore.Settings @using OrchardCore.Facebook.Settings @@ -5,11 +6,12 @@ @inject ISiteService SiteService @{ var fbInit = (await SiteService.GetSettingsAsync()).FBInit; + var culture = CultureInfo.CurrentUICulture.Name; } @Html.Raw(Model.Html) @if (!fbInit) { - + } diff --git a/src/OrchardCore.Modules/OrchardCore.Liquid/Endpoints/Scripts/GetIntellisenseEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Liquid/Endpoints/Scripts/GetIntellisenseEndpoint.cs new file mode 100644 index 00000000000..3993a1ae369 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Liquid/Endpoints/Scripts/GetIntellisenseEndpoint.cs @@ -0,0 +1,89 @@ +using System.IO.Hashing; +using System.Text; +using Fluid; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using OrchardCore.DisplayManagement.Liquid; + +#nullable enable + +namespace OrchardCore.Liquid.Endpoints.Scripts; + +public static class GetIntellisenseEndpoint +{ + // Update this version when the script changes to invalidate client caches + private static readonly int ScriptVersion = 1; + + public static IEndpointRouteBuilder AddGetIntellisenseScriptEndpoint(this IEndpointRouteBuilder builder) + { + builder.MapGet("OrchardCore.Liquid/Scripts/liquid-intellisense.js", HandleRequest) + .AllowAnonymous() + .DisableAntiforgery(); + + return builder; + } + + public static ulong HashCacheBustingValues(LiquidViewParser liquidViewParser, TemplateOptions templateOptions) + { + var hash = new XxHash3(ScriptVersion); + + foreach (var filter in templateOptions.Filters) + { + hash.Append(Encoding.UTF8.GetBytes(filter.Key ?? "")); + } + + foreach (var tag in liquidViewParser.RegisteredTags) + { + hash.Append(Encoding.UTF8.GetBytes(tag.Key ?? "")); + } + + return hash.GetCurrentHashAsUInt64(); + } + + private static IResult HandleRequest(HttpContext context, IMemoryCache memoryCache, LiquidViewParser liquidViewParser, IOptions templateOptions) + { + // We could check that the current cache entry matches the requested hash, but instead we always return the actual content that + // the client should have. The hash is only used for cache busting on the client. + + // The cache entry will be busted whenever the filters or tags change, i.e. when some features are enabled or disabled. + + var scriptBytes = memoryCache.GetOrCreate("LiquidIntellisenseScript", entry => + { + entry.SetSlidingExpiration(TimeSpan.FromHours(1)); + + return GenerateScriptBytes(liquidViewParser, templateOptions); + }); + + if (scriptBytes == null) + { + return Results.StatusCode(500); + } + + // Set the cache timeout to the maximum allowed length of one year + // max-age is needed because immutable is not widely supported + context.Response.Headers.CacheControl = "public, max-age=31536000, immutable"; + + return Results.Bytes(scriptBytes, "application/javascript"); + } + + private static byte[] GenerateScriptBytes(LiquidViewParser liquidViewParser, IOptions templateOptions) + { + var filters = string.Join(',', templateOptions.Value.Filters.Select(x => $"'{x.Key}'")); + var tags = string.Join(',', liquidViewParser.RegisteredTags.Select(x => $"'{x.Key}'")); + + // Note: Update ScriptVersion when the script changes + // Note: All injected values except those in url must be used in HashCacheBustingValues + var scriptBytes = (new byte[][] { + "["u8.ToArray(), + Encoding.UTF8.GetBytes(filters), + "].forEach(value=>{if(!liquidFilters.includes(value)){ liquidFilters.push(value);}});["u8.ToArray(), + Encoding.UTF8.GetBytes(tags), + "].forEach(value=>{if(!liquidTags.includes(value)){ liquidTags.push(value);}});"u8.ToArray() + }).SelectMany(x => x).ToArray(); + + return scriptBytes; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Liquid/OrchardCore.Liquid.csproj b/src/OrchardCore.Modules/OrchardCore.Liquid/OrchardCore.Liquid.csproj index 82b5fc4ea23..6b3c8305f4e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Liquid/OrchardCore.Liquid.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Liquid/OrchardCore.Liquid.csproj @@ -26,4 +26,8 @@ + + + + diff --git a/src/OrchardCore.Modules/OrchardCore.Liquid/ResourceManagementOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Liquid/ResourceManagementOptionsConfiguration.cs index f09e6fffad6..b2b71feb6e4 100644 --- a/src/OrchardCore.Modules/OrchardCore.Liquid/ResourceManagementOptionsConfiguration.cs +++ b/src/OrchardCore.Modules/OrchardCore.Liquid/ResourceManagementOptionsConfiguration.cs @@ -1,10 +1,15 @@ +using Fluid; using Microsoft.Extensions.Options; +using OrchardCore.DisplayManagement.Liquid; +using OrchardCore.Liquid.Endpoints.Scripts; using OrchardCore.ResourceManagement; namespace OrchardCore.Liquid; public sealed class ResourceManagementOptionsConfiguration : IConfigureOptions { + private readonly LiquidViewParser _liquidViewParser; + private readonly IOptions _templateOptions; private static readonly ResourceManifest _manifest; static ResourceManagementOptionsConfiguration() @@ -16,15 +21,28 @@ static ResourceManagementOptionsConfiguration() .SetUrl("~/OrchardCore.Liquid/monaco/liquid-intellisense.js") .SetDependencies("monaco") .SetVersion("1.0.0"); + } - _manifest - .DefineScript("liquid-intellisense") - .SetDependencies("monaco-liquid-intellisense") - .SetUrl("~/OrchardCore.Liquid/Scripts/liquid-intellisense.js"); + public ResourceManagementOptionsConfiguration(LiquidViewParser liquidViewParser, IOptions templateOptions) + { + _liquidViewParser = liquidViewParser; + _templateOptions = templateOptions; } public void Configure(ResourceManagementOptions options) { + // The site is restarted when settings change + + var hash = GetIntellisenseEndpoint.HashCacheBustingValues(_liquidViewParser, _templateOptions.Value); + + var manifest = new ResourceManifest(); + + manifest + .DefineScript("liquid-intellisense") + .SetDependencies("monaco-liquid-intellisense") + .SetUrl($"~/OrchardCore.Liquid/Scripts/liquid-intellisense.js?v={hash}"); + options.ResourceManifests.Add(_manifest); + options.ResourceManifests.Add(manifest); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Liquid/ScriptsMiddleware.cs b/src/OrchardCore.Modules/OrchardCore.Liquid/ScriptsMiddleware.cs deleted file mode 100644 index 6c5c148f961..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Liquid/ScriptsMiddleware.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Text; -using Fluid; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; -using OrchardCore.DisplayManagement.Liquid; -using OrchardCore.Environment.Shell.Configuration; - -namespace OrchardCore.Liquid; - -public class ScriptsMiddleware -{ - private readonly RequestDelegate _next; - private byte[] _bytes; - private string _etag; - - public ScriptsMiddleware(RequestDelegate next) - { - _next = next; - } - - public async Task Invoke(HttpContext httpContext) - { - if (httpContext.Request.Path.StartsWithSegments("/OrchardCore.Liquid/Scripts", StringComparison.OrdinalIgnoreCase)) - { - if (Path.GetFileName(httpContext.Request.Path.Value) == "liquid-intellisense.js") - { - if (httpContext.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var v)) - { - if (v.Contains(_etag)) - { - httpContext.Response.StatusCode = StatusCodes.Status304NotModified; - - return; - } - } - - var cacheControl = $"public, max-age={TimeSpan.FromDays(30).TotalSeconds}, s-max-age={TimeSpan.FromDays(365.25).TotalSeconds}"; - if (_bytes == null) - { - var templateOptions = httpContext.RequestServices.GetRequiredService>(); - var liquidViewParser = httpContext.RequestServices.GetRequiredService(); - var shellConfiguration = httpContext.RequestServices.GetRequiredService(); - cacheControl = shellConfiguration.GetValue("StaticFileOptions:CacheControl", cacheControl); - - var filters = string.Join(',', templateOptions.Value.Filters.Select(x => $"'{x.Key}'")); - var tags = string.Join(',', liquidViewParser.RegisteredTags.Select(x => $"'{x.Key}'")); - - var script = $@"[{filters}].forEach(value=>{{if(!liquidFilters.includes(value)){{ liquidFilters.push(value);}}}}); - [{tags}].forEach(value=>{{if(!liquidTags.includes(value)){{ liquidTags.push(value);}}}});"; - - _etag = Guid.NewGuid().ToString("n"); - _bytes = Encoding.UTF8.GetBytes(script); - } - - httpContext.Response.Headers[HeaderNames.CacheControl] = cacheControl; - httpContext.Response.Headers[HeaderNames.ContentType] = "application/javascript"; - httpContext.Response.Headers[HeaderNames.ETag] = _etag; - - await httpContext.Response.Body.WriteAsync(_bytes, httpContext.RequestAborted); - - return; - } - } - - await _next.Invoke(httpContext); - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Liquid/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Liquid/Startup.cs index 2f0d4c68880..18836484259 100644 --- a/src/OrchardCore.Modules/OrchardCore.Liquid/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Liquid/Startup.cs @@ -18,6 +18,7 @@ using OrchardCore.Liquid.Models; using OrchardCore.Liquid.Services; using OrchardCore.Liquid.ViewModels; +using OrchardCore.Liquid.Endpoints.Scripts; using OrchardCore.Modules; namespace OrchardCore.Liquid; @@ -26,11 +27,13 @@ public sealed class Startup : StartupBase { public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) { - app.UseMiddleware(); + routes.AddGetIntellisenseScriptEndpoint(); } public override void ConfigureServices(IServiceCollection services) { + services.AddScoped(); + services.Configure(options => { options.Filters.AddFilter("t", LiquidViewFilters.Localize); diff --git a/src/OrchardCore/OrchardCore.ResourceManagement/ResourceManager.cs b/src/OrchardCore/OrchardCore.ResourceManagement/ResourceManager.cs index 4336b6bd90b..c3ac5d95d4a 100644 --- a/src/OrchardCore/OrchardCore.ResourceManagement/ResourceManager.cs +++ b/src/OrchardCore/OrchardCore.ResourceManagement/ResourceManager.cs @@ -393,7 +393,15 @@ private void ExpandDependenciesImplementation( // Bar dependency should be too. This behavior only applies to the dependencies. var dependencySettings = ((RequireSettings)allResources[resource])?.New() ?? new RequireSettings(_options, resource); - dependencySettings = isTopLevel ? dependencySettings.Combine(settings) : dependencySettings.AtLocation(settings.Location); + if (isTopLevel) + { + dependencySettings = dependencySettings.Combine(settings); + } + else + { + dependencySettings = dependencySettings.AtLocation(settings.Location); + dependencySettings.Culture = settings.Culture; + } dependencySettings = dependencySettings.CombinePosition(settings); if (dependencies != null)