// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System; using System.Collections; using System.Data; using System.Globalization; using System.IO; using System.Security; using System.Xml; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Sdk.Sfc; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlTools.ServiceLayer.Connection; namespace Microsoft.SqlTools.ServiceLayer.Management { /// /// CDataContainer /// public class CDataContainer : IDisposable { #region Nested types public enum ServerType { SQL, OLAP, //This type is used only for non-express sku SQLCE, UNKNOWN } #endregion #region Fields private ServerConnection serverConnection; private Server m_server = null; protected XmlDocument m_doc = null; private XmlDocument originalDocument = null; private SqlOlapConnectionInfoBase connectionInfo = null; private SqlConnectionInfoWithConnection sqlCiWithConnection; private bool ownConnection = true; private IManagedConnection managedConnection; protected string serverName; //This member is used for non-express sku only protected string olapServerName; protected string sqlceFilename; private ServerType serverType = ServerType.UNKNOWN; private Hashtable m_hashTable = null; private string objectNameKey = "object-name-9524b5c1-e996-4119-a433-b5b947985566"; private string objectSchemaKey = "object-schema-ccaf2efe-8fa3-4f62-be79-62ef3cbe7390"; private SqlSmoObject sqlDialogSubject = null; private int sqlServerVersion = 0; private int sqlServerEffectiveVersion = 0; #endregion #region Public properties /// /// gets/sets XmlDocument with parameters /// public XmlDocument Document { get { return this.m_doc; } set { this.m_doc = value; if (value != null) { //this.originalDocument = (XmlDocument) value.Clone(); this.originalDocument = value; } else { this.originalDocument = null; } } } /// /// returns the Hashtable that can be used to store generic information /// public Hashtable HashTable { get { if (m_hashTable == null) { m_hashTable = new Hashtable(); } return m_hashTable; } } /// /// gets/sets SMO server object /// public Server Server { get { return m_server; } set { m_server = value; } } /// /// connection info that should be used by the dialogs /// public SqlOlapConnectionInfoBase ConnectionInfo { get { //// update the database name in the serverconnection object to set the correct database context when connected to Azure //var conn = this.connectionInfo as SqlConnectionInfoWithConnection; //if (conn != null && conn.ServerConnection.DatabaseEngineType == DatabaseEngineType.SqlAzureDatabase) //{ // if (this.RelevantDatabaseName != null) // { // IComparer dbNamesComparer = ServerConnection.ConnectionFactory.GetInstance(conn.ServerConnection).ServerComparer as IComparer; // if (dbNamesComparer.Compare(this.RelevantDatabaseName, conn.DatabaseName) != 0) // { // ServerConnection serverConnection = conn.ServerConnection.GetDatabaseConnection(this.RelevantDatabaseName, true, conn.AccessToken); // ((SqlConnectionInfoWithConnection)this.connectionInfo).ServerConnection = serverConnection; // } // } //} return this.connectionInfo; } } /// /// returns SMO server connection object constructed off the connectionInfo. /// This method cannot work until ConnectionInfo property has been set /// public ServerConnection ServerConnection { get { if (this.serverConnection == null) { if (this.serverType != ServerType.SQL) { throw new InvalidOperationException(); } if (this.connectionInfo == null) { throw new InvalidOperationException(); } if (this.sqlCiWithConnection != null) { this.serverConnection = this.sqlCiWithConnection.ServerConnection; } else { SqlConnectionInfo sci = this.connectionInfo as SqlConnectionInfo; this.serverConnection = new ServerConnection(sci); } } return this.serverConnection; } } /// /// returns SMO server connection object constructed off the connectionInfo. /// This method cannot work until ConnectionInfo property has been set /// public SqlConnectionInfoWithConnection SqlInfoWithConnection { get { if (this.serverConnection == null) { if (this.serverType != ServerType.SQL) { throw new InvalidOperationException(); } if (this.connectionInfo == null) { throw new InvalidOperationException(); } if (this.sqlCiWithConnection != null) { this.serverConnection = this.sqlCiWithConnection.ServerConnection; } else { SqlConnectionInfo sci = this.connectionInfo as SqlConnectionInfo; this.serverConnection = new ServerConnection(sci); } } return this.sqlCiWithConnection; } } public string ServerName { get { return this.serverName; } set { this.serverName = value; } } public ServerType ContainerServerType { get { return this.serverType; } set { this.serverType = value; } } public string SqlCeFileName { get { return this.sqlceFilename; } set { this.sqlceFilename = value; } } //This member is used for non-express sku only public string OlapServerName { get { return this.olapServerName; } set { this.olapServerName = value; } } /// /// Whether we are creating a new object /// public bool IsNewObject { get { string itemType = this.GetDocumentPropertyString("itemtype"); return (itemType.Length != 0); } } /// /// The URN to the parent of the object we are creating/modifying /// public string ParentUrn { get { string result = String.Empty; string documentUrn = this.GetDocumentPropertyString("urn"); if (this.IsNewObject) { result = documentUrn; } else { Urn urn = new Urn(documentUrn); result = urn.Parent.ToString(); } return result; } } /// /// The URN to the object we are creating/modifying /// public string ObjectUrn { get { string result = string.Empty; if (this.IsNewObject) { string objectName = this.ObjectName; string objectSchema = this.ObjectSchema; if (0 == objectName.Length) { throw new InvalidOperationException("object name is not known, so URN for object can't be formed"); } if (0 == objectSchema.Length) { result = String.Format(CultureInfo.InvariantCulture, "{0}/{1}[@Name='{2}']", this.ParentUrn, this.ObjectType, Urn.EscapeString(objectName)); } else { result = String.Format(CultureInfo.InvariantCulture, "{0}/{1}[@Schema='{2}' and @Name='{3}']", this.ParentUrn, this.ObjectType, Urn.EscapeString(objectSchema), Urn.EscapeString(objectName)); } } else { result = this.GetDocumentPropertyString("urn"); } return result; } } /// /// The name of the object we are modifying /// public string ObjectName { get { return this.GetDocumentPropertyString(objectNameKey); } set { this.SetDocumentPropertyValue(objectNameKey, value); } } /// /// The schema of the object we are modifying /// public string ObjectSchema { get { return this.GetDocumentPropertyString(objectSchemaKey); } set { this.SetDocumentPropertyValue(objectSchemaKey, value); } } /// /// The type of the object we are creating (as it appears in URNs) /// public string ObjectType { get { // note that the itemtype property is only set for new objects string result = String.Empty; string itemtype = this.GetDocumentPropertyString("itemtype"); // if this is not a new object if (0 == itemtype.Length) { string documentUrn = this.GetDocumentPropertyString("urn"); Urn urn = new Urn(documentUrn); result = urn.Type; } else { result = itemtype; } return result; } } /// /// The SQL SMO object that is the subject of the dialog. /// public SqlSmoObject SqlDialogSubject { get { SqlSmoObject result = null; if (this.sqlDialogSubject != null) { result = this.sqlDialogSubject; } else { result = this.Server.GetSmoObject(this.ObjectUrn); } return result; } set { this.sqlDialogSubject = value; } } /// /// Whether the logged in user is a system administrator /// public bool LoggedInUserIsSysadmin { get { bool result = false; if (this.Server != null && this.Server.ConnectionContext != null) { result = this.Server.ConnectionContext.IsInFixedServerRole(FixedServerRoles.SysAdmin); } return result; } } /// /// Get the name of the Database that contains (or is) the subject of the dialog. /// If no there is no relevant database, then an empty string is returned. /// public string RelevantDatabaseName { get { string result = String.Empty; string urnText = this.GetDocumentPropertyString("urn"); if (urnText.Length != 0) { Urn urn = new Urn(urnText); while ((urn != null) && (urn.Type != "Database")) { urn = urn.Parent; } if ((urn != null) && (urn.Type == "Database")) { result = urn.GetAttribute("Name"); } } return result; } } /// /// The server major version number /// public int SqlServerVersion { get { if (this.sqlServerVersion == 0) { this.sqlServerVersion = 9; if ((this.ConnectionInfo != null) && (ServerType.SQL == this.ContainerServerType)) { Enumerator enumerator = new Enumerator(); Urn urn = "Server/Information"; string[] fields = new string[] { "VersionMajor" }; DataTable dataTable = enumerator.Process(this.ConnectionInfo, new Request(urn, fields)); if (dataTable.Rows.Count != 0) { this.sqlServerVersion = (int)dataTable.Rows[0][0]; } } } return this.sqlServerVersion; } } /// /// The server version the database is emulating. If database compatibility level is /// not relevant to the subject, then this just returns the actual server version. /// public int EffectiveSqlServerVersion { get { if (this.sqlServerEffectiveVersion == 0) { this.sqlServerEffectiveVersion = 9; if ((this.ConnectionInfo != null) && (ServerType.SQL == this.ContainerServerType)) { string databaseName = this.RelevantDatabaseName; if (databaseName.Length != 0) { Enumerator enumerator = new Enumerator(); Urn urn = String.Format("Server/Database[@Name='{0}']", Urn.EscapeString(databaseName)); string[] fields = new string[] { "CompatibilityLevel" }; DataTable dataTable = enumerator.Process(this.ConnectionInfo, new Request(urn, fields)); if (dataTable.Rows.Count != 0) { CompatibilityLevel level = (CompatibilityLevel)dataTable.Rows[0][0]; switch (level) { case CompatibilityLevel.Version60: case CompatibilityLevel.Version65: this.sqlServerEffectiveVersion = 6; break; case CompatibilityLevel.Version70: this.sqlServerEffectiveVersion = 7; break; case CompatibilityLevel.Version80: this.sqlServerEffectiveVersion = 8; break; case CompatibilityLevel.Version90: this.sqlServerEffectiveVersion = 9; break; case CompatibilityLevel.Version100: this.sqlServerEffectiveVersion = 10; break; case CompatibilityLevel.Version110: this.sqlServerEffectiveVersion = 11; break; case CompatibilityLevel.Version120: this.sqlServerEffectiveVersion = 12; break; case CompatibilityLevel.Version130: this.sqlServerEffectiveVersion = 13; break; case CompatibilityLevel.Version140: this.sqlServerEffectiveVersion = 14; break; default: this.sqlServerEffectiveVersion = 14; break; } } else { this.sqlServerEffectiveVersion = this.SqlServerVersion; } } else { this.sqlServerEffectiveVersion = this.SqlServerVersion; } } } return this.sqlServerEffectiveVersion; } } #endregion #region Constructors, finalizer public CDataContainer() { } /// /// contructs the object and initializes its SQL ConnectionInfo and ServerConnection properties /// using the specified connection info containing live connection. /// /// connection info containing live connection public CDataContainer(object ciObj, bool ownConnection) { SqlConnectionInfoWithConnection ci = (SqlConnectionInfoWithConnection)ciObj; if (ci == null) { throw new ArgumentNullException("ci"); } ApplyConnectionInfo(ci, ownConnection); } /// /// contructs the object and initializes its SQL ConnectionInfo and ServerConnection properties /// using the specified connection info containing live connection. /// /// in addition creates a server of the given server type /// /// connection info containing live connection public CDataContainer(ServerType serverType, object ciObj, bool ownConnection) { SqlConnectionInfoWithConnection ci = (SqlConnectionInfoWithConnection)ciObj; if (ci == null) { throw new ArgumentNullException("ci"); } this.serverType = serverType; ApplyConnectionInfo(ci, ownConnection); if (serverType == ServerType.SQL) { //NOTE: ServerConnection property will constuct the object if needed m_server = new Server(ServerConnection); } else { throw new ArgumentException(SR.UnknownServerType(serverType.ToString())); } } /// /// Constructor /// /// Server type /// Server name /// true if connection is trused. If true user name and password are ignored /// User name for not trusted connections /// Password for not trusted connections /// XML string with parameters public CDataContainer(ServerType serverType, string serverName, bool trusted, string userName, SecureString password, string databaseName, string xmlParameters, string azureAccountToken = null) { this.serverType = serverType; this.serverName = serverName; if (serverType == ServerType.SQL) { // does some extra initialization ApplyConnectionInfo(GetTempSqlConnectionInfoWithConnection(serverName, trusted, userName, password, databaseName, azureAccountToken), true); // NOTE: ServerConnection property will constuct the object if needed m_server = new Server(ServerConnection); } else { throw new ArgumentException(SR.UnknownServerType(serverType.ToString())); } if (xmlParameters != null) { this.Document = GenerateXmlDocumentFromString(xmlParameters); } if (ServerType.SQL == serverType) { this.InitializeObjectNameAndSchema(); } } /// /// /// /// Data container /// XML string with parameters public CDataContainer(CDataContainer dataContainer, string xmlParameters) { Server = dataContainer.Server; this.serverName = dataContainer.serverName; this.serverType = dataContainer.serverType; this.connectionInfo = dataContainer.connectionInfo; this.ownConnection = dataContainer.ownConnection; this.sqlCiWithConnection = dataContainer.connectionInfo as SqlConnectionInfoWithConnection; if (this.sqlCiWithConnection != null) { //we want to be notified if it is closed this.sqlCiWithConnection.ConnectionClosed += new EventHandler(OnSqlConnectionClosed); } if (xmlParameters != null) { XmlDocument doc = GenerateXmlDocumentFromString(xmlParameters); this.Init(doc); } } ~CDataContainer() { Dispose(false); } #endregion #region Public virtual methods /// /// Initialization routine that is a convience fuction for clients with the data in a string /// /// The string that contains the xml data public virtual void Init(string xmlText) { XmlDocument xmlDoc = GenerateXmlDocumentFromString(xmlText); this.Init(xmlDoc); } /// /// Overload of basic Init which takes a IServiceProvider and initializes /// what it can of the container with elements provided by IServcieProvider /// /// Today this is only the IManagedProvider if available but this function /// could be modified to init other things provided by the ServiceProvider /// /// /// public virtual void Init(XmlDocument doc, IServiceProvider site) { if (site != null) { // see if service provider supports INodeInformation interface from the object explorer // NOTE: we're trying to forcefully set connection information on the data container. // If this code doesn't execute, then dc.Init call below will result in CDataContainer // initializing its ConnectionInfo member with a new object contructed off the parameters // in the XML doc [server name, user name etc] IManagedConnection managedConnection = site.GetService(typeof(IManagedConnection)) as IManagedConnection; if (managedConnection != null) { this.SetManagedConnection(managedConnection); } } this.Document = doc; LoadData(); // finish the initialization this.Init(doc); } /// /// main initialization method - the object is unusable until this method is called /// NOTE: it will ensure the ConnectionInfo and ServerConnetion objects are constructed /// for the appropriate server types /// /// public virtual void Init(XmlDocument doc) { // First, we read the data from XML by calling LoadData this.Document = doc; LoadData(); // Second, create the rignt server and connection objects if (this.serverType == ServerType.SQL) { // ensure that we have a valid ConnectionInfo if (ConnectionInfo == null) { throw new InvalidOperationException(); } if (m_server == null) { // NOTE: ServerConnection property will constuct the object if needed m_server = new Server(ServerConnection); } } else if (this.serverType == ServerType.SQLCE) { // do nothing; originally we were only distinguishing between two // types of servers (OLAP/SQL); as a result for SQLCE we were // executing the same codepath as for OLAP server which was // resulting in an exception; } } /// /// loads data into internal members from the XML document and detects the server type /// [SQL, OLAP etc] based on the info in the XML doc /// public virtual void LoadData() { STParameters param; bool bStatus; param = new STParameters(); param.SetDocument(m_doc); // This is an ugly way to distinguish between different server types // Maybe we should pass server type as one of the parameters? bStatus = param.GetParam("servername", ref this.serverName); if (!bStatus || this.serverName.Length == 0) { { bStatus = param.GetParam("database", ref this.sqlceFilename); if (bStatus && !string.IsNullOrEmpty(this.sqlceFilename)) { this.serverType = ServerType.SQLCE; } else if (this.sqlCiWithConnection != null) { this.serverType = ServerType.SQL; } else { this.serverType = ServerType.UNKNOWN; } } } else { // OK, let's see if was specified in the parameters. It it was, use // it to double check that it is SQL string specifiedServerType = ""; bStatus = param.GetParam("servertype", ref specifiedServerType); if (bStatus) { if (specifiedServerType != null && "sql" != specifiedServerType.ToLowerInvariant()) { this.serverType = ServerType.UNKNOWN; // we know only about 3 types, and 2 of them were excluded by if branch above } else { this.serverType = ServerType.SQL; } } else { this.serverType = ServerType.SQL; } } // Ensure there is no password in the XML document string temp = string.Empty; if (param.GetParam("password", ref temp)) { temp = null; throw new SecurityException(); } if (ServerType.SQL == this.serverType) { this.InitializeObjectNameAndSchema(); } } #endregion #region Public methods /// /// we need to store it as context from the OE /// /// internal void SetManagedConnection(IManagedConnection managedConnection) { this.managedConnection = managedConnection; ApplyConnectionInfo(managedConnection.Connection, true);//it will do some extra initialization } /// /// Get the named property value from the XML document /// /// The name of the property to get /// The property value public object GetDocumentPropertyValue(string propertyName) { object result = null; STParameters param = new STParameters(this.Document); param.GetBaseParam(propertyName, ref result); return result; } /// /// Get the named property value from the XML document /// /// The name of the property to get /// The property value public string GetDocumentPropertyString(string propertyName) { object result = GetDocumentPropertyValue(propertyName); if (result == null) { result = string.Empty; } return (string)result; } /// /// Set the named property value in the XML document /// /// The name of the property to set /// The property value public void SetDocumentPropertyValue(string propertyName, string propertyValue) { STParameters param = new STParameters(this.Document); param.SetParam(propertyName, propertyValue); } #endregion #region internal methods /// /// Reset the data container to its state from just after it was last initialized or reset /// internal void Reset() { if (this.originalDocument != null) { this.Init(this.originalDocument); } if (this.m_hashTable != null) { this.m_hashTable = new Hashtable(); } } #endregion #region Private helpers /// /// Get the name and schema (if applicable) for the object we are referring to /// private void InitializeObjectNameAndSchema() { string documentUrn = this.GetDocumentPropertyString("urn"); if (documentUrn.Length != 0) { Urn urn = new Urn(documentUrn); string name = urn.GetAttribute("Name"); string schema = urn.GetAttribute("Schema"); if ((name != null) && (name.Length != 0)) { this.ObjectName = name; } if ((schema != null) && (schema.Length != 0)) { this.ObjectSchema = schema; } } } /// /// returns SqlConnectionInfoWithConnection object constructed using our internal vars /// /// private SqlConnectionInfoWithConnection GetTempSqlConnectionInfoWithConnection( string serverName, bool trusted, string userName, SecureString password, string databaseName, string azureAccountToken) { SqlConnectionInfoWithConnection tempCI = new SqlConnectionInfoWithConnection(serverName); tempCI.SingleConnection = false; tempCI.Pooled = false; //BUGBUG - set the right application name? if (trusted) { tempCI.UseIntegratedSecurity = true; } else { tempCI.UseIntegratedSecurity = false; tempCI.UserName = userName; tempCI.SecurePassword = password; } tempCI.DatabaseName = databaseName; return tempCI; } /// /// our handler of sqlCiWithConnection.ConnectionClosed /// /// /// private void OnSqlConnectionClosed(object sender, EventArgs e) { } /// /// stores specified connection info and performs some extra initialization steps /// that can only be done after we have the connection information /// /// private void ApplyConnectionInfo(SqlOlapConnectionInfoBase ci, bool ownConnection) { this.connectionInfo = ci; this.ownConnection = ownConnection; // cache the cast value. It is OK that it is null for non SQL types this.sqlCiWithConnection = ci as SqlConnectionInfoWithConnection; if (this.sqlCiWithConnection != null) { // we want to be notified if it is closed this.sqlCiWithConnection.ConnectionClosed += new EventHandler(OnSqlConnectionClosed); } } private static bool MustRethrow(Exception exception) { bool result = false; switch (exception.GetType().Name) { case "ExecutionEngineException": case "OutOfMemoryException": case "AccessViolationException": case "BadImageFormatException": case "InvalidProgramException": result = true; break; } return result; } /// /// Generates an XmlDocument from a string, avoiding exploits available through /// DTDs /// /// /// private XmlDocument GenerateXmlDocumentFromString(string sourceXml) { if (sourceXml == null) { throw new ArgumentNullException("sourceXml"); } if (sourceXml.Length == 0) { throw new ArgumentException("sourceXml"); } using (MemoryStream memoryStream = new MemoryStream()) { using (StreamWriter streamWriter = new StreamWriter(memoryStream)) { // Writes the xml to the memory stream streamWriter.Write(sourceXml); streamWriter.Flush(); // Resets the stream to the beginning memoryStream.Seek(0, SeekOrigin.Begin); // Creates the XML reader from the stream // and moves it to the correct node XmlReader xmlReader = XmlReader.Create(memoryStream); xmlReader.MoveToContent(); // generate the xml document XmlDocument xmlDocument = new XmlDocument(); xmlDocument.PreserveWhitespace = true; xmlDocument.LoadXml(xmlReader.ReadOuterXml()); return xmlDocument; } } } #endregion #region IDisposable public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// MUST be called, as we'll be closing SQL connection inside this call /// private void Dispose(bool disposing) { try { //take care of live SQL connection if (this.sqlCiWithConnection != null) { this.sqlCiWithConnection.ConnectionClosed -= new EventHandler(OnSqlConnectionClosed); if (disposing) { //if we have the managed connection interface, then use it to disconnect. //Otherwise, Dispose on SqlConnectionInfoWithConnection should disconnect if (this.managedConnection != null) { //in this case we have gotten sqlCiWithConnection as this.managedConnection.Connection if (this.ownConnection) { this.managedConnection.Close(); } this.managedConnection = null; } else { if (this.ownConnection) { this.sqlCiWithConnection.Dispose();//internally will decide whether to disconnect or not } } this.sqlCiWithConnection = null; } else { this.managedConnection = null; this.sqlCiWithConnection = null; } } else if (this.managedConnection != null) { if (disposing && this.ownConnection) { this.managedConnection.Close(); } this.managedConnection = null; } } catch (Exception) { } } #endregion /// /// Create a data container object /// /// connection info /// flag indicating whether to create taskhelper for existing database or not internal static CDataContainer CreateDataContainer( ConnectionInfo connInfo, bool databaseExists = false, XmlDocument containerDoc = null) { if (containerDoc == null) { containerDoc = CreateDataContainerDocument(connInfo, databaseExists); } var serverConnection = ConnectionService.OpenServerConnection(connInfo, "DataContainer"); var connectionInfoWithConnection = new SqlConnectionInfoWithConnection(); connectionInfoWithConnection.ServerConnection = serverConnection; CDataContainer dataContainer = new CDataContainer(ServerType.SQL, connectionInfoWithConnection, true); dataContainer.Init(containerDoc); return dataContainer; } internal static System.Security.SecureString BuildSecureStringFromPassword(string password) { var passwordSecureString = new System.Security.SecureString(); if (password != null) { foreach (char c in password) { passwordSecureString.AppendChar(c); } } return passwordSecureString; } /// /// Create data container document /// /// connection info /// flag indicating whether to create document for existing database or not /// private static XmlDocument CreateDataContainerDocument(ConnectionInfo connInfo, bool databaseExists) { string xml = string.Empty; if (!databaseExists) { xml = string.Format(@" {0} {0} (SQLServer, user = {1}) sql Server[@Name='{0}'] Database ", connInfo.ConnectionDetails.ServerName.ToUpper(), connInfo.ConnectionDetails.UserName); } else { xml = string.Format(@" {0} {0} (SQLServer, user = {1}) sql Server[@Name='{0}'] {2} ", connInfo.ConnectionDetails.ServerName.ToUpper(), connInfo.ConnectionDetails.UserName, connInfo.ConnectionDetails.DatabaseName); } var xmlDoc = new XmlDocument(); xmlDoc.LoadXml(xml); return xmlDoc; } } }