|
//------------------------------------------------------------------------------
// <copyright file="ScriptResourceHandler.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
namespace System.Web.Handlers {
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Reflection;
using System.Resources;
using System.Security;
using System.Security.Cryptography;
using System.Security.Policy;
using System.Text;
using System.Web;
using System.Web.Configuration;
using System.Web.Hosting;
using System.Web.Resources;
using System.Web.Security.Cryptography;
using System.Web.UI;
using System.Web.Util;
using Debug = System.Diagnostics.Debug;
public class ScriptResourceHandler : IHttpHandler {
private const string _scriptResourceUrl = "~/ScriptResource.axd";
private static readonly IDictionary _assemblyInfoCache = Hashtable.Synchronized(new Hashtable());
private static readonly IDictionary _cultureCache = Hashtable.Synchronized(new Hashtable());
private static readonly Object _getMethodLock = new Object();
private static IScriptResourceHandler _scriptResourceHandler = new RuntimeScriptResourceHandler();
private static string _scriptResourceAbsolutePath;
// _bypassVirtualPathResolution set by unit tests to avoid resolving ~/ paths from unit tests.
private static bool _bypassVirtualPathResolution = false;
private static int _maximumResourceUrlLength = 2048;
private static string ScriptResourceAbsolutePath {
get {
if (_scriptResourceAbsolutePath == null) {
_scriptResourceAbsolutePath = VirtualPathUtility.ToAbsolute(_scriptResourceUrl);
}
return _scriptResourceAbsolutePath;
}
}
private static Exception Create404(Exception innerException) {
return new HttpException(404, AtlasWeb.ScriptResourceHandler_InvalidRequest, innerException);
}
internal static CultureInfo DetermineNearestAvailableCulture(
Assembly assembly,
string scriptResourceName,
CultureInfo culture) {
if (String.IsNullOrEmpty(scriptResourceName)) return CultureInfo.InvariantCulture;
Tuple<Assembly, string, CultureInfo> cacheKey = Tuple.Create(assembly, scriptResourceName, culture);
CultureInfo cachedCulture = (CultureInfo)_cultureCache[cacheKey];
if (cachedCulture == null) {
string releaseResourceName =
scriptResourceName.EndsWith(".debug.js", StringComparison.OrdinalIgnoreCase) ?
scriptResourceName.Substring(0, scriptResourceName.Length - 9) + ".js" :
null;
ScriptResourceInfo resourceInfo = ScriptResourceInfo.GetInstance(assembly, scriptResourceName);
ScriptResourceInfo releaseResourceInfo = (releaseResourceName != null) ?
ScriptResourceInfo.GetInstance(assembly, releaseResourceName) : null;
if (!String.IsNullOrEmpty(resourceInfo.ScriptResourceName) ||
((releaseResourceInfo != null) && !String.IsNullOrEmpty(releaseResourceInfo.ScriptResourceName))) {
ResourceManager resourceManager =
ScriptResourceAttribute.GetResourceManager(resourceInfo.ScriptResourceName, assembly);
ResourceManager releaseResourceManager = (releaseResourceInfo != null) ?
ScriptResourceAttribute.GetResourceManager(releaseResourceInfo.ScriptResourceName, assembly) : null;
ResourceSet localizedSet = null;
ResourceSet releaseSet = null;
if (resourceManager != null) {
resourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, true);
// Look for the explicitly localized version of the resources that is nearest the culture.
localizedSet = resourceManager.GetResourceSet(culture, true, false);
}
if (releaseResourceManager != null) {
releaseResourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, true);
// Look for the explicitly localized version of the resources that is nearest the culture.
releaseSet = releaseResourceManager.GetResourceSet(culture, true, false);
}
if ((resourceManager != null) || (releaseResourceManager != null)) {
while ((localizedSet == null) && (releaseSet == null)) {
culture = culture.Parent;
if (culture.Equals(CultureInfo.InvariantCulture)) break;
localizedSet = resourceManager.GetResourceSet(culture, true, false);
releaseSet = (releaseResourceManager != null) ?
releaseResourceManager.GetResourceSet(culture, true, false) : null;
}
}
else {
culture = CultureInfo.InvariantCulture;
}
}
else {
culture = CultureInfo.InvariantCulture;
}
// Neutral assembly culture falls back on invariant
CultureInfo neutralCulture = GetAssemblyNeutralCulture(assembly);
if ((neutralCulture != null) && neutralCulture.Equals(culture)) {
culture = CultureInfo.InvariantCulture;
}
cachedCulture = culture;
_cultureCache[cacheKey] = cachedCulture;
}
return cachedCulture;
}
private static void EnsureScriptResourceRequest(string path) {
if (!IsScriptResourceRequest(path)) {
Throw404();
}
}
private static Assembly GetAssembly(string assemblyName) {
Debug.Assert(!String.IsNullOrEmpty(assemblyName));
string[] parts = assemblyName.Split(',');
if ((parts.Length != 1) && (parts.Length != 4)) {
Throw404();
}
AssemblyName realName = new AssemblyName();
realName.Name = parts[0];
if (parts.Length == 4) {
realName.Version = new Version(parts[1]);
string cultureString = parts[2];
realName.CultureInfo = (cultureString.Length > 0) ?
new CultureInfo(cultureString) :
CultureInfo.InvariantCulture;
realName.SetPublicKeyToken(HexParser.Parse(parts[3]));
}
Assembly assembly = null;
try {
assembly = Assembly.Load(realName);
}
catch (FileNotFoundException fnf) {
Throw404(fnf);
}
catch (FileLoadException fl) {
Throw404(fl);
}
catch (BadImageFormatException badImage) {
Throw404(badImage);
}
return assembly;
}
private static Tuple<AssemblyName, String> GetAssemblyInfo(Assembly assembly) {
Tuple<AssemblyName, String> assemblyInfo =
(Tuple<AssemblyName, String>)_assemblyInfoCache[assembly];
if (assemblyInfo == null) {
assemblyInfo = GetAssemblyInfoInternal(assembly);
_assemblyInfoCache[assembly] = assemblyInfo;
}
Debug.Assert(assemblyInfo != null, "Assembly info should not be null");
return assemblyInfo;
}
private static Tuple<AssemblyName, String> GetAssemblyInfoInternal(Assembly assembly) {
AssemblyName assemblyName = new AssemblyName(assembly.FullName);
string hash = Convert.ToBase64String(assembly.ManifestModule.ModuleVersionId.ToByteArray());
return new Tuple<AssemblyName, String>(assemblyName, hash);
}
private static CultureInfo GetAssemblyNeutralCulture(Assembly assembly) {
CultureInfo neutralCulture = (CultureInfo)_cultureCache[assembly];
if (neutralCulture == null) {
object[] nrlas = assembly.GetCustomAttributes(typeof(NeutralResourcesLanguageAttribute), false);
if ((nrlas != null) && (nrlas.Length != 0)) {
neutralCulture = CultureInfo.GetCultureInfo(
((NeutralResourcesLanguageAttribute)nrlas[0]).CultureName);
_cultureCache[assembly] = neutralCulture;
}
}
return neutralCulture;
}
internal static string GetEmptyPageUrl(string title) {
return GetScriptResourceHandler().GetEmptyPageUrl(title);
}
private static IScriptResourceHandler GetScriptResourceHandler() {
if (_scriptResourceHandler == null) {
_scriptResourceHandler = new RuntimeScriptResourceHandler();
}
return _scriptResourceHandler;
}
internal static string GetScriptResourceUrl(
Assembly assembly,
string resourceName,
CultureInfo culture,
bool zip) {
return GetScriptResourceHandler()
.GetScriptResourceUrl(assembly, resourceName, culture, zip);
}
internal static string GetScriptResourceUrl(
List<Tuple<Assembly, List<Tuple<string, CultureInfo>>>> assemblyResourceLists,
bool zip) {
return GetScriptResourceHandler().GetScriptResourceUrl(assemblyResourceLists, zip);
}
protected virtual bool IsReusable {
get {
return true;
}
}
internal delegate string VirtualFileReader(string virtualPath, out Encoding encoding);
private static bool IsCompressionEnabled(HttpContext context) {
return ScriptingScriptResourceHandlerSection.ApplicationSettings.EnableCompression &&
((context == null) ||
!context.Request.Browser.IsBrowser("IE") ||
(context.Request.Browser.MajorVersion > 6));
}
internal static bool IsScriptResourceRequest(string path) {
return !String.IsNullOrEmpty(path) &&
String.Equals(path, ScriptResourceAbsolutePath, StringComparison.OrdinalIgnoreCase);
}
private static void OutputEmptyPage(HttpResponseBase response, string title) {
PrepareResponseCache(response);
response.Write(@"<!DOCTYPE html PUBLIC ""-//W3C//DTD XHTML 1.0 Transitional//EN"" ""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"">
<html xmlns=""http://www.w3.org/1999/xhtml""><head><script type=""text/javascript"">parent.Sys.Application._onIFrameLoad();</script><title>" +
HttpUtility.HtmlEncode(title) +
@"</title></head><body></body></html>");
}
private static void PrepareResponseCache(HttpResponseBase response) {
HttpCachePolicyBase cachePolicy = response.Cache;
DateTime now = DateTime.Now;
cachePolicy.SetCacheability(HttpCacheability.Public);
cachePolicy.VaryByParams["d"] = true;
cachePolicy.SetOmitVaryStar(true);
cachePolicy.SetExpires(now + TimeSpan.FromDays(365));
cachePolicy.SetValidUntilExpires(true);
cachePolicy.SetLastModified(now);
}
private static void PrepareResponseNoCache(HttpResponseBase response) {
HttpCachePolicyBase cachePolicy = response.Cache;
DateTime now = DateTime.Now;
cachePolicy.SetCacheability(HttpCacheability.Public);
cachePolicy.SetExpires(now + TimeSpan.FromDays(365));
cachePolicy.SetValidUntilExpires(true);
cachePolicy.SetLastModified(now);
cachePolicy.SetNoServerCaching();
}
[SecuritySafeCritical]
protected virtual void ProcessRequest(HttpContext context) {
ProcessRequest(new HttpContextWrapper(context));
}
internal static void ProcessRequest(HttpContextBase context, VirtualFileReader fileReader = null, Action<string, Exception> logAction = null, bool validatePath = true) {
string decryptedString = null;
bool resourceIdentifierPresent = false;
try {
HttpResponseBase response = context.Response;
response.Clear();
if (validatePath) {
// Checking that the handler is not being called from a different path.
EnsureScriptResourceRequest(context.Request.Path);
}
string encryptedData = context.Request.QueryString["d"];
if (String.IsNullOrEmpty(encryptedData)) {
Throw404();
}
resourceIdentifierPresent = true;
try {
decryptedString = Page.DecryptString(encryptedData, Purpose.ScriptResourceHandler_ScriptResourceUrl);
}
catch (CryptographicException ex) {
Throw404(ex);
}
fileReader = fileReader ?? new VirtualFileReader(delegate(string virtualPath, out Encoding encoding) {
VirtualPathProvider vpp = HostingEnvironment.VirtualPathProvider;
if (!vpp.FileExists(virtualPath)) {
Throw404();
}
VirtualFile file = vpp.GetFile(virtualPath);
if (!AppSettings.ScriptResourceAllowNonJsFiles && !file.Name.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) {
// MSRC 10405: Disallow all extensions other than *.js
Throw404();
}
using (Stream stream = file.Open()) {
using (StreamReader reader = new StreamReader(stream, true)) {
encoding = reader.CurrentEncoding;
return reader.ReadToEnd();
}
}
});
ProcessRequestInternal(response, decryptedString, fileReader);
} catch(Exception e) {
if (resourceIdentifierPresent) {
logAction = logAction ?? AssemblyResourceLoader.LogWebResourceFailure;
logAction(decryptedString, e);
}
// MSRC 10405: There's no reason for this to return anything other than a 404 if something
// goes wrong. We shouldn't propagate the inner exception inside the YSOD, as it might
// contain sensitive cryptographic information.
Throw404();
}
}
private static void ProcessRequestInternal(
HttpResponseBase response,
string decryptedString,
VirtualFileReader fileReader) {
if (String.IsNullOrEmpty(decryptedString)) {
Throw404();
}
bool zip;
bool singleAssemblyReference;
// See GetScriptResourceUrl comment below for first character meanings.
switch (decryptedString[0]) {
case 'Z':
case 'z':
singleAssemblyReference = true;
zip = true;
break;
case 'U':
case 'u':
singleAssemblyReference = true;
zip = false;
break;
case 'Q':
case 'q':
singleAssemblyReference = false;
zip = true;
break;
case 'R':
case 'r':
singleAssemblyReference = false;
zip = false;
break;
case 'T':
OutputEmptyPage(response, decryptedString.Substring(1));
return;
default:
Throw404();
return;
}
decryptedString = decryptedString.Substring(1);
if (String.IsNullOrEmpty(decryptedString)) {
Throw404();
}
string[] decryptedData = decryptedString.Split('|');
if (singleAssemblyReference) {
// expected: <assembly>|<resource>|<culture>[|#|<hash>]
if (decryptedData.Length != 3 && decryptedData.Length != 5) {
// The decrypted data must have 3 parts plus an optional 2 part hash code separated by pipes.
Throw404();
}
}
else {
// expected: <assembly1>|<resource1a>,<culture1a>,<resource1b>,<culture1b>,...|<assembly2>|<resource2a>,<culture2a>,<resource2b>,<culture2b>,...|#|<hash>
if (decryptedData.Length % 2 != 0) {
// The decrypted data must have an even number of parts separated by pipes.
Throw404();
}
}
StringBuilder script = new StringBuilder();
string firstContentType = null;
if (singleAssemblyReference) {
// single assembly reference, format is
// <assembly>|<resource>|<culture>
string assemblyName = decryptedData[0];
string resourceName = decryptedData[1];
string cultureName = decryptedData[2];
Assembly assembly = GetAssembly(assemblyName);
if (assembly == null) {
Throw404();
}
script.Append(ScriptResourceAttribute.GetScriptFromWebResourceInternal(
assembly,
resourceName,
String.IsNullOrEmpty(cultureName) ? CultureInfo.InvariantCulture : new CultureInfo(cultureName),
zip,
out firstContentType
));
}
else {
// composite script reference, format is:
// <assembly1>|<resource1a>,<culture1a>,<resource1b>,<culture1b>,...|<assembly2>|<resource2a>,<culture2a>,<resource2b>,<culture2b>,...
// Assembly is empty for path based scripts, and their resource/culture list is <path1>,<path2>,...
// If an assembly starts with "#", the segment is ignored (expected that this includes a hash to ensure
// url uniqueness when resources are changed). Also, for forward compatibility '#' segments may contain
// other data.
bool needsNewline = false;
for (int i = 0; i < decryptedData.Length; i += 2) {
string assemblyName = decryptedData[i];
bool hasAssembly = !String.IsNullOrEmpty(assemblyName);
if (hasAssembly && assemblyName[0] == '#') {
// hash segments are ignored, it contains a hash code for url uniqueness
continue;
}
Debug.Assert(!String.IsNullOrEmpty(decryptedData[i + 1]));
string[] resourcesAndCultures = decryptedData[i + 1].Split(',');
if (resourcesAndCultures.Length == 0) {
Throw404();
}
Assembly assembly = hasAssembly ? GetAssembly(assemblyName) : null;
if (assembly == null) {
// The scripts are path-based
if (firstContentType == null) {
firstContentType = "text/javascript";
}
for (int j = 0; j < resourcesAndCultures.Length; j++) {
Encoding encoding;
// DevDiv Bugs 197242
// path will either be absolute, as in "/app/foo/bar.js" or app relative, as in "~/foo/bar.js"
// ToAbsolute() ensures it is in the form /app/foo/bar.js
// This conversion was not done when the url was created to conserve url length.
string path = _bypassVirtualPathResolution ?
resourcesAndCultures[j] :
VirtualPathUtility.ToAbsolute(resourcesAndCultures[j]);
string fileContents = fileReader(path, out encoding);
if (needsNewline) {
// Output an additional newline between resources but not for the last one
script.Append('\n');
}
needsNewline = true;
script.Append(fileContents);
}
}
else {
Debug.Assert(resourcesAndCultures.Length % 2 == 0, "The list of resource names and cultures must have an even number of parts separated by commas.");
for (int j = 0; j < resourcesAndCultures.Length; j += 2) {
try {
string contentType;
string resourceName = resourcesAndCultures[j];
string cultureName = resourcesAndCultures[j + 1];
if (needsNewline) {
// Output an additional newline between resources but not for the last one
script.Append('\n');
}
needsNewline = true;
script.Append(ScriptResourceAttribute.GetScriptFromWebResourceInternal(
assembly,
resourceName,
String.IsNullOrEmpty(cultureName) ? CultureInfo.InvariantCulture : new CultureInfo(cultureName),
zip,
out contentType
));
if (firstContentType == null) {
firstContentType = contentType;
}
}
catch (MissingManifestResourceException ex) {
throw Create404(ex);
}
catch (HttpException ex) {
throw Create404(ex);
}
}
}
}
}
if (ScriptingScriptResourceHandlerSection.ApplicationSettings.EnableCaching) {
PrepareResponseCache(response);
}
else {
PrepareResponseNoCache(response);
}
response.ContentType = firstContentType;
if (zip) {
using (MemoryStream zipped = new MemoryStream()) {
using (Stream outputStream = new GZipStream(zipped, CompressionMode.Compress)) {
// The choice of an encoding matters little here.
// Input streams being of potentially different encodings, UTF-8 is the better
// choice as it's the natural encoding for JavaScript.
using (StreamWriter writer = new StreamWriter(outputStream, Encoding.UTF8)) {
writer.Write(script.ToString());
}
}
byte[] zippedBytes = zipped.ToArray();
response.AddHeader("Content-encoding", "gzip");
response.OutputStream.Write(zippedBytes, 0, zippedBytes.Length);
}
}
else {
// Bug DevDiv #175061, we don't want to force any encoding here and let the default
// encoding apply no matter what the incoming scripts might have been encoded with.
response.Write(script.ToString());
}
}
internal static void SetScriptResourceHandler(IScriptResourceHandler scriptResourceHandler) {
_scriptResourceHandler = scriptResourceHandler;
}
private static void Throw404() {
throw Create404(null);
}
private static void Throw404(Exception innerException) {
throw Create404(innerException);
}
#region IHttpHandler implementation
void IHttpHandler.ProcessRequest(HttpContext context) {
ProcessRequest(context);
}
bool IHttpHandler.IsReusable {
get {
return IsReusable;
}
}
#endregion
private class RuntimeScriptResourceHandler : IScriptResourceHandler {
// Keys in the URL cache will be IList objects, so use our custom list comparer.
private static readonly IDictionary _urlCache = Hashtable.Synchronized(new Hashtable(ListEqualityComparer.Instance));
private static readonly IDictionary _cultureCache = Hashtable.Synchronized(new Hashtable());
private static string _absoluteScriptResourceUrl;
string IScriptResourceHandler.GetScriptResourceUrl(
Assembly assembly, string resourceName, CultureInfo culture, bool zip) {
return ((IScriptResourceHandler)this).GetScriptResourceUrl(
new List<Tuple<Assembly, List<Tuple<string, CultureInfo>>>>() {
new Tuple<Assembly, List<Tuple<string, CultureInfo>>>(
assembly,
new List<Tuple<string,CultureInfo>>() {
new Tuple<string, CultureInfo>(resourceName, culture)
}
)
}, zip);
}
string IScriptResourceHandler.GetScriptResourceUrl(
List<Tuple<Assembly, List<Tuple<string, CultureInfo>>>> assemblyResourceLists,
bool zip) {
if (!IsCompressionEnabled(HttpContext.Current)) {
zip = false;
}
bool allAssemblyResources = true;
foreach (Tuple<Assembly, List<Tuple<string, CultureInfo>>> assemblyData in assemblyResourceLists) {
if (assemblyData.Item1 == null) {
allAssemblyResources = false;
break;
}
}
// If all the scripts are assembly resources, we can cache the generated ScriptResource URL, since
// the appdomain will reset if any of the assemblies are changed. We cannot cache the URL if any
// scripts are path-based, since the cache entry will not be removed if a path-based script is changed.
if (allAssemblyResources) {
List<object> cacheKeys = new List<object>();
foreach (Tuple<Assembly, List<Tuple<string, CultureInfo>>> assemblyData in assemblyResourceLists) {
cacheKeys.Add(assemblyData.Item1);
foreach (Tuple<string, CultureInfo> resourceAndCulture in assemblyData.Item2) {
cacheKeys.Add(resourceAndCulture.Item1);
cacheKeys.Add(resourceAndCulture.Item2);
}
}
cacheKeys.Add(zip);
string url = (string)_urlCache[cacheKeys];
if (url == null) {
url = GetScriptResourceUrlImpl(assemblyResourceLists, zip);
_urlCache[cacheKeys] = url;
}
return url;
}
else {
return GetScriptResourceUrlImpl(assemblyResourceLists, zip);
}
}
[SecuritySafeCritical]
private static string GetScriptResourceUrlImpl(
List<Tuple<Assembly, List<Tuple<string, CultureInfo>>>> assemblyResourceLists,
bool zip) {
EnsureAbsoluteScriptResourceUrl();
// If there's only a single assembly resource, format is
// [Z|U|z|u]<assembly>|<resource>|<culture>
// If there are multiple resources, or a single resource that is path based, format is
// [Q|R|q|r]<assembly1>|<resource1a>,<culture1a>,<resource1b>,<culture1b>...|<assembly2>|<resource2a>,<culture2a>,<resource2b>,<culture2b>...
// A path based reference has no assembly (empty).
// (the Q/R indicators used in place of Z/U give the handler indiciation that the url is a composite
// reference, and allows for System.Web.Extensions SP1 to maintain compatibility with RTM, should a
// single resource be encrypted with SP1 and decrypted with RTM).
bool singleAssemblyResource = false;
if (assemblyResourceLists.Count == 1) {
// only one assembly to pull from...
var reference = assemblyResourceLists[0];
if ((reference.Item1 != null) && (reference.Item2.Count == 1)) {
// resource is assembly not path, and there's only one resource within it to load
singleAssemblyResource = true;
}
}
// Next character of the encoded string is:
// Format: S = Single Assembly Reference, C = Composite Reference or Single Path Reference
// Zip: compress or not (true or false)
// First Format Zip?
// =====================
// Z S T
// U S F
// Q C T
// R C F
string indicator;
if (singleAssemblyResource) {
indicator = (zip ? "Z" : "U");
}
else {
indicator = (zip ? "Q" : "R");
}
StringBuilder url = new StringBuilder(indicator);
HashCodeCombiner hashCombiner = new HashCodeCombiner();
bool firstAssembly = true;
foreach (Tuple<Assembly, List<Tuple<string, CultureInfo>>> assemblyData in assemblyResourceLists) {
if (!firstAssembly) {
url.Append('|');
}
else {
firstAssembly = false;
}
if (assemblyData.Item1 != null) {
Tuple<AssemblyName, String> assemblyInfo = GetAssemblyInfo(assemblyData.Item1);
AssemblyName assemblyName = (AssemblyName)assemblyInfo.Item1;
string assemblyHash = (String)assemblyInfo.Item2;
hashCombiner.AddObject(assemblyHash);
if (assemblyData.Item1.GlobalAssemblyCache) {
// If the assembly is in the GAC, we need to store a full name to load the assembly later
// Pack the necessary values into a more compact format than FullName
url.Append(assemblyName.Name);
url.Append(',');
url.Append(assemblyName.Version);
url.Append(',');
if (assemblyName.CultureInfo != null) {
url.Append(assemblyName.CultureInfo);
}
url.Append(',');
url.Append(HexParser.ToString(assemblyName.GetPublicKeyToken()));
}
else {
// Otherwise, we can just use a partial name
url.Append(assemblyName.Name);
}
}
url.Append('|');
bool firstResource = true;
foreach (Tuple<string, CultureInfo> resourceAndCulture in assemblyData.Item2) {
if (!firstResource) {
url.Append(',');
}
if (assemblyData.Item1 != null) {
url.Append(resourceAndCulture.Item1);
Tuple<Assembly, string, CultureInfo> cacheKey = Tuple.Create(
assemblyData.Item1,
resourceAndCulture.Item1,
resourceAndCulture.Item2
);
string cultureName = (string)_cultureCache[cacheKey];
if (cultureName == null) {
// Check if the resources exist
ScriptResourceInfo resourceInfo =
ScriptResourceInfo.GetInstance(assemblyData.Item1, resourceAndCulture.Item1);
if (resourceInfo == ScriptResourceInfo.Empty) {
ThrowUnknownResource(resourceAndCulture.Item1);
}
Stream scriptStream = assemblyData.Item1.GetManifestResourceStream(resourceInfo.ScriptName);
if (scriptStream == null) {
ThrowUnknownResource(resourceAndCulture.Item1);
}
cultureName = DetermineNearestAvailableCulture(
assemblyData.Item1, resourceAndCulture.Item1, resourceAndCulture.Item2).Name;
_cultureCache[cacheKey] = cultureName;
}
url.Append(singleAssemblyResource ? "|" : ",");
url.Append(cultureName);
}
else {
Debug.Assert(!singleAssemblyResource, "This should never happen since this is a path reference.");
if (!_bypassVirtualPathResolution) {
VirtualPathProvider vpp = HostingEnvironment.VirtualPathProvider;
if (!vpp.FileExists(resourceAndCulture.Item1)) {
ThrowUnknownResource(resourceAndCulture.Item1);
}
string hash = vpp.GetFileHash(resourceAndCulture.Item1, new string[] { resourceAndCulture.Item1 });
hashCombiner.AddObject(hash);
}
url.Append(resourceAndCulture.Item1);
}
firstResource = false;
}
}
// DevDiv Bugs 186624: The hash code needs to be part of the encrypted blob for composite scripts
// because we cache the composite script on the server using a VaryByParam["d"]. Otherwise, if a
// path based script in the composite changes, only the 't' parameter would change, which would
// cause a new request to the server, but it would be served via cache since 'd' would be the same.
// This isn't a problem for assembly based resources since changing them also restarts the app and
// clears the cache. We do not vary by 't' because that makes it possible to flood the server cache
// with cache entries, since anything could be used for 't'. Putting the hash in 'd' ensures a different
// url and different cache entry when a script changes, but without the possibility of flooding
// the server cache.
// However, we continue to use the 't' parameter for single assembly references for compatibility.
string resourceUrl;
if (singleAssemblyResource) {
resourceUrl = _absoluteScriptResourceUrl +
Page.EncryptString(url.ToString(), Purpose.ScriptResourceHandler_ScriptResourceUrl) +
"&t=" + hashCombiner.CombinedHashString;
}
else {
// note that CombinedHashString is hex, it will never include a '|' that would confuse the handler.
url.Append("|#|");
url.Append(hashCombiner.CombinedHashString);
resourceUrl = _absoluteScriptResourceUrl +
Page.EncryptString(url.ToString(), Purpose.ScriptResourceHandler_ScriptResourceUrl);
}
if (resourceUrl.Length > _maximumResourceUrlLength) {
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, AtlasWeb.ScriptResourceHandler_ResourceUrlTooLong, _maximumResourceUrlLength));
}
return resourceUrl;
}
private static void EnsureAbsoluteScriptResourceUrl() {
if (_absoluteScriptResourceUrl == null) {
_absoluteScriptResourceUrl = _bypassVirtualPathResolution ?
_scriptResourceUrl + "?d=" :
VirtualPathUtility.ToAbsolute(_scriptResourceUrl) + "?d=";
}
}
string IScriptResourceHandler.GetEmptyPageUrl(string title) {
EnsureAbsoluteScriptResourceUrl();
return _absoluteScriptResourceUrl +
Page.EncryptString('T' + title, Purpose.ScriptResourceHandler_ScriptResourceUrl);
}
private static void ThrowUnknownResource(string resourceName) {
throw new HttpException(String.Format(CultureInfo.CurrentCulture,
AtlasWeb.ScriptResourceHandler_UnknownResource, resourceName));
}
}
}
}
|