//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ namespace System.Web.UI { using System; using System.Collections; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using System.Web; using System.Web.Handlers; using System.Web.Resources; using System.Web.Util; using Debug = System.Diagnostics.Debug; [ DefaultProperty("Path"), ] public class ScriptReference : ScriptReferenceBase { // Maps Tuple(resource name, assembly) to string (partial script path) private static readonly Hashtable _scriptPathCache = Hashtable.Synchronized(new Hashtable()); private string _assembly; private bool _ignoreScriptPath; private string _name; private ScriptEffectiveInfo _scriptInfo; public ScriptReference() : base() { } public ScriptReference(string name, string assembly) : this() { Name = name; Assembly = assembly; } public ScriptReference(string path) : this() { Path = path; } internal ScriptReference(string name, IClientUrlResolver clientUrlResolver, Control containingControl) : this() { Debug.Assert(!String.IsNullOrEmpty(name), "The script's name must be specified."); Debug.Assert(clientUrlResolver != null && clientUrlResolver is ScriptManager, "The clientUrlResolver must be the ScriptManager."); Name = name; ClientUrlResolver = clientUrlResolver; IsStaticReference = true; ContainingControl = containingControl; } internal bool IsDirectRegistration { // set to true internally to disable checking for adding .debug // used when registering a script directly through SM.RegisterClientScriptResource get; set; } [ Category("Behavior"), DefaultValue(""), ResourceDescription("ScriptReference_Assembly") ] public string Assembly { get { return (_assembly == null) ? String.Empty : _assembly; } set { _assembly = value; _scriptInfo = null; } } internal Assembly EffectiveAssembly { get { return ScriptInfo.Assembly; } } internal string EffectivePath { get { return String.IsNullOrEmpty(Path) ? ScriptInfo.Path : Path; } } internal string EffectiveResourceName { get { return ScriptInfo.ResourceName; } } internal ScriptMode EffectiveScriptMode { get { if (ScriptMode == ScriptMode.Auto) { // - When a mapping.DebugPath exists, ScriptMode.Auto is equivilent to ScriptMode.Inherit, // since a debug path exists, even though it may not be an assembly based script. // - An explicitly set Path on the ScriptReference effectively ignores a DebugPath. // - When only Path is specified, ScriptMode.Auto is equivalent to ScriptMode.Release. // - When only Name is specified, ScriptMode.Auto is equivalent to ScriptMode.Inherit. // - When Name and Path are both specified, the Path is used instead of the Name, but // ScriptMode.Auto is still equivalent to ScriptMode.Inherit, since the assumption // is that if the Assembly contains both release and debug scripts, the Path should // contain both as well. return ((String.IsNullOrEmpty(EffectiveResourceName) && (!String.IsNullOrEmpty(Path) || String.IsNullOrEmpty(ScriptInfo.DebugPath))) ? ScriptMode.Release : ScriptMode.Inherit); } else { return ScriptMode; } } } [ Category("Behavior"), DefaultValue(false), ResourceDescription("ScriptReference_IgnoreScriptPath"), Obsolete("This property is obsolete. Instead of using ScriptManager.ScriptPath, set the Path property on each individual ScriptReference.") ] public bool IgnoreScriptPath { get { return _ignoreScriptPath; } set { _ignoreScriptPath = value; } } [ Category("Behavior"), DefaultValue(""), ResourceDescription("ScriptReference_Name") ] public string Name { get { return (_name == null) ? String.Empty : _name; } set { _name = value; _scriptInfo = null; } } internal ScriptEffectiveInfo ScriptInfo { get { if (_scriptInfo == null) { _scriptInfo = new ScriptEffectiveInfo(this); } return _scriptInfo; } } private string AddCultureName(ScriptManager scriptManager, string resourceName) { Debug.Assert(!String.IsNullOrEmpty(resourceName)); CultureInfo culture = (scriptManager.EnableScriptLocalization ? DetermineCulture(scriptManager) : CultureInfo.InvariantCulture); if (!culture.Equals(CultureInfo.InvariantCulture)) { return AddCultureName(culture, resourceName); } else { return resourceName; } } private static string AddCultureName(CultureInfo culture, string resourceName) { if (resourceName.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) { resourceName = resourceName.Substring(0, resourceName.Length - 2) + culture.Name + ".js"; } return resourceName; } internal bool DetermineResourceNameAndAssembly(ScriptManager scriptManager, bool isDebuggingEnabled, ref string resourceName, ref Assembly assembly) { // If the assembly is the AjaxFrameworkAssembly, the resource may come from that assembly // or from the fallback assembly (SWE). if (assembly == scriptManager.AjaxFrameworkAssembly) { assembly = ApplyFallbackResource(assembly, resourceName); } // ShouldUseDebugScript throws exception if the resource name does not exist in the assembly bool isDebug = ShouldUseDebugScript(resourceName, assembly, isDebuggingEnabled, scriptManager.AjaxFrameworkAssembly); if (isDebug) { resourceName = GetDebugName(resourceName); } // returning true means the debug version is selected return isDebug; } internal CultureInfo DetermineCulture(ScriptManager scriptManager) { if ((ResourceUICultures == null) || (ResourceUICultures.Length == 0)) { // In this case we want to determine available cultures from assembly info if available if (!String.IsNullOrEmpty(EffectiveResourceName)) { return ScriptResourceHandler .DetermineNearestAvailableCulture(GetAssembly(scriptManager), EffectiveResourceName, CultureInfo.CurrentUICulture); } return CultureInfo.InvariantCulture; } CultureInfo currentCulture = CultureInfo.CurrentUICulture; while (!currentCulture.Equals(CultureInfo.InvariantCulture)) { string cultureName = currentCulture.ToString(); foreach (string uiCulture in ResourceUICultures) { if (String.Equals(cultureName, uiCulture.Trim(), StringComparison.OrdinalIgnoreCase)) { return currentCulture; } } currentCulture = currentCulture.Parent; } return currentCulture; } internal Assembly GetAssembly() { return String.IsNullOrEmpty(Assembly) ? null : AssemblyCache.Load(Assembly); } internal Assembly GetAssembly(ScriptManager scriptManager) { // normalizes the effective assembly by redirecting it to the given scriptmanager's // ajax framework assembly when it is set to SWE. // EffectiveAssembly can't do this since ScriptReference does not have access by itself // to the script manager. Debug.Assert(scriptManager != null); Assembly assembly = EffectiveAssembly; if (assembly == null) { return scriptManager.AjaxFrameworkAssembly; } else { return ((assembly == AssemblyCache.SystemWebExtensions) ? scriptManager.AjaxFrameworkAssembly : assembly); } } // Release: foo.js // Debug: foo.debug.js private static string GetDebugName(string releaseName) { // Since System.Web.Handlers.AssemblyResourceLoader treats the resource name as case-sensitive, // we must do the same when verifying the extension. // Ignore trailing whitespace. For example, "MicrosoftAjax.js " is valid (at least from // a debug/release naming perspective). if (!releaseName.EndsWith(".js", StringComparison.Ordinal)) { throw new InvalidOperationException( String.Format(CultureInfo.CurrentUICulture, AtlasWeb.ScriptReference_InvalidReleaseScriptName, releaseName)); } return ReplaceExtension(releaseName); } internal string GetPath(ScriptManager scriptManager, string releasePath, string predeterminedDebugPath, bool isDebuggingEnabled) { // convert the release path to a debug path if: // isDebuggingEnabled && not resource based // isDebuggingEnabled && resource based && debug resource exists // ShouldUseDebugScript is called even when isDebuggingEnabled=false as it verfies // the existence of the resource. // applies the culture name to the path if appropriate string path; if (!String.IsNullOrEmpty(EffectiveResourceName)) { Assembly assembly = GetAssembly(scriptManager); string resourceName = EffectiveResourceName; isDebuggingEnabled = DetermineResourceNameAndAssembly(scriptManager, isDebuggingEnabled, ref resourceName, ref assembly); } if (isDebuggingEnabled) { // Just use predeterminedDebugPath if it is provided. This may be because // a script mapping has DebugPath set. If it is empty or null, then '.debug' is added // to the .js extension of the release path. path = String.IsNullOrEmpty(predeterminedDebugPath) ? GetDebugPath(releasePath) : predeterminedDebugPath; } else { path = releasePath; } return AddCultureName(scriptManager, path); } internal Assembly ApplyFallbackResource(Assembly assembly, string releaseName) { // fall back to SWE if the assembly does not contain the requested resource if ((assembly != AssemblyCache.SystemWebExtensions) && !WebResourceUtil.AssemblyContainsWebResource(assembly, releaseName)) { assembly = AssemblyCache.SystemWebExtensions; } return assembly; } // Format: /// // This function does not canonicalize the path in any way (i.e. remove duplicate slashes). // You must call ResolveClientUrl() on this path before rendering to the page. internal static string GetScriptPath( string resourceName, Assembly assembly, CultureInfo culture, string scriptPath) { return scriptPath + "/" + GetScriptPathCached(resourceName, assembly, culture); } // Cache partial script path, since Version.ToString() and HttpUtility.UrlEncode() are expensive. // Increases requests/second by 50% in ScriptManagerScriptPath.aspx test. private static string GetScriptPathCached(string resourceName, Assembly assembly, CultureInfo culture) { Tuple key = Tuple.Create(resourceName, assembly, culture); string scriptPath = (string)_scriptPathCache[key]; if (scriptPath == null) { // Need to use "new AssemblyName(assembly.FullName)" instead of "assembly.GetName()", // since Assembly.GetName() requires FileIOPermission to the path of the assembly. // In partial trust, we may not have this permission. AssemblyName assemblyName = new AssemblyName(assembly.FullName); string name = assemblyName.Name; string version = assemblyName.Version.ToString(); string fileVersion = AssemblyUtil.GetAssemblyFileVersion(assembly); if (!culture.Equals(CultureInfo.InvariantCulture)) { resourceName = AddCultureName(culture, resourceName); } // Assembly name, fileVersion, and resource name may contain invalid URL characters (like '#' or '/'), // so they must be url-encoded. scriptPath = String.Join("/", new string[] { HttpUtility.UrlEncode(name), version, HttpUtility.UrlEncode(fileVersion), HttpUtility.UrlEncode(resourceName) }); _scriptPathCache[key] = scriptPath; } return scriptPath; } [SuppressMessage("Microsoft.Design", "CA1055", Justification = "Consistent with other URL properties in ASP.NET.")] protected internal override string GetUrl(ScriptManager scriptManager, bool zip) { bool hasName = !String.IsNullOrEmpty(Name); bool hasAssembly = !String.IsNullOrEmpty(Assembly); if (!hasName && String.IsNullOrEmpty(Path)) { throw new InvalidOperationException(AtlasWeb.ScriptReference_NameAndPathCannotBeEmpty); } if (hasAssembly && !hasName) { throw new InvalidOperationException(AtlasWeb.ScriptReference_AssemblyRequiresName); } return GetUrlInternal(scriptManager, zip); } internal string GetUrlInternal(ScriptManager scriptManager, bool zip) { bool enableCdn = scriptManager != null && scriptManager.EnableCdn; return GetUrlInternal(scriptManager, zip, useCdnPath: enableCdn); } internal string GetUrlInternal(ScriptManager scriptManager, bool zip, bool useCdnPath) { if (!String.IsNullOrEmpty(EffectiveResourceName) && !IsAjaxFrameworkScript(scriptManager) && AssemblyCache.IsAjaxFrameworkAssembly(GetAssembly(scriptManager))) { // it isnt an AjaxFrameworkScript but it might be from an assembly that is meant to // be an ajax script assembly, in which case we should throw an error. throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, AtlasWeb.ScriptReference_ResourceRequiresAjaxAssembly, EffectiveResourceName, GetAssembly(scriptManager))); } if (!String.IsNullOrEmpty(Path)) { // if an explicit path is set on the SR (not on a mapping) it always // takes precedence, even when EnableCdn=true. // Also, even if a script mapping has a DebugPath, the explicitly set path // overrides all -- so path.debug.js is used instead of the mapping's DebugPath, // hence the null 3rd parameter. return GetUrlFromPath(scriptManager, Path, null); } else if (!String.IsNullOrEmpty(ScriptInfo.Path)) { // when only the mapping has a path, CDN takes first priority if (useCdnPath) { // first determine the actual resource name and assembly to be used // This is so we can (1) apply fallback logic, where ajax fx scripts can come from the // current ajax assembly or from SWE, whichever is first, and (2) the .debug resource // name is applied if appropriate. string resourceName = EffectiveResourceName; Assembly assembly = null; bool hasDebugResource = false; if (!String.IsNullOrEmpty(resourceName)) { assembly = GetAssembly(scriptManager); hasDebugResource = DetermineResourceNameAndAssembly(scriptManager, IsDebuggingEnabled(scriptManager), ref resourceName, ref assembly); } string cdnPath = GetUrlForCdn(scriptManager, resourceName, assembly, hasDebugResource); if (!String.IsNullOrEmpty(cdnPath)) { return cdnPath; } } // the mapping's DebugPath applies if it exists return GetUrlFromPath(scriptManager, ScriptInfo.Path, ScriptInfo.DebugPath); } Debug.Assert(!String.IsNullOrEmpty(EffectiveResourceName)); return GetUrlFromName(scriptManager, scriptManager.Control, zip, useCdnPath); } private string GetUrlForCdn(ScriptManager scriptManager, string resourceName, Assembly assembly, bool hasDebugResource) { // if EnableCdn, then url always comes from mapping.Cdn[Debug]Path or WRA.CdnPath, if available. // first see if the script description mapping has a cdn path defined bool isDebuggingEnabled = IsDebuggingEnabled(scriptManager); bool isAssemblyResource = !String.IsNullOrEmpty(resourceName); bool secureConnection = scriptManager.IsSecureConnection; isDebuggingEnabled = isDebuggingEnabled && (hasDebugResource || !isAssemblyResource); string cdnPath = isDebuggingEnabled ? (secureConnection ? ScriptInfo.CdnDebugPathSecureConnection : ScriptInfo.CdnDebugPath) : (secureConnection ? ScriptInfo.CdnPathSecureConnection : ScriptInfo.CdnPath); // then see if the WebResourceAttribute for the resource has one // EXCEPT when the ScriptInfo has a cdnpath but it wasn't selected due to this being a secure connection // and it does not support secure connections. Avoid having the HTTP cdn path come from the mapping and the // HTTPS path come from the WRA. if (isAssemblyResource && String.IsNullOrEmpty(cdnPath) && String.IsNullOrEmpty(isDebuggingEnabled ? ScriptInfo.CdnDebugPath : ScriptInfo.CdnPath)) { ScriptResourceInfo scriptResourceInfo = ScriptResourceInfo.GetInstance(assembly, resourceName); if (scriptResourceInfo != null) { cdnPath = secureConnection ? scriptResourceInfo.CdnPathSecureConnection : scriptResourceInfo.CdnPath; } } return String.IsNullOrEmpty(cdnPath) ? null : ClientUrlResolver.ResolveClientUrl(AddCultureName(scriptManager, cdnPath)); } private string GetUrlFromName(ScriptManager scriptManager, IControl scriptManagerControl, bool zip, bool useCdnPath) { string resourceName = EffectiveResourceName; Assembly assembly = GetAssembly(scriptManager); bool hasDebugResource = DetermineResourceNameAndAssembly(scriptManager, IsDebuggingEnabled(scriptManager), ref resourceName, ref assembly); if (useCdnPath) { string cdnPath = GetUrlForCdn(scriptManager, resourceName, assembly, hasDebugResource); if (!String.IsNullOrEmpty(cdnPath)) { return cdnPath; } } CultureInfo culture = (scriptManager.EnableScriptLocalization ? DetermineCulture(scriptManager) : CultureInfo.InvariantCulture); #pragma warning disable 618 // ScriptPath is obsolete but still functional if (IgnoreScriptPath || String.IsNullOrEmpty(scriptManager.ScriptPath)) { return ScriptResourceHandler.GetScriptResourceUrl(assembly, resourceName, culture, zip); } else { string path = GetScriptPath(resourceName, assembly, culture, scriptManager.ScriptPath); if (IsBundleReference) { return scriptManager.BundleReflectionHelper.GetBundleUrl(path); } // Always want to resolve ScriptPath urls against the ScriptManager itself, // regardless of whether the ScriptReference was declared on the ScriptManager // or a ScriptManagerProxy. return scriptManagerControl.ResolveClientUrl(path); } #pragma warning restore 618 } private string GetUrlFromPath(ScriptManager scriptManager, string releasePath, string predeterminedDebugPath) { string path = GetPath(scriptManager, releasePath, predeterminedDebugPath, IsDebuggingEnabled(scriptManager)); if (IsBundleReference) { return scriptManager.BundleReflectionHelper.GetBundleUrl(path); } return ClientUrlResolver.ResolveClientUrl(path); } private bool IsDebuggingEnabled(ScriptManager scriptManager) { // Deployment mode retail overrides all values of ScriptReference.ScriptMode. if (IsDirectRegistration || scriptManager.DeploymentSectionRetail) { return false; } switch (EffectiveScriptMode) { case ScriptMode.Inherit: return scriptManager.IsDebuggingEnabled; case ScriptMode.Debug: return true; case ScriptMode.Release: return false; default: Debug.Fail("Invalid value for ScriptReference.EffectiveScriptMode"); return false; } } protected internal override bool IsAjaxFrameworkScript(ScriptManager scriptManager) { return (GetAssembly(scriptManager) == scriptManager.AjaxFrameworkAssembly); } [Obsolete("This method is obsolete. Use IsAjaxFrameworkScript(ScriptManager) instead.")] protected internal override bool IsFromSystemWebExtensions() { return (EffectiveAssembly == AssemblyCache.SystemWebExtensions); } internal bool IsFromSystemWeb() { return (EffectiveAssembly == AssemblyCache.SystemWeb); } internal bool ShouldUseDebugScript(string releaseName, Assembly assembly, bool isDebuggingEnabled, Assembly currentAjaxAssembly) { bool useDebugScript; string debugName = null; if (isDebuggingEnabled) { debugName = GetDebugName(releaseName); // If an assembly contains a release script but not a corresponding debug script, and we // need to register the debug script, we normally throw an exception. However, we automatically // use the release script if ScriptReference.ScriptMode is Auto. This improves the developer // experience when ScriptMode is Auto, yet still gives the developer full control with the // other ScriptModes. if (ScriptMode == ScriptMode.Auto && !WebResourceUtil.AssemblyContainsWebResource(assembly, debugName)) { useDebugScript = false; } else { useDebugScript = true; } } else { useDebugScript = false; } // Verify that assembly contains required web resources. Always check for release // script before debug script. if (!IsDirectRegistration) { // Don't check if direct registration, because calls to ScriptManager.RegisterClientScriptResource // with resources that do not exist does not throw an exception until the resource is served. // This was the existing behavior and matches the behavior with ClientScriptManager.GetWebResourceUrl. WebResourceUtil.VerifyAssemblyContainsReleaseWebResource(assembly, releaseName, currentAjaxAssembly); if (useDebugScript) { Debug.Assert(debugName != null); WebResourceUtil.VerifyAssemblyContainsDebugWebResource(assembly, debugName); } } return useDebugScript; } // Improves the UI in the VS collection editor, by displaying the Name or Path (if available), or // the short type name. [SuppressMessage("Microsoft.Security", "CA2123:OverrideLinkDemandsShouldBeIdenticalToBase")] public override string ToString() { if (!String.IsNullOrEmpty(Name)) { return Name; } else if (!String.IsNullOrEmpty(Path)) { return Path; } else { return GetType().Name; } } internal class ScriptEffectiveInfo { private string _resourceName; private Assembly _assembly; private string _path; private string _debugPath; private string _cdnPath; private string _cdnDebugPath; private string _cdnPathSecureConnection; private string _cdnDebugPathSecureConnection; public ScriptEffectiveInfo(ScriptReference scriptReference) { ScriptResourceDefinition definition = ScriptManager.ScriptResourceMapping.GetDefinition(scriptReference); string name = scriptReference.Name; string path = scriptReference.Path; Assembly assembly = scriptReference.GetAssembly(); if (definition != null) { if (String.IsNullOrEmpty(path)) { // only when the SR has no path, the mapping's path and debug path, if any, apply path = definition.Path; _debugPath = definition.DebugPath; } name = definition.ResourceName; assembly = definition.ResourceAssembly; _cdnPath = definition.CdnPath; _cdnDebugPath = definition.CdnDebugPath; _cdnPathSecureConnection = definition.CdnPathSecureConnection; _cdnDebugPathSecureConnection = definition.CdnDebugPathSecureConnection; LoadSuccessExpression = definition.LoadSuccessExpression; } else if ((assembly == null) && !String.IsNullOrEmpty(name)) { // name is set and there is no mapping, default to SWE for assembly assembly = AssemblyCache.SystemWebExtensions; } _resourceName = name; _assembly = assembly; _path = path; if (assembly != null && !String.IsNullOrEmpty(name) && String.IsNullOrEmpty(LoadSuccessExpression)) { var scriptResourceInfo = ScriptResourceInfo.GetInstance(assembly, name); if (scriptResourceInfo != null) { LoadSuccessExpression = scriptResourceInfo.LoadSuccessExpression; } } } public Assembly Assembly { get { return _assembly; } } public string CdnDebugPath { get { return _cdnDebugPath; } } public string CdnPath { get { return _cdnPath; } } public string CdnDebugPathSecureConnection { get { return _cdnDebugPathSecureConnection; } } public string CdnPathSecureConnection { get { return _cdnPathSecureConnection; } } public string LoadSuccessExpression { get; private set; } public string DebugPath { get { return _debugPath; } } public string Path { get { return _path; } } public string ResourceName { get { return _resourceName; } } } } }