//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ namespace System.Web.UI.WebControls { using System; using System.Collections; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Design; using System.Globalization; using System.IO; using System.Text; using System.Web; using System.Web.Caching; using System.Web.Hosting; using System.Web.UI; using System.Web.Util; using System.Xml; using System.Xml.Xsl; /// /// Represents an XML file as both an IDataSource and an IHierarchicalDataSource. /// The XML data is retrieved either from a file specified by the DataFile property /// or by inline XML content in the Data property. /// [ DefaultEvent("Transforming"), DefaultProperty("DataFile"), Designer("System.Web.UI.Design.WebControls.XmlDataSourceDesigner, " + AssemblyRef.SystemDesign), ParseChildren(true), PersistChildren(false), ToolboxBitmap(typeof(XmlDataSource)), WebSysDescription(SR.XmlDataSource_Description), WebSysDisplayName(SR.XmlDataSource_DisplayName) ] public class XmlDataSource : HierarchicalDataSourceControl, IDataSource, IListSource { private static readonly object EventTransforming = new object(); private const string DefaultViewName = "DefaultView"; private DataSourceCache _cache; private bool _cacheLookupDone; private bool _disallowChanges; private XsltArgumentList _transformArgumentList; private ICollection _viewNames; private XmlDocument _xmlDocument; private string _writeableDataFile; private string _data; private string _dataFile; private string _transform; private string _transformFile; private string _xPath; /// /// Specifies the cache settings for this data source. /// private DataSourceCache Cache { get { if (_cache == null) { _cache = new DataSourceCache(); _cache.Enabled = true; } return _cache; } } /// /// The duration, in seconds, of the expiration. The expiration policy is specified by the CacheExpirationPolicy property. /// [ DefaultValue(DataSourceCache.Infinite), TypeConverterAttribute(typeof(DataSourceCacheDurationConverter)), WebCategory("Cache"), WebSysDescription(SR.DataSourceCache_Duration), ] public virtual int CacheDuration { get { return Cache.Duration; } set { Cache.Duration = value; } } /// /// The expiration policy of the cache. The duration for the expiration is specified by the CacheDuration property. /// [ DefaultValue(DataSourceCacheExpiry.Absolute), WebCategory("Cache"), WebSysDescription(SR.DataSourceCache_ExpirationPolicy), ] public virtual DataSourceCacheExpiry CacheExpirationPolicy { get { return Cache.ExpirationPolicy; } set { Cache.ExpirationPolicy = value; } } /// /// Indicates an arbitrary cache key to make this cache entry depend on. This allows /// the user to further customize when this cache entry will expire. /// [ DefaultValue(""), WebCategory("Cache"), WebSysDescription(SR.DataSourceCache_KeyDependency), ] public virtual string CacheKeyDependency { get { return Cache.KeyDependency; } set { Cache.KeyDependency = value; } } [ DefaultValue(""), WebCategory("Cache"), WebSysDescription(SR.XmlDataSource_CacheKeyContext), ] public virtual string CacheKeyContext { get { return (string)ViewState["CacheKeyContext "] ?? String.Empty; } set { ViewState["CacheKeyContext "] = value; } } /// /// Inline XML content. /// [ DefaultValue(""), Editor("System.ComponentModel.Design.MultilineStringEditor," + AssemblyRef.SystemDesign, typeof(UITypeEditor)), PersistenceMode(PersistenceMode.InnerProperty), TypeConverter("System.ComponentModel.MultilineStringConverter," + AssemblyRef.System), WebCategory("Data"), WebSysDescription(SR.XmlDataSource_Data), ] public virtual string Data { get { if (_data == null) { return String.Empty; } return _data; } set { if (value != null) { value = value.Trim(); } if (Data != value) { if (_disallowChanges) { throw new InvalidOperationException(SR.GetString(SR.XmlDataSource_CannotChangeWhileLoading, "Data", ID)); } _data = value; _xmlDocument = null; OnDataSourceChanged(EventArgs.Empty); } } } /// /// Path to an XML file. /// [ DefaultValue(""), Editor("System.Web.UI.Design.XmlDataFileEditor, " + AssemblyRef.SystemDesign, typeof(UITypeEditor)), WebCategory("Data"), WebSysDescription(SR.XmlDataSource_DataFile), ] public virtual string DataFile { get { if (_dataFile == null) { return String.Empty; } return _dataFile; } set { if (DataFile != value) { if (_disallowChanges) { throw new InvalidOperationException(SR.GetString(SR.XmlDataSource_CannotChangeWhileLoading, "DataFile", ID)); } _dataFile = value; _xmlDocument = null; _writeableDataFile = null; OnDataSourceChanged(EventArgs.Empty); } } } /// /// Whether caching is enabled for this data source. /// [ DefaultValue(true), WebCategory("Cache"), WebSysDescription(SR.DataSourceCache_Enabled), ] public virtual bool EnableCaching { get { return Cache.Enabled; } set { Cache.Enabled = value; } } /// /// Indicates whether the XML data can be modified. /// This is also used by XmlDataSourceView to determine whether CanDelete/Insert/Update are true. /// internal bool IsModifiable { get { return (String.IsNullOrEmpty(TransformFile) && String.IsNullOrEmpty(Transform) && !String.IsNullOrEmpty(WriteableDataFile)); } } /// /// Inline XSL transform. /// [ DefaultValue(""), Editor("System.ComponentModel.Design.MultilineStringEditor," + AssemblyRef.SystemDesign, typeof(UITypeEditor)), PersistenceMode(PersistenceMode.InnerProperty), TypeConverter("System.ComponentModel.MultilineStringConverter," + AssemblyRef.System), WebCategory("Data"), WebSysDescription(SR.XmlDataSource_Transform), ] public virtual string Transform { get { if (_transform == null) { return String.Empty; } return _transform; } set { if (value != null) { value = value.Trim(); } if (Transform != value) { if (_disallowChanges) { throw new InvalidOperationException(SR.GetString(SR.XmlDataSource_CannotChangeWhileLoading, "Transform", ID)); } _transform = value; _xmlDocument = null; OnDataSourceChanged(EventArgs.Empty); } } } /// /// Arguments for the XSL transform. /// This should be populated in the Transforming event. /// [ Browsable(false), ] public virtual XsltArgumentList TransformArgumentList { get { return _transformArgumentList; } set { _transformArgumentList = value; } } /// /// Path to an XSL transform file. /// [ DefaultValue(""), Editor("System.Web.UI.Design.XslTransformFileEditor, " + AssemblyRef.SystemDesign, typeof(UITypeEditor)), WebCategory("Data"), WebSysDescription(SR.XmlDataSource_TransformFile), ] public virtual string TransformFile { get { if (_transformFile == null) { return String.Empty; } return _transformFile; } set { if (TransformFile != value) { if (_disallowChanges) { throw new InvalidOperationException(SR.GetString(SR.XmlDataSource_CannotChangeWhileLoading, "TransformFile", ID)); } _transformFile = value; _xmlDocument = null; OnDataSourceChanged(EventArgs.Empty); } } } /// /// Gets a physical path of the data file that can be written to. /// The value is null if the path is not a writable path. /// private string WriteableDataFile { get { if (_writeableDataFile == null) { _writeableDataFile = GetWriteableDataFile(); } return _writeableDataFile; } } /// /// Specifies an initial XPath that is applied to the XML data. /// [ DefaultValue(""), WebCategory("Data"), WebSysDescription(SR.XmlDataSource_XPath), ] public virtual string XPath { get { if (_xPath == null) { return String.Empty; } return _xPath; } set { if (XPath != value) { if (_disallowChanges) { throw new InvalidOperationException(SR.GetString(SR.XmlDataSource_CannotChangeWhileLoading, "XPath", ID)); } _xPath = value; OnDataSourceChanged(EventArgs.Empty); } } } /// /// Raised before the XSL transform is applied. /// [ WebCategory("Data"), WebSysDescription(SR.XmlDataSource_Transforming), ] public event EventHandler Transforming { add { Events.AddHandler(EventTransforming, value); } remove { Events.RemoveHandler(EventTransforming, value); } } /// /// Creates a unique cache key for this data source's data. /// // Made internal for unit testing [SuppressMessage("Microsoft.Usage", "CA2303:FlagTypeGetHashCode", Justification = "This is specifically on XmlDataSource type which is not a com interop type.")] internal string CreateCacheKey() { StringBuilder sb = new StringBuilder(CacheInternal.PrefixDataSourceControl, 1024); sb.Append(GetType().GetHashCode().ToString(CultureInfo.InvariantCulture)); sb.Append(CacheDuration.ToString(CultureInfo.InvariantCulture)); sb.Append(':'); sb.Append(((int)CacheExpirationPolicy).ToString(CultureInfo.InvariantCulture)); bool includeUniqueID = false; if (!String.IsNullOrEmpty(CacheKeyContext)) { sb.Append(':'); sb.Append(CacheKeyContext); } if (DataFile.Length > 0) { sb.Append(':'); sb.Append(DataFile); } else { if (Data.Length > 0) { includeUniqueID = true; } } if (TransformFile.Length > 0) { sb.Append(':'); sb.Append(TransformFile); } else { if (Transform.Length > 0) { includeUniqueID = true; } } if (includeUniqueID) { // If we don't have any paths, use the Page if (Page != null) { sb.Append(':'); sb.Append(Page.GetType().AssemblyQualifiedName); } sb.Append(':'); string uniqueID = UniqueID; if (String.IsNullOrEmpty(uniqueID)) { throw new InvalidOperationException(SR.GetString(SR.XmlDataSource_NeedUniqueIDForCache)); } sb.Append(uniqueID); } return sb.ToString(); } /// /// Returns a HierarchicalDataSourceView based on an XPath specified by viewPath. /// protected override HierarchicalDataSourceView GetHierarchicalView(string viewPath) { return new XmlHierarchicalDataSourceView(this, viewPath); } /// /// Gets an XmlReader representing XML or XSL content, and optionally a cache /// dependency for that content. /// Supported paths are: Relative paths, physical paths, UNC paths, and HTTP URLs /// If a path is not provided, the content parameter is assumed to contain the /// actual content. /// If there is no data, null is returned. /// This method is fully compatible with Virtual Path Providers. /// private XmlReader GetReader(string path, string content, out CacheDependency cacheDependency) { // If a filename is specified, load from file. Otherwise load from inner content. if (path.Length != 0) { // First try to detect if it is an HTTP URL Uri uri; bool success = Uri.TryCreate(path, UriKind.Absolute, out uri); if (success) { if (uri.Scheme == Uri.UriSchemeHttp) { // Check for Web permissions for the URL we want if (!HttpRuntime.HasWebPermission(uri)) { throw new InvalidOperationException(SR.GetString(SR.XmlDataSource_NoWebPermission, uri.PathAndQuery, ID)); } // Dependencies are not supported with HTTP URLs cacheDependency = null; // If it is an HTTP URL and we have permissions, get a reader return XmlUtils.CreateXmlReader(path); } } // Now see what kind of file-based path it is VirtualPath virtualPath; string physicalPath; ResolvePhysicalOrVirtualPath(path, out virtualPath, out physicalPath); if (virtualPath != null && DesignMode) { // This exception should never be thrown - the designer always maps paths // before using the runtime control. throw new NotSupportedException(SR.GetString(SR.XmlDataSource_DesignTimeRelativePathsNotSupported, ID)); } Stream dataStream = OpenFileAndGetDependency(virtualPath, physicalPath, out cacheDependency); return XmlUtils.CreateXmlReader(dataStream); } else { // Dependencies are not supported with inline content cacheDependency = null; content = content.Trim(); if (content.Length == 0) { return null; } else { return XmlUtils.CreateXmlReader(new StringReader(content)); } } } /// /// Gets a path to a writeable file where we can save data to. /// The return value is null if a writeable path cannot be found. /// private string GetWriteableDataFile() { if (DataFile.Length != 0) { // First try to detect if it is an HTTP URL Uri uri; bool success = Uri.TryCreate(DataFile, UriKind.Absolute, out uri); if (success) { if (uri.Scheme == Uri.UriSchemeHttp) { // Cannot write to HTTP URLs return null; } } if (HostingEnvironment.UsingMapPathBasedVirtualPathProvider) { // Now see what kind of file-based path it is VirtualPath virtualPath; string physicalPath; ResolvePhysicalOrVirtualPath(DataFile, out virtualPath, out physicalPath); if (physicalPath == null) { physicalPath = virtualPath.MapPathInternal(this.TemplateControlVirtualDirectory, true /*allowCrossAppMapping*/); } return physicalPath; } else { // File is coming from a custom virtual path provider, and there is no support for writing return null; } } else { // Data is specified using Data property, so it is not writeable return null; } } /// /// Returns the XmlDocument representing the XML data. /// If necessary, the XML data will be reloaded along with the transform, if available. /// public XmlDocument GetXmlDocument() { string cacheKey = null; if (!_cacheLookupDone && Cache.Enabled) { // If caching is enabled, attempt to load from cache. cacheKey = CreateCacheKey(); _xmlDocument = Cache.LoadDataFromCache(cacheKey) as XmlDocument; _cacheLookupDone = true; } if (_xmlDocument == null) { // Load up the data _xmlDocument = new XmlDocument(); CacheDependency transformCacheDependency; CacheDependency dataCacheDependency; PopulateXmlDocument(_xmlDocument, out dataCacheDependency, out transformCacheDependency); if (cacheKey != null) { Debug.Assert(Cache.Enabled); // If caching is enabled, save the XmlDocument to cache. CacheDependency fileDependency; if (dataCacheDependency != null) { if (transformCacheDependency != null) { // We have both a data file as well as a transform file dependency AggregateCacheDependency aggregateDependency = new AggregateCacheDependency(); aggregateDependency.Add(dataCacheDependency, transformCacheDependency); fileDependency = aggregateDependency; } else { // We only have a data file dependency fileDependency = dataCacheDependency; } } else { // We have at most only a transform file dependency (or no dependency at all) fileDependency = transformCacheDependency; } Cache.SaveDataToCache(cacheKey, _xmlDocument, fileDependency); } } return _xmlDocument; } /// /// Populates an XmlDocument with the appropriate XML data, including applying transforms. /// [SuppressMessage("Microsoft.Security", "MSEC1204:UseSecureXmlResolver", Justification = "Legacy code that trusts our developer input. Optional safer codepath available via appSettings/aspnet:RestrictXmlControls configuration.")] private void PopulateXmlDocument(XmlDocument document, out CacheDependency dataCacheDependency, out CacheDependency transformCacheDependency) { XmlReader transformReader = null; XmlReader dataReader = null; XmlReader tempDataReader = null; try { // Don't allow changes to the XmlDataSource while we are loading the document _disallowChanges = true; // Check if transform is specified. // If there is a transform, load the data, then the transform, and get an XmlReader from the transformation. transformReader = GetReader(TransformFile, Transform, out transformCacheDependency); if (transformReader != null) { tempDataReader = GetReader(DataFile, Data, out dataCacheDependency); // Now load the transform and transform the document data #pragma warning disable 0618 // To avoid deprecation warning XslTransform transform = XmlUtils.CreateXslTransform(transformReader, null); #pragma warning restore 0618 if (transform != null) { OnTransforming(EventArgs.Empty); XmlDocument tempDocument = new XmlDocument(); tempDocument.Load(tempDataReader); // The XmlResolver cast on the third parameter is required to eliminate an ambiguity // from the compiler. dataReader = transform.Transform(tempDocument, _transformArgumentList, (XmlResolver)null); document.Load(dataReader); } else { // XslCompiledTransform for some reason wants to completely re-create an internal XmlReader // from scratch. In doing so, it does not respect all the settings of XmlTextReader. Be 100% // sure that this XmlReader we are using here uses settings of XmlReader and not those // introduced by XmlTextReader. XslCompiledTransform compiledTransform = XmlUtils.CreateXslCompiledTransform(transformReader); OnTransforming(EventArgs.Empty); using (MemoryStream ms = new MemoryStream()) { XmlWriter writer = XmlWriter.Create(ms); compiledTransform.Transform(tempDataReader, _transformArgumentList, writer, null); document.Load(XmlUtils.CreateXmlReader(ms)); } } } else { dataReader = GetReader(DataFile, Data, out dataCacheDependency); document.Load(dataReader); } } finally { _disallowChanges = false; if (dataReader != null) { dataReader.Close(); } if (tempDataReader != null) { tempDataReader.Close(); } if (transformReader != null) { transformReader.Close(); } } } /// /// Called right before the XSLT transform is applied. /// This allows a developer to supply an XsltArgumentList in the TransformArgumentList property. /// protected virtual void OnTransforming(EventArgs e) { EventHandler handler = (EventHandler)Events[EventTransforming]; if (handler != null) { handler(this, e); } } /// /// Saves the XML data to disk. /// public void Save() { if (!IsModifiable) { throw new InvalidOperationException(SR.GetString(SR.XmlDataSource_SaveNotAllowed, ID)); } string writeableDataFile = WriteableDataFile; Debug.Assert(!String.IsNullOrEmpty(writeableDataFile), "Did not expect WriteableDataFile to be empty in Save()"); // Check for write permissions HttpRuntime.CheckFilePermission(writeableDataFile, true); // Save the document GetXmlDocument().Save(writeableDataFile); } #region Implementation of IDataSource event EventHandler IDataSource.DataSourceChanged { add { ((IHierarchicalDataSource)this).DataSourceChanged += value; } remove { ((IHierarchicalDataSource)this).DataSourceChanged -= value; } } /// DataSourceView IDataSource.GetView(string viewName) { if (viewName.Length == 0) { viewName = DefaultViewName; } return new XmlDataSourceView(this, viewName); } /// ICollection IDataSource.GetViewNames() { if (_viewNames == null) { _viewNames = new string[1] { DefaultViewName }; } return _viewNames; } #endregion #region Implementation of IListSource /// bool IListSource.ContainsListCollection { get { if (DesignMode) { return false; } return ListSourceHelper.ContainsListCollection(this); } } /// IList IListSource.GetList() { if (DesignMode) { return null; } return ListSourceHelper.GetList(this); } #endregion } }