File: UI\WebControls\XmlDataSource.cs
Project: ndp\fx\src\xsp\system\Web\System.Web.csproj (System.Web)
//------------------------------------------------------------------------------
// <copyright file="XmlDataSource.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
 
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;
 
 
 
    /// <devdoc>
    /// 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.
    /// </devdoc>
    [
    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;
 
 
        /// <devdoc>
        /// Specifies the cache settings for this data source.
        /// </devdoc>
        private DataSourceCache Cache {
            get {
                if (_cache == null) {
                    _cache = new DataSourceCache();
                    _cache.Enabled = true;
                }
                return _cache;
            }
        }
 
 
        /// <devdoc>
        /// The duration, in seconds, of the expiration. The expiration policy is specified by the CacheExpirationPolicy property.
        /// </devdoc>
        [
        DefaultValue(DataSourceCache.Infinite),
        TypeConverterAttribute(typeof(DataSourceCacheDurationConverter)),
        WebCategory("Cache"),
        WebSysDescription(SR.DataSourceCache_Duration),
        ]
        public virtual int CacheDuration {
            get {
                return Cache.Duration;
            }
            set {
                Cache.Duration = value;
            }
        }
 
        /// <devdoc>
        /// The expiration policy of the cache. The duration for the expiration is specified by the CacheDuration property.
        /// </devdoc>
        [
        DefaultValue(DataSourceCacheExpiry.Absolute),
        WebCategory("Cache"),
        WebSysDescription(SR.DataSourceCache_ExpirationPolicy),
        ]
        public virtual DataSourceCacheExpiry CacheExpirationPolicy {
            get {
                return Cache.ExpirationPolicy;
            }
            set {
                Cache.ExpirationPolicy = value;
            }
        }
 
        /// <devdoc>
        /// 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.
        /// </devdoc>
        [
        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;
            }
        }
 
        /// <devdoc>
        /// Inline XML content.
        /// </devdoc>
        [
        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);
                }
            }
        }
 
        /// <devdoc>
        /// Path to an XML file.
        /// </devdoc>
        [
        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);
                }
            }
        }
 
        /// <devdoc>
        /// Whether caching is enabled for this data source.
        /// </devdoc>
        [
        DefaultValue(true),
        WebCategory("Cache"),
        WebSysDescription(SR.DataSourceCache_Enabled),
        ]
        public virtual bool EnableCaching {
            get {
                return Cache.Enabled;
            }
            set {
                Cache.Enabled = value;
            }
        }
 
        /// <devdoc>
        /// Indicates whether the XML data can be modified.
        /// This is also used by XmlDataSourceView to determine whether CanDelete/Insert/Update are true.
        /// </devdoc>
        internal bool IsModifiable {
            get {
                return (String.IsNullOrEmpty(TransformFile) &&
                        String.IsNullOrEmpty(Transform) &&
                        !String.IsNullOrEmpty(WriteableDataFile));
            }
        }
 
        /// <devdoc>
        /// Inline XSL transform.
        /// </devdoc>
        [
        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);
                }
            }
        }
 
        /// <devdoc>
        /// Arguments for the XSL transform.
        /// This should be populated in the Transforming event.
        /// </devdoc>
        [
        Browsable(false),
        ]
        public virtual XsltArgumentList TransformArgumentList {
            get {
                return _transformArgumentList;
            }
            set {
                _transformArgumentList = value;
            }
        }
 
        /// <devdoc>
        /// Path to an XSL transform file.
        /// </devdoc>
        [
        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);
                }
            }
        }
 
        /// <devdoc>
        /// 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.
        /// </devdoc>
        private string WriteableDataFile {
            get {
                if (_writeableDataFile == null) {
                    _writeableDataFile = GetWriteableDataFile();
                }
                return _writeableDataFile;
            }
        }
 
        /// <devdoc>
        /// Specifies an initial XPath that is applied to the XML data.
        /// </devdoc>
        [
        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);
                }
            }
        }
 
 
        /// <devdoc>
        /// Raised before the XSL transform is applied.
        /// </devdoc>
        [
        WebCategory("Data"),
        WebSysDescription(SR.XmlDataSource_Transforming),
        ]
        public event EventHandler Transforming {
            add {
                Events.AddHandler(EventTransforming, value);
            }
            remove {
                Events.RemoveHandler(EventTransforming, value);
            }
        }
 
 
        /// <devdoc>
        /// Creates a unique cache key for this data source's data.
        /// </devdoc>
        // 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();
        }
 
        /// <devdoc>
        /// Returns a HierarchicalDataSourceView based on an XPath specified by viewPath.
        /// </devdoc>
        protected override HierarchicalDataSourceView GetHierarchicalView(string viewPath) {
            return new XmlHierarchicalDataSourceView(this, viewPath);
        }
 
        /// <devdoc>
        /// 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.
        /// </devdoc>
        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));
                }
            }
        }
 
        /// <devdoc>
        /// 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.
        /// </devdoc>
        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;
            }
        }
 
        /// <devdoc>
        /// Returns the XmlDocument representing the XML data.
        /// If necessary, the XML data will be reloaded along with the transform, if available.
        /// </devdoc>
        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;
        }
 
        /// <devdoc>
        /// Populates an XmlDocument with the appropriate XML data, including applying transforms.
        /// </devdoc>
        [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();
                }
            }
        }
 
        /// <devdoc>
        /// Called right before the XSLT transform is applied.
        /// This allows a developer to supply an XsltArgumentList in the TransformArgumentList property.
        /// </devdoc>
        protected virtual void OnTransforming(EventArgs e) {
            EventHandler handler = (EventHandler)Events[EventTransforming];
            if (handler != null) {
                handler(this, e);
            }
        }
 
        /// <devdoc>
        /// Saves the XML data to disk.
        /// </devdoc>
        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;
            }
        }
 
 
        /// <internalonly/>
        DataSourceView IDataSource.GetView(string viewName) {
            if (viewName.Length == 0) {
                viewName = DefaultViewName;
            }
            return new XmlDataSourceView(this, viewName);
        }
 
 
        /// <internalonly/>
        ICollection IDataSource.GetViewNames() {
            if (_viewNames == null) {
                _viewNames = new string[1] { DefaultViewName };
            }
            return _viewNames;
        }
        #endregion
 
        #region Implementation of IListSource
        /// <internalonly/>
        bool IListSource.ContainsListCollection {
            get {
                if (DesignMode) {
                    return false;
                }
                return ListSourceHelper.ContainsListCollection(this);
            }
        }
 
 
        /// <internalonly/>
        IList IListSource.GetList() {
            if (DesignMode) {
                return null;
            }
            return ListSourceHelper.GetList(this);
        }
        #endregion
    }
}