diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/Interface/ISearch.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/Interface/ISearch.cs new file mode 100644 index 0000000000..b0722a8a72 --- /dev/null +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/Interface/ISearch.cs @@ -0,0 +1,10 @@ +using Microsoft.Plugin.Indexer.SearchHelper; +using System.Collections.Generic; + +namespace Microsoft.Plugin.Indexer.Interface +{ + public interface ISearch + { + List Query(string connectionString, string sqlQuery); + } +} diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/Main.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/Main.cs index 97196f95c8..b010e866d3 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/Main.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/Main.cs @@ -30,7 +30,7 @@ namespace Microsoft.Plugin.Indexer private PluginJsonStorage _storage; // To access Windows Search functionalities - private readonly WindowsSearchAPI _api = new WindowsSearchAPI(); + private readonly WindowsSearchAPI _api = new WindowsSearchAPI(new OleDBSearch()); // Reserved keywords in oleDB private string ReservedStringPattern = @"^[\/\\\$\%]+$"; diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/SearchHelper/OleDBResult.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/SearchHelper/OleDBResult.cs new file mode 100644 index 0000000000..6c61bcca8b --- /dev/null +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/SearchHelper/OleDBResult.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Data.OleDb; +using System.Text; + +namespace Microsoft.Plugin.Indexer.SearchHelper +{ + public class OleDBResult + { + public List fieldData; + + public OleDBResult(List fieldData) + { + this.fieldData = fieldData; + } + } +} diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/SearchHelper/OleDBSearch.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/SearchHelper/OleDBSearch.cs new file mode 100644 index 0000000000..7903f4b4a6 --- /dev/null +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/SearchHelper/OleDBSearch.cs @@ -0,0 +1,80 @@ +using Microsoft.Plugin.Indexer.Interface; +using System; +using System.Collections.Generic; +using System.Data.OleDb; + +namespace Microsoft.Plugin.Indexer.SearchHelper +{ + public class OleDBSearch : ISearch + { + private OleDbCommand command; + private OleDbConnection conn; + private OleDbDataReader WDSResults; + + public List Query(string connectionString, string sqlQuery) + { + List result = new List(); + + using (conn = new OleDbConnection(connectionString)) + { + // open the connection + conn.Open(); + + // now create an OleDB command object with the query we built above and the connection we just opened. + using (command = new OleDbCommand(sqlQuery, conn)) + { + using (WDSResults = command.ExecuteReader()) + { + if (WDSResults.HasRows) + { + while (WDSResults.Read()) + { + List fieldData = new List(); + for (int i = 0; i < WDSResults.FieldCount; i++) + { + fieldData.Add(WDSResults.GetValue(i)); + } + result.Add(new OleDBResult(fieldData)); + } + } + } + } + } + + return result; + } + + /// Checks if all the variables related to database connection have been properly disposed + public bool HaveAllDisposableItemsBeenDisposed() + { + bool commandDisposed = false; + bool connDisposed = false; + bool resultDisposed = false; + + try + { + command.ExecuteReader(); + } + catch (InvalidOperationException) + { + commandDisposed = true; + } + + try + { + WDSResults.Read(); + } + catch (InvalidOperationException) + { + resultDisposed = true; + } + + if(conn.State == System.Data.ConnectionState.Closed) + { + connDisposed = true; + } + + return commandDisposed && resultDisposed && connDisposed; + } + } +} diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/SearchHelper/WindowsSearchAPI.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/SearchHelper/WindowsSearchAPI.cs index dc8da1ab60..c3c82fd28e 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/SearchHelper/WindowsSearchAPI.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Indexer/SearchHelper/WindowsSearchAPI.cs @@ -1,55 +1,55 @@ using System; using System.Collections.Generic; -using System.Data.OleDb; +using Microsoft.Plugin.Indexer.Interface; using Microsoft.Search.Interop; namespace Microsoft.Plugin.Indexer.SearchHelper { public class WindowsSearchAPI { - public OleDbConnection conn; - public OleDbCommand command; - public OleDbDataReader WDSResults; + public bool DisplayHiddenFiles { get; set; } + + private readonly ISearch WindowsIndexerSearch; private readonly object _lock = new object(); - + private readonly UInt32 FILE_ATTRIBUTE_HIDDEN = 0x2; + + public WindowsSearchAPI(ISearch windowsIndexerSearch, bool displayHiddenFiles = false) + { + this.WindowsIndexerSearch = windowsIndexerSearch; + this.DisplayHiddenFiles = displayHiddenFiles; + } public List ExecuteQuery(ISearchQueryHelper queryHelper, string keyword) { List _Result = new List(); + // Generate SQL from our parameters, converting the userQuery from AQS->WHERE clause string sqlQuery = queryHelper.GenerateSQLFromUserQuery(keyword); - // --- Perform the query --- - // create an OleDbConnection object which connects to the indexer provider with the windows application - using (conn = new OleDbConnection(queryHelper.ConnectionString)) - { - // open the connection - conn.Open(); + // execute the command, which returns the results as an OleDBResults. + List oleDBResults = WindowsIndexerSearch.Query(queryHelper.ConnectionString, sqlQuery); - // now create an OleDB command object with the query we built above and the connection we just opened. - using (command = new OleDbCommand(sqlQuery, conn)) + // Loop over all records from the database + foreach(OleDBResult oleDBResult in oleDBResults) + { + if (oleDBResult.fieldData[0] == DBNull.Value || oleDBResult.fieldData[1] == DBNull.Value || oleDBResult.fieldData[2] == DBNull.Value) { - // execute the command, which returns the results as an OleDbDataReader. - using (WDSResults = command.ExecuteReader()) - { - if(WDSResults.HasRows) - { - while (WDSResults.Read()) - { - if (WDSResults.GetValue(0) != DBNull.Value && WDSResults.GetValue(1) != DBNull.Value) - { - var uri_path = new Uri(WDSResults.GetString(0)); - var result = new SearchResult - { - Path = uri_path.LocalPath, - Title = WDSResults.GetString(1) - }; - _Result.Add(result); - } - } - } - } + continue; } + + UInt32 fileAttributes = (UInt32)((Int64)oleDBResult.fieldData[2]); + bool isFileHidden = (fileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN; + + if (DisplayHiddenFiles || !isFileHidden) + { + var uri_path = new Uri((string)oleDBResult.fieldData[0]); + var result = new SearchResult + { + Path = uri_path.LocalPath, + Title = (string)oleDBResult.fieldData[1] + }; + _Result.Add(result); + } } return _Result; @@ -91,7 +91,7 @@ namespace Microsoft.Plugin.Indexer.SearchHelper queryHelper.QueryMaxResults = maxCount; // Set list of columns we want to display, getting the path presently - queryHelper.QuerySelectColumns = "System.ItemUrl, System.FileName"; + queryHelper.QuerySelectColumns = "System.ItemUrl, System.FileName, System.FileAttributes"; // Set additional query restriction queryHelper.QueryWhereRestrictions = "AND scope='file:'"; diff --git a/src/modules/launcher/Wox.Test/Plugins/WindowsIndexerTest.cs b/src/modules/launcher/Wox.Test/Plugins/WindowsIndexerTest.cs index b3677629c4..ded997c628 100644 --- a/src/modules/launcher/Wox.Test/Plugins/WindowsIndexerTest.cs +++ b/src/modules/launcher/Wox.Test/Plugins/WindowsIndexerTest.cs @@ -4,6 +4,9 @@ using System.Collections.Generic; using System.Data.OleDb; using Microsoft.Search.Interop; using Microsoft.Plugin.Indexer.SearchHelper; +using Microsoft.Plugin.Indexer.Interface; +using Moq; +using System.Linq; namespace Wox.Test.Plugins { @@ -11,13 +14,20 @@ namespace Wox.Test.Plugins [TestFixture] public class WindowsIndexerTest { - private WindowsSearchAPI _api = new WindowsSearchAPI(); + + public WindowsSearchAPI GetWindowsSearchAPI() + { + var mock = new Mock(); + mock.Setup(x => x.Query("dummy-connection-string", "dummy-query")).Returns(new List()); + return new WindowsSearchAPI(mock.Object); + } [Test] public void InitQueryHelper_ShouldInitialize_WhenFunctionIsCalled() { // Arrange int maxCount = 10; + WindowsSearchAPI _api = GetWindowsSearchAPI(); ISearchQueryHelper queryHelper = null; // Act @@ -34,6 +44,7 @@ namespace Wox.Test.Plugins // Arrange ISearchQueryHelper queryHelper; String pattern = "*"; + WindowsSearchAPI _api = GetWindowsSearchAPI(); _api.InitQueryHelper(out queryHelper, 10); // Act @@ -50,6 +61,7 @@ namespace Wox.Test.Plugins // Arrange ISearchQueryHelper queryHelper; String pattern = "tt*^&)"; + WindowsSearchAPI _api = GetWindowsSearchAPI(); _api.InitQueryHelper(out queryHelper, 10); // Act @@ -66,6 +78,7 @@ namespace Wox.Test.Plugins // Arrange ISearchQueryHelper queryHelper; String pattern = "tt%^&)"; + WindowsSearchAPI _api = GetWindowsSearchAPI(); _api.InitQueryHelper(out queryHelper, 10); // Act @@ -82,6 +95,7 @@ namespace Wox.Test.Plugins // Arrange ISearchQueryHelper queryHelper; String pattern = "tt_^&)"; + WindowsSearchAPI _api = GetWindowsSearchAPI(); _api.InitQueryHelper(out queryHelper, 10); // Act @@ -98,6 +112,7 @@ namespace Wox.Test.Plugins // Arrange ISearchQueryHelper queryHelper; String pattern = "tt?^&)"; + WindowsSearchAPI _api = GetWindowsSearchAPI(); _api.InitQueryHelper(out queryHelper, 10); // Act @@ -114,6 +129,7 @@ namespace Wox.Test.Plugins // Arrange ISearchQueryHelper queryHelper; String pattern = "tt^&)bc"; + WindowsSearchAPI _api = GetWindowsSearchAPI(); _api.InitQueryHelper(out queryHelper, 10); // Act @@ -128,37 +144,75 @@ namespace Wox.Test.Plugins public void ExecuteQuery_ShouldDisposeAllConnections_AfterFunctionCall() { // Arrange - ISearchQueryHelper queryHelper; - _api.InitQueryHelper(out queryHelper, 10); - _api.ModifyQueryHelper(ref queryHelper, "*"); - string keyword = "test"; - bool commandDisposed = false; - bool resultDisposed = false; + OleDBSearch oleDbSearch = new OleDBSearch(); + WindowsSearchAPI _api = new WindowsSearchAPI(oleDbSearch); // Act - _api.ExecuteQuery(queryHelper, keyword); - try - { - _api.command.ExecuteReader(); - } - catch(InvalidOperationException) - { - commandDisposed = true; - } - - try - { - _api.WDSResults.Read(); - } - catch(InvalidOperationException) - { - resultDisposed = true; - } + _api.Search("FilePath"); // Assert - Assert.IsTrue(_api.conn.State == System.Data.ConnectionState.Closed); - Assert.IsTrue(commandDisposed); - Assert.IsTrue(resultDisposed); + Assert.IsTrue(oleDbSearch.HaveAllDisposableItemsBeenDisposed()); + } + + [Test] + public void WindowsSearchAPI_ShouldShowHiddenFiles_WhenDisplayHiddenFilesIsTrue() + { + // Arrange + OleDBResult unHiddenFile = new OleDBResult(new List() { "C:/test/path/file1.txt", "file1.txt", (Int64)0x0 }); + OleDBResult hiddenFile = new OleDBResult(new List() { "C:/test/path/file2.txt", "file2.txt", (Int64)0x2 }); + List results = new List() { hiddenFile, unHiddenFile }; + var mock = new Mock(); + mock.Setup(x => x.Query(It.IsAny(), It.IsAny())).Returns(results); + WindowsSearchAPI _api = new WindowsSearchAPI(mock.Object, true); + + // Act + var windowsSearchAPIResults = _api.Search("FilePath"); + + // Assert + Assert.IsTrue(windowsSearchAPIResults.Count() == 2); + Assert.IsTrue(windowsSearchAPIResults.Any(x => x.Title == "file1.txt")); + Assert.IsTrue(windowsSearchAPIResults.Any(x => x.Title == "file2.txt")); + } + + [Test] + public void WindowsSearchAPI_ShouldNotShowHiddenFiles_WhenDisplayHiddenFilesIsFalse() + { + // Arrange + OleDBResult unHiddenFile = new OleDBResult(new List() { "C:/test/path/file1.txt", "file1.txt", (Int64)0x0 }); + OleDBResult hiddenFile = new OleDBResult(new List() { "C:/test/path/file2.txt", "file2.txt", (Int64)0x2 }); + List results = new List() { hiddenFile, unHiddenFile }; + var mock = new Mock(); + mock.Setup(x => x.Query(It.IsAny(), It.IsAny())).Returns(results); + WindowsSearchAPI _api = new WindowsSearchAPI(mock.Object, false); + + // Act + var windowsSearchAPIResults = _api.Search("FilePath"); + + // Assert + Assert.IsTrue(windowsSearchAPIResults.Count() == 1); + Assert.IsTrue(windowsSearchAPIResults.Any(x => x.Title == "file1.txt")); + Assert.IsFalse(windowsSearchAPIResults.Any(x => x.Title == "file2.txt")); + } + + [Test] + public void WindowsSearchAPI_ShouldNotReturnResultsWithNullValue_WhenDbResultHasANullColumn() + { + // Arrange + OleDBResult file1 = new OleDBResult(new List() { "C:/test/path/file1.txt", DBNull.Value, (Int64)0x0 }); + OleDBResult file2 = new OleDBResult(new List() { "C:/test/path/file2.txt", "file2.txt", (Int64)0x0 }); + + List results = new List() { file1, file2 }; + var mock = new Mock(); + mock.Setup(x => x.Query(It.IsAny(), It.IsAny())).Returns(results); + WindowsSearchAPI _api = new WindowsSearchAPI(mock.Object, false); + + // Act + var windowsSearchAPIResults = _api.Search("FilePath"); + + // Assert + Assert.IsTrue(windowsSearchAPIResults.Count() == 1); + Assert.IsFalse(windowsSearchAPIResults.Any(x => x.Title == "file1.txt")); + Assert.IsTrue(windowsSearchAPIResults.Any(x => x.Title == "file2.txt")); } } }