// // Copyright (c) 2006 Microsoft Corporation. All rights reserved. // // THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF // ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A // PARTICULAR PURPOSE. // using System; using System.IO; using System.Data; using System.Data.Odbc; using System.Collections.ObjectModel; using System.Text; using System.Diagnostics; using System.Text.RegularExpressions; using System.Management.Automation; using System.Management.Automation.Provider; using System.ComponentModel; using System.Globalization; namespace Microsoft.Samples.PowerShell.Providers { #region AccessDBProvider /// /// A PowerShell Provider which acts upon a access database. /// /// /// This example implements the item overloads. /// [CmdletProvider("AccessDB", ProviderCapabilities.None)] public class AccessDBProvider : ItemCmdletProvider { #region Drive Manipulation /// /// Create a new drive. Create a connection to the database file and set /// the Connection property in the PSDriveInfo. /// /// /// Information describing the drive to add. /// /// The added drive. protected override PSDriveInfo NewDrive(PSDriveInfo drive) { // check if drive object is null if (drive == null) { WriteError(new ErrorRecord( new ArgumentNullException("drive"), "NullDrive", ErrorCategory.InvalidArgument, null) ); return null; } // check if drive root is not null or empty // and if its an existing file if (String.IsNullOrEmpty(drive.Root) || (File.Exists(drive.Root) == false)) { WriteError(new ErrorRecord( new ArgumentException("drive.Root"), "NoRoot", ErrorCategory.InvalidArgument, drive) ); return null; } // create a new drive and create an ODBC connection to the new drive AccessDBPSDriveInfo accessDBPSDriveInfo = new AccessDBPSDriveInfo(drive); OdbcConnectionStringBuilder builder = new OdbcConnectionStringBuilder(); builder.Driver = "Microsoft Access Driver (*.mdb)"; builder.Add("DBQ", drive.Root); OdbcConnection conn = new OdbcConnection(builder.ConnectionString); conn.Open(); accessDBPSDriveInfo.Connection = conn; return accessDBPSDriveInfo; } // NewDrive /// /// Removes a drive from the provider. /// /// The drive to remove. /// The drive removed. protected override PSDriveInfo RemoveDrive(PSDriveInfo drive) { // check if drive object is null if (drive == null) { WriteError(new ErrorRecord( new ArgumentNullException("drive"), "NullDrive", ErrorCategory.InvalidArgument, drive) ); return null; } // close ODBC connection to the drive AccessDBPSDriveInfo accessDBPSDriveInfo = drive as AccessDBPSDriveInfo; if (accessDBPSDriveInfo == null) { return null; } accessDBPSDriveInfo.Connection.Close(); return accessDBPSDriveInfo; } // RemoveDrive #endregion Drive Manipulation #region Item Methods /// /// Retrieves an item using the specified path. /// /// The path to the item to return. protected override void GetItem(string path) { // check if the path represented is a drive if (PathIsDrive(path)) { WriteItemObject(this.PSDriveInfo, path, true); return; }// if (PathIsDrive... // Get table name and row information from the path and do // necessary actions string tableName; int rowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); if (type == PathType.Table) { DatabaseTableInfo table = GetTable(tableName); WriteItemObject(table, path, true); } else if (type == PathType.Row) { DatabaseRowInfo row = GetRow(tableName, rowNumber); WriteItemObject(row, path, false); } else { ThrowTerminatingInvalidPathException(path); } } // GetItem /// /// Set the content of a row of data specified by the supplied path /// parameter. /// /// Specifies the path to the row whose columns /// will be updated. /// Comma separated string of values protected override void SetItem(string path, object values) { // Get type, table name and row number from the path specified string tableName; int rowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); if (type != PathType.Row) { WriteError(new ErrorRecord(new NotSupportedException( "SetNotSupported"), "", ErrorCategory.InvalidOperation, path)); return; } // Get in-memory representation of table OdbcDataAdapter da = GetAdapterForTable(tableName); if (da == null) { return; } DataSet ds = GetDataSetForTable(da, tableName); DataTable table = GetDataTable(ds, tableName); if (rowNumber >= table.Rows.Count) { // The specified row number has to be available. If not // NewItem has to be used to add a new row throw new ArgumentException("Row specified is not available"); } // if (rowNum... string[] colValues = (values as string).Split(','); // set the specified row DataRow row = table.Rows[rowNumber]; for (int i = 0; i < colValues.Length; i++) { row[i] = colValues[i]; } // Update the table if (ShouldProcess(path, "SetItem")) { da.Update(ds, tableName); } } // SetItem /// /// Test to see if the specified item exists. /// /// The path to the item to verify. /// True if the item is found. protected override bool ItemExists(string path) { // check if the path represented is a drive if (PathIsDrive(path)) { return true; } // Obtain type, table name and row number from path string tableName; int rowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); DatabaseTableInfo table = GetTable(tableName); if (type == PathType.Table) { // if specified path represents a table then DatabaseTableInfo // object for the same should exist if (table != null) { return true; } } else if (type == PathType.Row) { // if specified path represents a row then DatabaseTableInfo should // exist for the table and then specified row number must be within // the maximum row count in the table if (table != null && rowNumber < table.RowCount) { return true; } } return false; } // ItemExists /// /// Test to see if the specified path is syntactically valid. /// /// The path to validate. /// True if the specified path is valid. protected override bool IsValidPath(string path) { bool result = true; // check if the path is null or empty if (String.IsNullOrEmpty(path)) { result = false; } // convert all separators in the path to a uniform one path = NormalizePath(path); // split the path into individual chunks string[] pathChunks = path.Split(pathSeparator.ToCharArray()); foreach (string pathChunk in pathChunks) { if (pathChunk.Length == 0) { result = false; } } return result; } // IsValidPath #endregion Item Overloads #region Helper Methods /// /// Checks if a given path is actually a drive name. /// /// The path to check. /// /// True if the path given represents a drive, false otherwise. /// private bool PathIsDrive(string path) { // Remove the drive name and first path separator. If the // path is reduced to nothing, it is a drive. Also if its // just a drive then there wont be any path separators if (String.IsNullOrEmpty( path.Replace(this.PSDriveInfo.Root, "")) || String.IsNullOrEmpty( path.Replace(this.PSDriveInfo.Root + pathSeparator, "")) ) { return true; } else { return false; } } // PathIsDrive /// /// Breaks up the path into individual elements. /// /// The path to split. /// An array of path segments. private string[] ChunkPath(string path) { // Normalize the path before splitting string normalPath = NormalizePath(path); // Return the path with the drive name and first path // separator character removed, split by the path separator. string pathNoDrive = normalPath.Replace(this.PSDriveInfo.Root + pathSeparator, ""); return pathNoDrive.Split(pathSeparator.ToCharArray()); } // ChunkPath /// /// Adapts the path, making sure the correct path separator /// character is used. /// /// /// private string NormalizePath(string path) { string result = path; if (!String.IsNullOrEmpty(path)) { result = path.Replace("/", pathSeparator); } return result; } // NormalizePath /// /// Chunks the path and returns the table name and the row number /// from the path /// /// Path to chunk and obtain information /// Name of the table as represented in the /// path /// Row number obtained from the path /// what the path represents private PathType GetNamesFromPath(string path, out string tableName, out int rowNumber) { PathType retVal = PathType.Invalid; rowNumber = -1; tableName = null; // Check if the path specified is a drive if (PathIsDrive(path)) { return PathType.Database; } // chunk the path into parts string[] pathChunks = ChunkPath(path); switch (pathChunks.Length) { case 1: { string name = pathChunks[0]; if (TableNameIsValid(name)) { tableName = name; retVal = PathType.Table; } } break; case 2: { string name = pathChunks[0]; if (TableNameIsValid(name)) { tableName = name; } int number = SafeConvertRowNumber(pathChunks[1]); if (number >= 0) { rowNumber = number; retVal = PathType.Row; } else { WriteError(new ErrorRecord( new ArgumentException("Row number is not valid"), "RowNumberNotValid", ErrorCategory.InvalidArgument, path)); } } break; default: { WriteError(new ErrorRecord( new ArgumentException("The path supplied has too many segments"), "PathNotValid", ErrorCategory.InvalidArgument, path)); } break; } // switch(pathChunks... return retVal; } // GetNamesFromPath /// /// Throws an argument exception stating that the specified path does /// not represent either a table or a row /// /// path which is invalid private void ThrowTerminatingInvalidPathException(string path) { StringBuilder message = new StringBuilder("Path must represent either a table or a row :"); message.Append(path); throw new ArgumentException(message.ToString()); } /// /// Retrieve the list of tables from the database. /// /// /// Collection of DatabaseTableInfo objects, each object representing /// information about one database table /// private Collection GetTables() { Collection results = new Collection(); // using ODBC connection to the database and get the schema of tables AccessDBPSDriveInfo di = this.PSDriveInfo as AccessDBPSDriveInfo; if (di == null) { return null; } OdbcConnection connection = di.Connection; DataTable dt = connection.GetSchema("Tables"); int count; // iterate through all rows in the schema and create DatabaseTableInfo // objects which represents a table foreach (DataRow dr in dt.Rows) { String tableName = dr["TABLE_NAME"] as String; DataColumnCollection columns = null; // find the number of rows in the table try { String cmd = "Select count(*) from \"" + tableName + "\""; OdbcCommand command = new OdbcCommand(cmd, connection); count = (Int32)command.ExecuteScalar(); } catch { count = 0; } // create DatabaseTableInfo object representing the table DatabaseTableInfo table = new DatabaseTableInfo(dr, tableName, count, columns); results.Add(table); } // foreach (DataRow... return results; } // GetTables /// /// Return row information from a specified table. /// /// The name of the database table from /// which to retrieve rows. /// Collection of row information objects. private Collection GetRows(string tableName) { Collection results = new Collection(); // Obtain rows in the table and add it to the collection try { OdbcDataAdapter da = GetAdapterForTable(tableName); if (da == null) { return null; } DataSet ds = GetDataSetForTable(da, tableName); DataTable table = GetDataTable(ds, tableName); int i = 0; foreach (DataRow row in table.Rows) { results.Add(new DatabaseRowInfo(row, i.ToString(CultureInfo.CurrentCulture))); i++; } // foreach (DataRow... } catch (Exception e) { WriteError(new ErrorRecord(e, "CannotAccessSpecifiedRows", ErrorCategory.InvalidOperation, tableName)); } return results; } // GetRows /// /// Retrieve information about a single table. /// /// The table for which to retrieve /// data. /// Table information. private DatabaseTableInfo GetTable(string tableName) { foreach (DatabaseTableInfo table in GetTables()) { if (String.Equals(tableName, table.Name, StringComparison.OrdinalIgnoreCase)) { return table; } } return null; } // GetTable /// /// Obtain a data adapter for the specified Table /// /// Name of the table to obtain the /// adapter for /// Adapter object for the specified table /// An adapter serves as a bridge between a DataSet (in memory /// representation of table) and the data source private OdbcDataAdapter GetAdapterForTable(string tableName) { OdbcDataAdapter da = null; AccessDBPSDriveInfo di = this.PSDriveInfo as AccessDBPSDriveInfo; if (di == null || !TableNameIsValid(tableName) || !TableIsPresent(tableName)) { return null; } OdbcConnection connection = di.Connection; try { // Create a odbc data adpater. This can be sued to update the // data source with the records that will be created here // using data sets string sql = "Select * from " + tableName; da = new OdbcDataAdapter(new OdbcCommand(sql, connection)); // Create a odbc command builder object. This will create sql // commands automatically for a single table, thus // eliminating the need to create new sql statements for // every operation to be done. OdbcCommandBuilder cmd = new OdbcCommandBuilder(da); // Open the connection if its not already open if (connection.State != ConnectionState.Open) { connection.Open(); } } catch (Exception e) { WriteError(new ErrorRecord(e, "CannotAccessSpecifiedTable", ErrorCategory.InvalidOperation, tableName)); } return da; } // GetAdapterForTable /// /// Gets the DataSet (in memory representation) for the table /// for the specified adapter /// /// Adapter to be used for obtaining /// the table /// Name of the table for which a /// DataSet is required /// The DataSet with the filled in schema private DataSet GetDataSetForTable(OdbcDataAdapter adapter, string tableName) { Debug.Assert(adapter != null); // Create a dataset object which will provide an in-memory // representation of the data being worked upon in the // data source. DataSet ds = new DataSet(); // Create a table named "Table" which will contain the same // schema as in the data source. //adapter.FillSchema(ds, SchemaType.Source); adapter.Fill(ds, tableName); ds.Locale = CultureInfo.InvariantCulture; return ds; } //GetDataSetForTable /// /// Get the DataTable object which can be used to operate on /// for the specified table in the data source /// /// DataSet object which contains the tables /// schema /// Name of the table /// Corresponding DataTable object representing /// the table /// private DataTable GetDataTable(DataSet ds, string tableName) { Debug.Assert(ds != null); Debug.Assert(tableName != null); DataTable table = ds.Tables[tableName]; table.Locale = CultureInfo.InvariantCulture; return table; } // GetDataTable /// /// Retrieves a single row from the named table. /// /// The table that contains the /// numbered row. /// The index of the row to return. /// The specified table row. private DatabaseRowInfo GetRow(string tableName, int row) { Collection di = GetRows(tableName); // if the row is invalid write an appropriate error else return the // corresponding row information if (row < di.Count && row >= 0) { return di[row]; } else { WriteError(new ErrorRecord( new ItemNotFoundException(), "RowNotFound", ErrorCategory.ObjectNotFound, row.ToString(CultureInfo.CurrentCulture)) ); } return null; } // GetRow /// /// Method to safely convert a string representation of a row number /// into its Int32 equivalent /// /// String representation of the row /// number /// If there is an exception, -1 is returned private int SafeConvertRowNumber(string rowNumberAsStr) { int rowNumber = -1; try { rowNumber = Convert.ToInt32(rowNumberAsStr, CultureInfo.CurrentCulture); } catch (FormatException fe) { WriteError(new ErrorRecord(fe, "RowStringFormatNotValid", ErrorCategory.InvalidData, rowNumberAsStr)); } catch (OverflowException oe) { WriteError(new ErrorRecord(oe, "RowStringConversionToNumberFailed", ErrorCategory.InvalidData, rowNumberAsStr)); } return rowNumber; } // SafeConvertRowNumber /// /// Check if a table name is valid /// /// Table name to validate /// Helps to check for SQL injection attacks private bool TableNameIsValid(string tableName) { Regex exp = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); if (exp.IsMatch(tableName)) { return true; } WriteError(new ErrorRecord( new ArgumentException("Table name not valid"), "TableNameNotValid", ErrorCategory.InvalidArgument, tableName)); return false; } // TableNameIsValid /// /// Checks to see if the specified table is present in the /// database /// /// Name of the table to check /// true, if table is present, false otherwise private bool TableIsPresent(string tableName) { // using ODBC connection to the database and get the schema of tables AccessDBPSDriveInfo di = this.PSDriveInfo as AccessDBPSDriveInfo; if (di == null) { return false; } OdbcConnection connection = di.Connection; DataTable dt = connection.GetSchema("Tables"); // check if the specified tableName is available // in the list of tables present in the database foreach (DataRow dr in dt.Rows) { string name = dr["TABLE_NAME"] as string; if (name.Equals(tableName, StringComparison.OrdinalIgnoreCase)) { return true; } } WriteError(new ErrorRecord( new ArgumentException("Specified Table is not present in database"), "TableNotAvailable", ErrorCategory.InvalidArgument, tableName)); return false; }// TableIsPresent #endregion Helper Methods #region Private Properties private string pathSeparator = "\\"; private static string pattern = @"^[a-z]+[0-9]*_*$"; private enum PathType { Database, Table, Row, Invalid }; #endregion Private Properties } #endregion AccessDBProvider #region Helper Classes #region AccessDBPSDriveInfo /// /// Any state associated with the drive should be held here. /// In this case, it's the connection to the database. /// internal class AccessDBPSDriveInfo : PSDriveInfo { private OdbcConnection connection; /// /// ODBC connection information. /// public OdbcConnection Connection { get { return connection; } set { connection = value; } } /// /// Constructor that takes one argument /// /// Drive provided by this provider public AccessDBPSDriveInfo(PSDriveInfo driveInfo) : base(driveInfo) { } } // class AccessDBPSDriveInfo #endregion AccessDBPSDriveInfo #region DatabaseTableInfo /// /// Contains information specific to the database table. /// Similar to the DirectoryInfo class. /// public class DatabaseTableInfo { /// /// Row from the "tables" schema /// public DataRow Data { get { return data; } set { data = value; } } private DataRow data; /// /// The table name. /// public string Name { get { return name; } set { name = value; } } private String name; /// /// The number of rows in the table. /// public int RowCount { get { return rowCount; } set { rowCount = value; } } private int rowCount; /// /// The column definitions for the table. /// public DataColumnCollection Columns { get { return columns; } set { columns = value; } } private DataColumnCollection columns; /// /// Constructor. /// /// The row definition. /// The table name. /// The number of rows in the table. /// Information on the column tables. public DatabaseTableInfo(DataRow row, string name, int rowCount, DataColumnCollection columns) { Name = name; Data = row; RowCount = rowCount; Columns = columns; } // DatabaseTableInfo } // class DatabaseTableInfo #endregion DatabaseTableInfo #region DatabaseRowInfo /// /// Contains information specific to an individual table row. /// Analogous to the FileInfo class. /// public class DatabaseRowInfo { /// /// Row data information. /// public DataRow Data { get { return data; } set { data = value; } } private DataRow data; /// /// The row index. /// public string RowNumber { get { return rowNumber; } set { rowNumber = value; } } private string rowNumber; /// /// Constructor. /// /// The row information. /// The row index. public DatabaseRowInfo(DataRow row, string name) { RowNumber = name; Data = row; } // DatabaseRowInfo } // class DatabaseRowInfo #endregion DatabaseRowInfo #endregion Helper Classes }