diff --git a/.gitignore b/.gitignore index 1b71e0c..3a788d7 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,30 @@ /PropertyHandler/x64/Release /x64/Release /Setup/obj/x64/Release +/TestDriver/TestDriver.suo +/AssociationManager/bin/Debug +/CommandLine/Debug +/CommandLineAssociationManager/bin/Debug +/ContextMenuHandler/Debug +/Debug +/PropertyHandler/Debug +/Setup/bin +/Setup/obj/x86/Debug +/Setup/msi/Debug +/AssociationManager/bin/Release +/CommandLine/Release +/CommandLineAssociationManager/bin/Release +/ContextMenuHandler/Release +/PropertyHandler/Release +/Release +/Setup/msi/Release +/Setup/obj/x86/Release +/CommandLine/FileMeta.aps +/ContextMenuHandler/ContextMenuHandler.aps +/PropertyHandler/FileMetaPropertyHandler.aps +/TestDriverAssoc/.vs/TestDriverAssoc/v15 +/TestSandbox +/TestDriver/bin/Debug +/TestDriverAssoc/bin/Debug +/TestSandbox32 +/CommandLineAssociationManager/CommandLineAssociationManager.csproj.user diff --git a/AssociationManager/Extension.cs b/AssociationManager/Extension.cs index b458d6f..26dcd1d 100644 --- a/AssociationManager/Extension.cs +++ b/AssociationManager/Extension.cs @@ -19,16 +19,20 @@ public enum HandlerState Ours, Foreign, Chained, + ProfileOnly, } public class Extension : INotifyPropertyChanged { const string OurPropertyHandlerTitle = "File Meta Property Handler"; const string OurPropertyHandlerPrefix = "File Meta + "; + const string OurProfilePrefix = "Profile + "; const string OurPropertyHandlerGuid64 = "{D06391EE-2FEB-419B-9667-AD160D0849F3}"; const string OurPropertyHandlerGuid32 = "{60211757-EF87-465e-B6C1-B37CF98295F9}"; const string OurContextHandlerGuid64 = "{28D14D00-2D80-4956-9657-9D50C8BB47A5}"; const string OurContextHandlerGuid32 = "{DA38301B-BE91-4397-B2C8-E27A0BD80CC5}"; + const string OurExportContextHandlerGuid64 = "{DE4C4CAF-C564-4EEA-9FF7-C46FB8023818}"; + const string OurExportContextHandlerGuid32 = "{5A677F18-527D-42B3-BAA0-9785D3A8256F}"; const string FullDetailsValueName = "FullDetails"; const string PreviewDetailsValueName = "PreviewDetails"; @@ -67,6 +71,9 @@ public FontWeight Weight case HandlerState.Chained: return FontWeights.ExtraBold; + case HandlerState.ProfileOnly: + return FontWeights.Bold; + case HandlerState.Foreign: case HandlerState.None: default: @@ -91,6 +98,9 @@ public string PropertyHandlerDisplay case HandlerState.Chained: return OurPropertyHandlerPrefix + (PropertyHandlerTitle != null ? PropertyHandlerTitle : PropertyHandlerGuid); + case HandlerState.ProfileOnly: + return OurProfilePrefix + (PropertyHandlerTitle != null ? PropertyHandlerTitle : PropertyHandlerGuid); + case HandlerState.None: default: return LocalizedMessages.PropertyHandlerNone; @@ -111,9 +121,11 @@ public static bool IsElevated #if x64 private static string OurPropertyHandlerGuid { get { return OurPropertyHandlerGuid64; } } private static string OurContextHandlerGuid { get { return OurContextHandlerGuid64; } } + private static string OurExportContextHandlerGuid { get { return OurExportContextHandlerGuid64; } } #elif x86 private static string OurPropertyHandlerGuid { get { return OurPropertyHandlerGuid32; } } private static string OurContextHandlerGuid { get { return OurContextHandlerGuid32; } } + private static string OurExportContextHandlerGuid { get { return OurExportContextHandlerGuid32; } } #endif public static bool IsOurPropertyHandlerRegistered @@ -151,21 +163,26 @@ public void RecordPropertyHandler(string handlerGuid, string handlerChainedGuid) if (handlerGuid == OurPropertyHandlerGuid) { if (handlerChainedGuid != null) + { RecordPropertyHandler(HandlerState.Chained, handlerChainedGuid, GetHandlerTitle(handlerChainedGuid)); + Profile = GetCurrentProfileIfKnown(); + } else + { RecordPropertyHandler(HandlerState.Ours, null, null); + Profile = GetCurrentProfileIfKnown(); + } } else if (handlerGuid != null) - RecordPropertyHandler(HandlerState.Foreign, handlerGuid, GetHandlerTitle(handlerGuid)); + { + Profile = GetCurrentProfileIfKnown(true); + RecordPropertyHandler(Profile == null ? HandlerState.Foreign : HandlerState.ProfileOnly, + handlerGuid, GetHandlerTitle(handlerGuid)); + } else RecordPropertyHandler(HandlerState.None, null, null); } - - public void IdentifyCurrentProfile() - { - Profile = GetCurrentProfileIfKnown(); - } - + public Profile GetDefaultCustomProfile() { var p = new Profile(); @@ -226,14 +243,20 @@ public void SetupHandlerForExtension(Profile selectedProfile, bool createMergedP } // Find the key for the extension in HKEY_CLASSES_ROOT - using (RegistryKey target = GetHKCRProfileKey(true)) + if (PropertyHandlerState == HandlerState.Foreign) { // We used to place entries on this key, but no longer do, because such keys can be shared, // and we only want to affect a specific extension // We still have to hide any existing entries, because otherwise they would take priority, but fortunately they do not often occur // If there are entries, and we are merging, we read them into the new profile, in case this is the only place they occur - if (PropertyHandlerState == HandlerState.Foreign) - GetAndHidePreExistingProgidRegistryEntries(target, createMergedProfile ? profile : null); + bool exist; + // Use read-only access to test for existence + using (RegistryKey target = GetHKCRProfileKey(false)) + { exist = PreExistingProgidRegistryEntries(target); } + + if (exist) + using (RegistryKey target = GetHKCRProfileKey(true)) + { GetAndHidePreExistingProgidRegistryEntries(target, createMergedProfile ? profile : null); } } // Now we only update the extension specific area @@ -252,15 +275,53 @@ public void SetupHandlerForExtension(Profile selectedProfile, bool createMergedP GetAndHidePreExistingProgidRegistryEntries(target, null); } - SetupProgidRegistryEntries(target, profile); + SetupProfileDetailEntries(target, profile); + SetupContextMenuRegistryEntries(target); } if (PropertyHandlerState == HandlerState.Foreign) { // Write the updated custom profile information back to the store State.StoreUpdatedProfile(profile); - } + } + + AddPropertyHandler(); + + // Test to see if our handler is actually being used, or if this is one of those cases where + // Windows ignores the registry setting when providing the property handler + if (PropertyHandlerState == HandlerState.Chained && + PropertyHandlerTest.WindowsIgnoresOurPropertyHandler(Name)) + { + // Rollback our property handler + RemovePropertyHandler(); + + // Switch to the export only context menu + SetupExportContextMenu(); + // Update the property handler state + this.RecordPropertyHandler(HandlerState.ProfileOnly, PropertyHandlerGuid, PropertyHandlerTitle); + } + + this.Profile = profile; + + State.HasChanged = true; + } + + // Temporary code to support MainWindow.CheckForBlockedExtensions() + //public bool IsPropertyHandlerBlocked() + //{ + // bool result = false; + // if (PropertyHandlerState == HandlerState.Foreign) + // { + // AddPropertyHandler(); + // result = PropertyHandlerTest.WindowsIgnoresOurPropertyHandler(Name); + // RemovePropertyHandler(); + // } + // return result; + //} + + private void AddPropertyHandler() + { #if x64 // On 64-bit machines, set up the 32-bit property handler, so that 32-bit applications can also access our properties using (RegistryKey handlers = RegistryExtensions.OpenBaseKey(RegistryHive.LocalMachine, RegistryExtensions.RegistryHiveType.X86). @@ -270,7 +331,7 @@ public void SetupHandlerForExtension(Profile selectedProfile, bool createMergedP { if (PropertyHandlerState == HandlerState.None) { - var temp = handler.GetValue(null); + var temp = handler.GetValue(null); // In the case where there is no 64-bit handler, but there is a 32-bit handler, leave it alone if (temp == null) handler.SetValue(null, OurPropertyHandlerGuid32); @@ -284,7 +345,7 @@ public void SetupHandlerForExtension(Profile selectedProfile, bool createMergedP } } } -#endif +#endif // Now, add the main handler extension key, which is 32- or 64-bit, depending on how we were built // The 32-bit and 64-bit values of these are separate and isolated on 64-bit Windows, // the 32-bit value being under SOFTWARE\Wow6432Node. Thus a 64-bit manager is needed to set up a 64-bit handler @@ -305,10 +366,6 @@ public void SetupHandlerForExtension(Profile selectedProfile, bool createMergedP } } } - - this.Profile = profile; - - State.HasChanged = true; } // Update the registry settings for an extension when the Full details or Preview details in a profile are changed @@ -322,7 +379,9 @@ public void UpdateProfileSettingsForExtension(Profile profile) ErrorCode = WindowsErrorCodes.ERROR_ACCESS_DENIED }; - if (PropertyHandlerState != HandlerState.Ours && PropertyHandlerState != HandlerState.Chained) + if (PropertyHandlerState != HandlerState.Ours && + PropertyHandlerState != HandlerState.Chained && + PropertyHandlerState != HandlerState.ProfileOnly) return; using (RegistryKey target = GetSystemFileAssociationsProfileKey(true)) @@ -352,6 +411,16 @@ private bool GetExistingProgidRegistryEntries(RegistryKey target, Profile profil return val != null; } + private bool PreExistingProgidRegistryEntries(RegistryKey target) + { + if (target == null) + return false; + + return (target.GetValue(FullDetailsValueName) != null || + target.GetValue(InfoTipValueName) != null || + target.GetValue(PreviewDetailsValueName) != null); + } + // If the caller does not need the existing values, it should pass null for the profile private void GetAndHidePreExistingProgidRegistryEntries(RegistryKey target, Profile profile) { @@ -387,10 +456,8 @@ private void GetAndHidePreExistingProgidRegistryEntries(RegistryKey target, Prof } } - private void SetupProgidRegistryEntries(RegistryKey target, Profile profile) + private void SetupContextMenuRegistryEntries(RegistryKey target, bool export = false) { - SetupProfileDetailEntries(target, profile); - // Set up the eontext handler, if registered if (IsOurContextHandlerRegistered) { @@ -400,7 +467,7 @@ private void SetupProgidRegistryEntries(RegistryKey target, Profile profile) { using (RegistryKey keyHandler = keyCMH.CreateSubKey(ContextHandlerKeyName)) { - keyHandler.SetValue(null, OurContextHandlerGuid); + keyHandler.SetValue(null, export ? OurExportContextHandlerGuid : OurContextHandlerGuid); } } } @@ -419,14 +486,13 @@ private void SetupProfileDetailEntries(RegistryKey target, Profile profile) target.SetValue(FileMetaCustomProfileValueName, profile.Name); } - private Profile GetCurrentProfileIfKnown() + private Profile GetCurrentProfileIfKnown(bool keyOnly = false) { - if (PropertyHandlerState != HandlerState.Ours && PropertyHandlerState != HandlerState.Chained) - return null; - // Find the key for the extension string pd; + string opd; string cp; + using (RegistryKey target = GetSystemFileAssociationsProfileKey(false)) { if (target == null) @@ -434,6 +500,7 @@ private Profile GetCurrentProfileIfKnown() // Try to identify the profile pd = (string)target.GetValue(PreviewDetailsValueName); + opd = (string)target.GetValue(OldPreviewDetailsValueName); cp = (string)target.GetValue(FileMetaCustomProfileValueName); } @@ -441,8 +508,8 @@ private Profile GetCurrentProfileIfKnown() if (cp != null) return State.CustomProfiles.Where(p => p.Name == cp).FirstOrDefault(); - // Otherwise, tried to match the preview details against one of the built-in values - else if (pd != null) + // Otherwise, try to match the preview details against one of the built-in values + else if ((!keyOnly || opd != null) && pd != null) return State.BuiltInProfiles.Where(p => p.PreviewDetailsString == pd).FirstOrDefault(); return null; @@ -477,7 +544,7 @@ public bool IsRefreshRequired() return true; } } -#endif +#endif return false; } @@ -491,17 +558,42 @@ public void RemoveHandlerFromExtension() ErrorCode = WindowsErrorCodes.ERROR_ACCESS_DENIED }; - if (PropertyHandlerState != HandlerState.Ours && PropertyHandlerState != HandlerState.Chained) + if (PropertyHandlerState != HandlerState.Ours && PropertyHandlerState != HandlerState.Chained && + PropertyHandlerState != HandlerState.ProfileOnly) return; - // Now find the key for the extension in HKEY_CLASSES_ROOT - using (RegistryKey target = GetHKCRProfileKey(true)) + // Now find the key for the extension in HKEY_CLASSES_ROOT + bool exist; + // Use read-only access to test for existence + using (RegistryKey target = GetHKCRProfileKey(false)) { - // Tolerate the case where the extension has been removed since we set up a handler for it - // We still do this even though no longer write entries here so as to be sure to clean up after earlier versions, - // and to restore pre-existing entries if we had to hide them - if (target != null) - RemoveProgidRegistryEntries(target); + exist = HiddenProgidRegistryEntries(target); + } + + // Cleanup required for hidden cases, and for some old configurations + if (exist || PropertyHandlerState == HandlerState.Ours) + { + try + { + using (RegistryKey target = GetHKCRProfileKey(true)) + { + // Tolerate the case where the extension has been removed since we set up a handler for it + // We still do this even though no longer write entries here so as to be sure to clean up after earlier versions, + // and to restore pre-existing entries if we had to hide them + if (target != null) + { + RemoveProfileDetailRegistryEntries(target); + RemoveContextMenuRegistryEntries(target); + } + } + } + catch (UnauthorizedAccessException ex) + { + // Tolerate actors problems if it is our handler. We need to try and get write access + // to clean up settings from older versions, but this fails some extensions like .chm + if (PropertyHandlerState != HandlerState.Ours) + throw ex; + } } // Now go after the settings in SystemFileAssociations @@ -509,9 +601,34 @@ public void RemoveHandlerFromExtension() using (RegistryKey target = GetSystemFileAssociationsProfileKey(true)) { if (target != null) - RemoveProgidRegistryEntries(target); + { + RemoveProfileDetailRegistryEntries(target); + RemoveContextMenuRegistryEntries(target); + } } + if (PropertyHandlerState != HandlerState.ProfileOnly) + RemovePropertyHandler(); + else + this.RecordPropertyHandler(HandlerState.Foreign, PropertyHandlerGuid, PropertyHandlerTitle); + + this.Profile = null; + State.HasChanged = true; + } + + private void SetupExportContextMenu() + { + using (RegistryKey target = GetSystemFileAssociationsProfileKey(true)) + { + if (target != null) + { + SetupContextMenuRegistryEntries(target, true); + } + } + } + + private void RemovePropertyHandler() + { #if x64 // On 64-bit machines, remove the 32-bit property handler, as we set up both using (RegistryKey handlers = RegistryExtensions.OpenBaseKey(RegistryHive.LocalMachine, RegistryExtensions.RegistryHiveType.X86). @@ -540,7 +657,7 @@ public void RemoveHandlerFromExtension() if (delete) handlers.DeleteSubKey(Name); } -#endif +#endif // Now, remove the main handler extension key, which is 32- or 64-bit, depending on how we were built // The 32-bit and 64-bit values of these are separate and isolated on 64-bit Windows, // the 32-bit value being under SOFTWARE\Wow6432Node. Thus a 64-bit manager is needed to set up a 64-bit handler @@ -550,23 +667,20 @@ public void RemoveHandlerFromExtension() { handlers.DeleteSubKey(Name); this.RecordPropertyHandler(HandlerState.None, null, null); - this.Profile = null; } - else // Chained + else if (PropertyHandlerState == HandlerState.Chained) { using (RegistryKey handler = handlers.OpenSubKey(Name, true)) { handler.SetValue(null, PropertyHandlerGuid); handler.DeleteValue(ChainedValueName); this.RecordPropertyHandler(HandlerState.Foreign, PropertyHandlerGuid, PropertyHandlerTitle); - this.Profile = null; } } } - State.HasChanged = true; } - private void RemoveProgidRegistryEntries(RegistryKey target) + private void RemoveProfileDetailRegistryEntries(RegistryKey target) { target.DeleteValue(FullDetailsValueName, false); target.DeleteValue(InfoTipValueName, false); @@ -574,7 +688,10 @@ private void RemoveProgidRegistryEntries(RegistryKey target) target.DeleteValue(FileMetaCustomProfileValueName, false); UnhidePreExistingProgidRegistryEntries(target); + } + private void RemoveContextMenuRegistryEntries(RegistryKey target) + { // Always have a go at removing the context handler setup, even if the handler is not registered // There might be entries lying around from when it was using (RegistryKey keyShellEx = target.OpenSubKey(ShellExKeyName, true)) @@ -609,6 +726,16 @@ private void RemoveProgidRegistryEntries(RegistryKey target) target.Close(); } + private bool HiddenProgidRegistryEntries(RegistryKey target) + { + if (target == null) + return false; + + return (target.GetValue(OldFullDetailsValueName) != null || + target.GetValue(OldInfoTipValueName) != null || + target.GetValue(OldPreviewDetailsValueName) != null); + } + private void UnhidePreExistingProgidRegistryEntries(RegistryKey target) { if (target == null) @@ -729,7 +856,7 @@ private string GetHandlerTitle(string handlerGuid) return handlerTitle; } - + public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(String info) @@ -740,4 +867,5 @@ private void OnPropertyChanged(String info) } } } + } diff --git a/AssociationManager/FileMetaAssociationManager.csproj b/AssociationManager/FileMetaAssociationManager.csproj index 295f048..335292a 100644 --- a/AssociationManager/FileMetaAssociationManager.csproj +++ b/AssociationManager/FileMetaAssociationManager.csproj @@ -111,6 +111,7 @@ + diff --git a/AssociationManager/MainView.cs b/AssociationManager/MainView.cs index 076a811..68a60ea 100644 --- a/AssociationManager/MainView.cs +++ b/AssociationManager/MainView.cs @@ -164,6 +164,10 @@ public void AddHandlers() foreach (Extension ext in SelectedExtensions) { ext.SetupHandlerForExtension(SelectedProfile, true); + + if (ext.PropertyHandlerState == HandlerState.ProfileOnly) + MessageBox.Show(string.Format(LocalizedMessages.WindowsWontExtendHandler, ext.Name), + LocalizedMessages.SetupHandler); } } @@ -242,7 +246,9 @@ private void DeterminePossibleActions() break; } } - else if (e.PropertyHandlerState == HandlerState.Ours || e.PropertyHandlerState == HandlerState.Chained) + else if (e.PropertyHandlerState == HandlerState.Ours || + e.PropertyHandlerState == HandlerState.Chained || + e.PropertyHandlerState == HandlerState.ProfileOnly) { if (handlersSelected == null) handlersSelected = HandlerSet.Ours; diff --git a/AssociationManager/MainWindow.xaml.cs b/AssociationManager/MainWindow.xaml.cs index 41f3220..554f046 100644 --- a/AssociationManager/MainWindow.xaml.cs +++ b/AssociationManager/MainWindow.xaml.cs @@ -147,8 +147,32 @@ private void profiles_Click(object sender, RoutedEventArgs e) view.SelectedProfile = state.SelectedProfile; } + // Temporary code to enumerate extensions for which Add Handler fails + //private void CheckForBlockedExtensions() + //{ + // using (var w = new System.IO.StreamWriter(@"D:\TestProperties\Results\Extensions.csv")) + // { + // foreach (var ext in state.Extensions.Where(x => x.PropertyHandlerState == HandlerState.Foreign)) + // { + // string result = "Exception"; + // try + // { + // result = ext.IsPropertyHandlerBlocked() ? "Blocked" : "Ok"; + // } + // catch (System.Exception ex) + // { + // } + // w.WriteLine(String.Format("{0}, {1}, {2}", ext.Name, result, ext.PropertyHandlerDisplay)); + // } + // } + //} + private void restartExplorer_Click(object sender, RoutedEventArgs e) { + // Temporary code to enumerate extensions for which Add Handler fails + //CheckForBlockedExtensions(); + //return; + bool failed = false; try { diff --git a/AssociationManager/PropertyHandlerTest.cs b/AssociationManager/PropertyHandlerTest.cs new file mode 100644 index 0000000..b63ed78 --- /dev/null +++ b/AssociationManager/PropertyHandlerTest.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace FileMetadataAssociationManager +{ + static class PropertyHandlerTest + { + private static Guid IPropertyStoreGuid = new Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"); + + public static bool WindowsIgnoresOurPropertyHandler(string extension) + { + // Create a temporary file with the right extension + string fileFullName = Path.ChangeExtension( + Path.GetTempPath() + Guid.NewGuid().ToString(), extension); + using (var fs = File.Create(fileFullName)) { } + + // Get the property store that Explorer would use + IPropertyStore ps; + bool result = true; + HResult hr = (HResult)SHGetPropertyStoreFromParsingName(fileFullName, IntPtr.Zero, + GETPROPERTYSTOREFLAGS.GPS_NO_OPLOCK | GETPROPERTYSTOREFLAGS.GPS_HANDLERPROPERTIESONLY, ref IPropertyStoreGuid, out ps); + if (hr == HResult.Ok) + { + // Look for the signature property value that marks the handler as ours + PropertyKey key; + PropVariant pv = new PropVariant(); + PropertySystemNativeMethods.PSGetPropertyKeyFromName("System.Software.ProductName", out key); + hr = ps.GetValue(key, pv); + if (hr == HResult.Ok && !pv.IsNullOrEmpty) + { + if ((pv.Value as string) == "FileMetadata") + result = false; + } + Marshal.ReleaseComObject(ps); // This release frees up the file for deletion + } + + File.Delete(fileFullName); + + return result; + } + + [DllImport("shell32.dll", SetLastError = true)] + public static extern int SHGetPropertyStoreFromParsingName( + [In][MarshalAs(UnmanagedType.LPWStr)] string pszPath, + IntPtr zeroWorks, + GETPROPERTYSTOREFLAGS flags, + ref Guid iIdPropStore, + [Out] out IPropertyStore propertyStore); + } + + public enum GETPROPERTYSTOREFLAGS + { + // If no flags are specified (GPS_DEFAULT), a read-only property store is returned that includes properties for the file or item. + // In the case that the shell item is a file, the property store contains: + // 1. properties about the file from the file system + // 2. properties from the file itself provided by the file's property handler, unless that file is offline, + // see GPS_OPENSLOWITEM + // 3. if requested by the file's property handler and supported by the file system, properties stored in the + // alternate property store. + // + // Non-file shell items should return a similar read-only store + // + // Specifying other GPS_ flags modifies the store that is returned + GPS_DEFAULT = 0x00000000, + GPS_HANDLERPROPERTIESONLY = 0x00000001, // only include properties directly from the file's property handler + GPS_READWRITE = 0x00000002, // Writable stores will only include handler properties + GPS_TEMPORARY = 0x00000004, // A read/write store that only holds properties for the lifetime of the IShellItem object + GPS_FASTPROPERTIESONLY = 0x00000008, // do not include any properties from the file's property handler (because the file's property handler will hit the disk) + GPS_OPENSLOWITEM = 0x00000010, // include properties from a file's property handler, even if it means retrieving the file from offline storage. + GPS_DELAYCREATION = 0x00000020, // delay the creation of the file's property handler until those properties are read, written, or enumerated + GPS_BESTEFFORT = 0x00000040, // For readonly stores, succeed and return all available properties, even if one or more sources of properties fails. Not valid with GPS_READWRITE. + GPS_NO_OPLOCK = 0x00000080, // some data sources protect the read property store with an oplock, this disables that + GPS_MASK_VALID = 0x000000FF, + } + + /// + /// A property store + /// + [ComImport] + [Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + interface IPropertyStore + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + HResult GetCount([Out] out uint propertyCount); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + HResult GetAt([In] uint propertyIndex, out PropertyKey key); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + HResult GetValue([In] ref PropertyKey key, [Out] PropVariant pv); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), PreserveSig] + HResult SetValue([In] ref PropertyKey key, [In] PropVariant pv); + + [PreserveSig] + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + HResult Commit(); + } + + [ComImport] + [Guid("C8E2D566-186E-4D49-BF41-6909EAD56ACC")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + interface IPropertyStoreCapabilities + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + HResult IsPropertyWritable([In]ref PropertyKey propertyKey); + } +} diff --git a/AssociationManager/State.cs b/AssociationManager/State.cs index d58c0ec..2974a31 100644 --- a/AssociationManager/State.cs +++ b/AssociationManager/State.cs @@ -68,7 +68,8 @@ public void Populate(string savedStateFile = null) public void SortExtensions() { - // Sort by file extension, but group by our handler, chained handlers, other handlers, and finally no handler + // Sort by file extension, but group by our handler, chained handlers, profile only, + // other handlers, and finally no handler // This uses a Sort extension to ObservableCollection extensions.Sort((e, f) => { @@ -82,6 +83,10 @@ public void SortExtensions() return -1; else if (f.PropertyHandlerState == HandlerState.Chained) return 1; + else if (e.PropertyHandlerState == HandlerState.ProfileOnly) + return -1; + else if (f.PropertyHandlerState == HandlerState.ProfileOnly) + return 1; else if (e.PropertyHandlerState == HandlerState.Foreign) return -1; else if (f.PropertyHandlerState == HandlerState.Foreign) @@ -409,8 +414,6 @@ private void PopulateExtensions() if (dictExtensions.TryGetValue(name.ToLower(), out e)) { e.RecordPropertyHandler(handlerGuid, handlerChainedGuid); - - e.IdentifyCurrentProfile(); } } } diff --git a/AssociationMessages/LocalizedMessages.Designer.cs b/AssociationMessages/LocalizedMessages.Designer.cs index e0ed0f6..b8a7b67 100644 --- a/AssociationMessages/LocalizedMessages.Designer.cs +++ b/AssociationMessages/LocalizedMessages.Designer.cs @@ -528,6 +528,15 @@ public static string UseDragDrop { } } + /// + /// Looks up a localized string similar to Windows will not allow the handler for {0} to be extended. Using profile with Windows handler.. + /// + public static string WindowsWontExtendHandler { + get { + return ResourceManager.GetString("WindowsWontExtendHandler", resourceCulture); + } + } + /// /// Looks up a localized string similar to Error parsing XML store of saved custom profiles. /// diff --git a/AssociationMessages/LocalizedMessages.resx b/AssociationMessages/LocalizedMessages.resx index cd7366e..d36d41c 100644 --- a/AssociationMessages/LocalizedMessages.resx +++ b/AssociationMessages/LocalizedMessages.resx @@ -279,4 +279,7 @@ -add and -remove commands require at least one extension to be specified + + Windows will not allow the handler for {0} to be extended. Using profile with Windows handler. + \ No newline at end of file diff --git a/CommandLine/FileMeta.cpp b/CommandLine/FileMeta.cpp index 8b1328f..f0eaf68 100644 --- a/CommandLine/FileMeta.cpp +++ b/CommandLine/FileMeta.cpp @@ -40,6 +40,10 @@ int wmain(int argc, WCHAR* argv[]) SwitchArg promptSwitch(L"p",L"prompt",L"After execution, prompt to continue", false); cmd.add(promptSwitch); + // Define Explorer view of meta data switch + SwitchArg explorerSwitch(L"v", L"explorer", L"Export metadata thisExplorer sees", false); + cmd.add(explorerSwitch); + // Define XML file name override ValueArg xmlFileArg(L"x",L"xml",L"Name of XML file (only valid if one target file)",false,L"",L"file name"); cmd.add( xmlFileArg ); @@ -83,6 +87,11 @@ int wmain(int argc, WCHAR* argv[]) if (!exportSwitch.isSet()) throw ArgException(L"-c can only be used with -e", L"console"); } + else if (explorerSwitch.isSet()) + { + if (!exportSwitch.isSet()) + throw ArgException(L"-v can only be used with -e", L"explorer"); + } for (auto pos = targetFiles.begin(); pos != targetFiles.end(); ++pos) { @@ -94,9 +103,11 @@ int wmain(int argc, WCHAR* argv[]) result = ERROR_FILE_NOT_FOUND; break; } - else if (!checker.HasOurPropertyHandler(targetFile)) + else if (0 == checker.HasPropertyHandler(targetFile) || + (-1 == checker.HasPropertyHandler(targetFile) && !explorerSwitch.isSet())) { - // Skip files that do not have our property handler + // Skip files that do not have our property handler, + // unless we were asked for the Explorer view continue; } else if (deleteSwitch.isSet()) @@ -139,7 +150,7 @@ int wmain(int argc, WCHAR* argv[]) if (exportSwitch.isSet()) { xml_document doc; - ExportMetadata(&doc, targetFile); + ExportMetadata(&doc, targetFile, explorerSwitch.isSet()); // writing to a string rather than directly to the stream is odd, but writing directly does not compile // (trying to access a private constructor on traits - a typically arcane template issue) diff --git a/CommandLine/FileMeta.rc b/CommandLine/FileMeta.rc index bc5d780..3a44500 100644 Binary files a/CommandLine/FileMeta.rc and b/CommandLine/FileMeta.rc differ diff --git a/CommandLine/XmlHelpers.cpp b/CommandLine/XmlHelpers.cpp index 5f74b34..cb5bd2d 100644 --- a/CommandLine/XmlHelpers.cpp +++ b/CommandLine/XmlHelpers.cpp @@ -7,6 +7,7 @@ #include "tclap/CmdLine.h" #include #include +#include using namespace TCLAP; @@ -58,11 +59,12 @@ WCHAR* CPHException::GetMessage() return _pszError; } -// See if file is handled by our property handler -BOOL CExtensionChecker::HasOurPropertyHandler(wstring fileName) +// See if file is handled by our property handler, or another one, or none +// Returns 1 if our handler is used, 0 if no handler is set up, or -1 if a foreign handler is used +int CExtensionChecker::HasPropertyHandler(wstring fileName) { WCHAR pszExt[_MAX_EXT]; - BOOL val = FALSE; + int val = 0; // Try and get the extension if (0 == _wsplitpath_s(fileName.c_str(), NULL, 0, NULL, 0, NULL, 0, pszExt, _MAX_EXT)) @@ -78,16 +80,18 @@ BOOL CExtensionChecker::HasOurPropertyHandler(wstring fileName) WCHAR buffer[_MAX_EXT]; DWORD size = _MAX_EXT; - // Not finding the key results in FALSE + // Not finding the key results in 0 if (ERROR_SUCCESS == RegGetValue(HKEY_LOCAL_MACHINE, subKey.c_str(), NULL, RRF_RT_REG_SZ, NULL, buffer, &size)) { - // If the key is found, TRUE iff our property handler + // If the key is found, 1 iff our property handler, else -1 #ifdef _WIN64 - val = (0 == StrCmpI(buffer, OurPropertyHandlerGuid64)); + val = (0 == StrCmpI(buffer, OurPropertyHandlerGuid64)) ? 1 : -1; #else - val = (0 == StrCmpI(buffer, OurPropertyHandlerGuid32)); + val = (0 == StrCmpI(buffer, OurPropertyHandlerGuid32)) ? 1 : -1; #endif } + else + val = 0; m_extensions[pszExt] = val; } @@ -363,66 +367,75 @@ inline bool operator<(const PROPERTYKEY& a, const PROPERTYKEY& b) return a.pid < b.pid; } -void ExportMetadata (xml_document *doc, wstring targetFile) +void ExportMetadata (xml_document *doc, wstring targetFile, bool explorerView) { HRESULT hr = E_UNEXPECTED; + CComPtr pStore; + PROPERTYKEY * keys = NULL; xml_node *root = doc->allocate_node(node_element, MetadataNodeName); doc->append_node(root); - if (GetStgOpenStorageEx()) + try { - CComPtr pPropSetStg; - CComPtr pStore; - PROPERTYKEY * keys = NULL; - - try + if (explorerView) { - hr = (v_pfnStgOpenStorageEx)(targetFile.c_str(), STGM_READ | STGM_SHARE_EXCLUSIVE, STGFMT_FILE, 0, NULL, 0, - IID_IPropertySetStorage, (void**)&pPropSetStg); - if( FAILED(hr) ) - throw CPHException(ERROR_OPEN_FAILED, hr, IDS_E_IPSS_1, hr); - - hr = PSCreatePropertyStoreFromPropertySetStorage(pPropSetStg, STGM_READ, IID_IPropertyStore, (void **)&pStore); - pPropSetStg.Release(); - if( FAILED(hr) ) - throw CPHException(ERROR_OPEN_FAILED, hr, IDS_E_PSCREATE_1, hr); - - DWORD cProps; - hr = pStore->GetCount(&cProps); - if( FAILED(hr) ) - throw CPHException(ERROR_OPEN_FAILED, hr, IDS_E_IPS_GETCOUNT_1, hr); - - keys = new PROPERTYKEY[cProps]; - - for (DWORD i = 0; i < cProps; i++) + // Access the property store that Explorer would see - will not always be our handler + hr = SHGetPropertyStoreFromParsingName(targetFile.c_str(), NULL, GPS_READWRITE, IID_IPropertyStore, (void **)&pStore); + } + else + { + // Always use our own handler, which will access the alternate stream + CComPtr pPropSetStg; + if (GetStgOpenStorageEx()) { - hr = pStore->GetAt(i, &keys[i]); + hr = (v_pfnStgOpenStorageEx)(targetFile.c_str(), STGM_READ | STGM_SHARE_EXCLUSIVE, STGFMT_FILE, 0, NULL, 0, + IID_IPropertySetStorage, (void**)&pPropSetStg); if( FAILED(hr) ) - throw CPHException(ERROR_UNKNOWN_PROPERTY, hr, IDS_E_IPS_GETAT_1, hr); + throw CPHException(ERROR_OPEN_FAILED, hr, IDS_E_IPSS_1, hr); + + hr = PSCreatePropertyStoreFromPropertySetStorage(pPropSetStg, STGM_READ, IID_IPropertyStore, (void **)&pStore); + pPropSetStg.Release(); } + } - // Sort keys into their property sets - // We used to use IPropertyStorage to get the grouping, but this worked badly with Unicode property value - sort(keys, &keys[cProps]); + if( FAILED(hr) ) + throw CPHException(ERROR_OPEN_FAILED, hr, IDS_E_PSCREATE_1, hr); - // Loop through all the properties - DWORD index = 0; + DWORD cProps; + hr = pStore->GetCount(&cProps); + if( FAILED(hr) ) + throw CPHException(ERROR_OPEN_FAILED, hr, IDS_E_IPS_GETCOUNT_1, hr); - while( index < cProps) - { - // Export the properties in the property set - throws exceptions on error - ExportPropertySetData( doc, root, keys, index, pStore ); - } - } - catch (CPHException& e) + keys = new PROPERTYKEY[cProps]; + + for (DWORD i = 0; i < cProps; i++) { - delete [] keys; - throw e; + hr = pStore->GetAt(i, &keys[i]); + if( FAILED(hr) ) + throw CPHException(ERROR_UNKNOWN_PROPERTY, hr, IDS_E_IPS_GETAT_1, hr); } + // Sort keys into their property sets + // We used to use IPropertyStorage to get the grouping, but this worked badly with Unicode property value + sort(keys, &keys[cProps]); + + // Loop through all the properties + DWORD index = 0; + + while( index < cProps) + { + // Export the properties in the property set - throws exceptions on error + ExportPropertySetData( doc, root, keys, index, pStore ); + } + } + catch (CPHException& e) + { delete [] keys; + throw e; } + + delete [] keys; } // throws CPHException on error diff --git a/CommandLine/XmlHelpers.h b/CommandLine/XmlHelpers.h index b972188..d485919 100644 --- a/CommandLine/XmlHelpers.h +++ b/CommandLine/XmlHelpers.h @@ -35,11 +35,11 @@ class CPHException class CExtensionChecker { private: - std::map m_extensions; // map of extensions checked so far + std::map m_extensions; // map of extensions checked so far public: - // See if file is handled by our property handler - BOOL HasOurPropertyHandler(wstring fileName); + // See if file is handled by our property handler, or another, or none + int HasPropertyHandler(wstring fileName); }; int AccessResourceString(UINT uId, LPWSTR lpBuffer, int nBufferMax); @@ -61,7 +61,7 @@ void OutputDebugStringFormat( WCHAR* lpszFormat, ... ); typedef HRESULT (CALLBACK* PFN_STGOPENSTGEX)(const WCHAR*, DWORD, DWORD, DWORD, void*, void*, REFIID riid, void **); HRESULT MetadataPresent(wstring targetFile); -void ExportMetadata (xml_document *doc, wstring targetFile); +void ExportMetadata (xml_document *doc, wstring targetFile, bool explorerView = false); void ExportPropertySetData (xml_document *doc, xml_node *root, PROPERTYKEY* keys, DWORD& index, CComPtr pStore); void ImportMetadata (xml_document *doc, wstring targetFile); diff --git a/CommandLineAssociationManager/CommandLineAssociationManager.csproj b/CommandLineAssociationManager/CommandLineAssociationManager.csproj index fc1099c..e9b752f 100644 --- a/CommandLineAssociationManager/CommandLineAssociationManager.csproj +++ b/CommandLineAssociationManager/CommandLineAssociationManager.csproj @@ -87,6 +87,9 @@ PropertyListEntry.cs + + PropertyHandlerTest.cs + SavedState.cs @@ -96,6 +99,15 @@ TreeItem.cs + + + + + + + + + diff --git a/CommandLineAssociationManager/Program.cs b/CommandLineAssociationManager/Program.cs index 4147c94..d029171 100644 --- a/CommandLineAssociationManager/Program.cs +++ b/CommandLineAssociationManager/Program.cs @@ -18,7 +18,7 @@ class Program "Usage:", "", " FileMetaAssoc.exe {-l|-a|-r|-h} [-p=] ", - " [-d=] [ ...]", + " [-d=] [-m] [ ...]", "", "Where:", "", @@ -42,6 +42,10 @@ class Program " --d=, --definitions=", " Profile definitions file to be used for -add", "", + " -m, --merge", + " If set -add merges any existing settings into a new profile. ", + " Otherwise, profile settings are used", + "", " ", " One or more target extensions for -add or -remove, for example, .txt", "", @@ -57,6 +61,7 @@ static int Main(string[] args) bool remove = false; string profile = null; string definitions = null; + bool merge = false; try { @@ -67,6 +72,7 @@ static int Main(string[] args) { "h|?|help", v => {help = v != null; commands++;} }, { "p|profile=", v => profile = v }, { "d|definitions=", v => definitions = v }, + { "m|merge", v => merge = true }, }; List extensions = argParser.Parse(args); @@ -120,7 +126,9 @@ static int Main(string[] args) ErrorCode = WindowsErrorCodes.ERROR_INVALID_PARAMETER }; - if (!(e.PropertyHandlerState == HandlerState.Ours || e.PropertyHandlerState == HandlerState.Chained)) + if (!(e.PropertyHandlerState == HandlerState.Ours || + e.PropertyHandlerState == HandlerState.Chained || + e.PropertyHandlerState == HandlerState.ProfileOnly)) throw new AssocMgrException { Description = String.Format(LocalizedMessages.DoesNotHaveHandler, ext), @@ -158,7 +166,7 @@ static int Main(string[] args) e = state.CreateExtension(ext); } - e.SetupHandlerForExtension(p, false); + e.SetupHandlerForExtension(p, merge); Console.WriteLine(String.Format(LocalizedMessages.HandlerAddedOK, e.Name)); } } diff --git a/ContextMenuHandler/ContextMenuHandler.cpp b/ContextMenuHandler/ContextMenuHandler.cpp index 73005fe..29182ff 100644 --- a/ContextMenuHandler/ContextMenuHandler.cpp +++ b/ContextMenuHandler/ContextMenuHandler.cpp @@ -147,7 +147,6 @@ IFACEMETHODIMP CContextMenuHandler::QueryContextMenu( if (hDrop != NULL) { // Determine how many files are involved in this operation. - // We show the custom context menu item only when exactly one file is selected UINT nFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0); WCHAR buff[MAX_PATH]; @@ -258,9 +257,9 @@ IFACEMETHODIMP CContextMenuHandler::InvokeCommand(LPCMINVOKECOMMANDINFO pici) for (int i = 0; i < m_files.size(); i++) { // In the multi-file case, we know only that at least one of the files has our context menu, - // so we need to check the extension of each file to see if our propertyhandler is configured for it + // so we need to check the extension of each file to see if a propertyhandler is configured for it if (m_files.size() > 1) - if (!m_checker.HasOurPropertyHandler(m_files[i])) + if (0 == m_checker.HasPropertyHandler(m_files[i])) continue; // Build an XML document containing thw metadata @@ -307,7 +306,7 @@ IFACEMETHODIMP CContextMenuHandler::InvokeCommand(LPCMINVOKECOMMANDINFO pici) // In the multi-file case, we know only that at least one of the files has our context menu, // so we need to check the extension of each file to see if our propertyhandler is configured for it if (m_files.size() > 1) - if (!m_checker.HasOurPropertyHandler(m_files[i])) + if (1 != m_checker.HasPropertyHandler(m_files[i])) continue; wstring szXmlTarget = m_files[i]; @@ -409,7 +408,7 @@ IFACEMETHODIMP CContextMenuHandler::InvokeCommand(LPCMINVOKECOMMANDINFO pici) // so we need to check the extension of each file to see if our propertyhandler is configured for it if (m_files.size() > 1) { - if (!m_checker.HasOurPropertyHandler(m_files[i])) + if (1 != m_checker.HasPropertyHandler(m_files[i])) continue; // Also, we need to check if metadata is present, to avoid adding an empty alternate stream by opening diff --git a/ContextMenuHandler/ContextMenuHandler.rc b/ContextMenuHandler/ContextMenuHandler.rc index 75f87f2..3c44de3 100644 Binary files a/ContextMenuHandler/ContextMenuHandler.rc and b/ContextMenuHandler/ContextMenuHandler.rc differ diff --git a/ContextMenuHandler/ContextMenuHandler.vcxproj b/ContextMenuHandler/ContextMenuHandler.vcxproj index f25c848..7302f10 100644 --- a/ContextMenuHandler/ContextMenuHandler.vcxproj +++ b/ContextMenuHandler/ContextMenuHandler.vcxproj @@ -134,6 +134,7 @@ + diff --git a/ContextMenuHandler/Dll.cpp b/ContextMenuHandler/Dll.cpp index e6d630d..af83cce 100644 --- a/ContextMenuHandler/Dll.cpp +++ b/ContextMenuHandler/Dll.cpp @@ -22,7 +22,8 @@ struct CLASS_OBJECT_INIT // add classes supported by this module here const CLASS_OBJECT_INIT c_rgClassObjectInit[] = { - { &__uuidof(CContextMenuHandler), CContextMenuHandler_CreateInstance } + { &__uuidof(CContextMenuHandler), CContextMenuHandler_CreateInstance }, + { &__uuidof(CExportContextMenuHandler), CExportContextMenuHandler_CreateInstance } }; @@ -152,12 +153,14 @@ STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void **ppv) STDAPI DllRegisterServer() { HRESULT hr = RegisterContextMenuHandler(); + if (SUCCEEDED(hr)) hr = RegisterExportContextMenuHandler(); return hr; } STDAPI DllUnregisterServer() { HRESULT hr = UnregisterContextMenuHandler(); + if (SUCCEEDED(hr)) hr = UnregisterExportContextMenuHandler(); return hr; } diff --git a/ContextMenuHandler/ExportContextMenuHandler.cpp b/ContextMenuHandler/ExportContextMenuHandler.cpp new file mode 100644 index 0000000..381f79d --- /dev/null +++ b/ContextMenuHandler/ExportContextMenuHandler.cpp @@ -0,0 +1,339 @@ +// Some parts copied from CppShellExtContextMenuHandler Copyright (c) Microsoft Corporation and subject to the Microsoft Public License: http://www.microsoft.com/opensource/licenses.mspx#Ms-PL. +// All other code Copyright (c) 2013, Dijji, and released under Ms-PL. This, with other relevant licenses, can be found in the root of this distribution. + +#include +#include +#include +#include +#include +#include // For IShellExtInit and IContextMenu +#include // For CComPtr +#include +#include "..\CommandLine\XmlHelpers.h" +#include "resource.h" +#include "dll.h" +#include "RegisterExtension.h" +#include +#include +#include +#include +using namespace std; +using namespace rapidxml; + +#define IDM_EXPORT 0 // The commands' identifier offsets + +static const WCHAR* PropertyHandlerDescription = L"File Metadata Export Context Menu Handler"; +static const WCHAR* ExportVerb = L"CMHExport"; + +class CExportContextMenuHandler : public IShellExtInit, public IContextMenu +{ +public: + CExportContextMenuHandler() : _cRef(1), m_pdtobj(NULL) + { + DllAddRef(); + } + + // IUnknown + IFACEMETHODIMP QueryInterface(REFIID riid, void ** ppv) + { + static const QITAB qit[] = + { + QITABENT(CExportContextMenuHandler, IContextMenu), + QITABENT(CExportContextMenuHandler, IShellExtInit), + {0, 0 }, + }; + return QISearch(this, qit, riid, ppv); + } + + IFACEMETHODIMP_(ULONG) AddRef() + { + return InterlockedIncrement(&_cRef); + } + + IFACEMETHODIMP_(ULONG) Release() + { + long cRef = InterlockedDecrement(&_cRef); + if (cRef == 0) + { + delete this; + } + return cRef; + } + + // IShellExtInit + IFACEMETHODIMP Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hKeyProgID); + + // IContextMenu + IFACEMETHODIMP QueryContextMenu(HMENU hMenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags); + IFACEMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO pici); + IFACEMETHODIMP GetCommandString(UINT_PTR idCommand, UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax); + + +private: + ~CExportContextMenuHandler() + { + SafeRelease(&m_pdtobj); + DllRelease(); + } + + long _cRef; + CExtensionChecker m_checker; + + IDataObject *m_pdtobj; + std::vector m_files; +}; + + +#pragma region IShellExtInit + +// Initialize the context menu handler. +// If any value other than S_OK is returned from the method, the context +// menu item is not displayed. +IFACEMETHODIMP CExportContextMenuHandler::Initialize( + LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hKeyProgID) +{ + // This is important because Initialize can be called multiple times, + // e.g. when more than 16 files are selected + SafeRelease(&m_pdtobj); + + if (NULL == pDataObj) + { + return E_INVALIDARG; + } + else + { + m_pdtobj = pDataObj; + m_pdtobj->AddRef(); + } + + return S_OK; +} + +#pragma endregion + +#pragma region IContextMenu + +// +// FUNCTION: CExportContextMenuHandler::QueryContextMenu +// +// PURPOSE: The Shell calls IContextMenu::QueryContextMenu to allow the +// context menu handler to add its menu items to the menu. It +// passes in the HMENU handle in the hmenu parameter. The +// indexMenu parameter is set to the index to be used for the +// first menu item that is to be added. +// +IFACEMETHODIMP CExportContextMenuHandler::QueryContextMenu( + HMENU hMenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags) +{ + // If uFlags include CMF_DEFAULTONLY then we should not do anything. + if (CMF_DEFAULTONLY & uFlags) + { + return MAKE_HRESULT(SEVERITY_SUCCESS, 0, USHORT(0)); + } + + HRESULT hr = E_FAIL; + + FORMATETC fe = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + STGMEDIUM stm; + + // The pDataObj pointer contains the objects being acted upon + if (NULL != m_pdtobj && SUCCEEDED(m_pdtobj->GetData(&fe, &stm))) + { + // Get an HDROP handle. + HDROP hDrop = static_cast(GlobalLock(stm.hGlobal)); + if (hDrop != NULL) + { + // Determine how many files are involved in this operation. + UINT nFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0); + WCHAR buff[MAX_PATH]; + + for (int i=0; i < nFiles; i++) + { + DragQueryFile(hDrop, i, buff, sizeof(buff)); + m_files.push_back(buff); + } + + GlobalUnlock(stm.hGlobal); + } + + ReleaseStgMedium(&stm); + } + + WCHAR szXmlTarget[MAX_PATH]; + hr = S_OK; + + if (m_files.size() == 1) + { + wcscpy_s(szXmlTarget, MAX_PATH, m_files[0].c_str()); + wcscat_s(szXmlTarget, MAX_PATH, MetadataFileSuffix); + } + else + { + AccessResourceString(IDS_XML_FILE, szXmlTarget, MAX_PATH); + } + + // First, create and populate a submenu. + HMENU hSubmenu = CreatePopupMenu(); + UINT uID = idCmdFirst; + WCHAR buffer[2*MAX_PATH]; + + // Export + AccessResourceString(IDS_EXPORT, buffer, MAX_PATH); + wcscat_s(buffer, 2*MAX_PATH, szXmlTarget); + if (!InsertMenu ( hSubmenu, 0, MF_BYPOSITION, uID++, buffer) ) + return HRESULT_FROM_WIN32(GetLastError()); + + // Insert the submenu into the ctx menu provided by Explorer. + MENUITEMINFO mii = { sizeof(MENUITEMINFO) }; + + mii.fMask = MIIM_SUBMENU | MIIM_STRING | MIIM_ID; + mii.wID = uID++; + mii.hSubMenu = hSubmenu; + + AccessResourceString(IDS_METADATA, buffer, MAX_PATH); + mii.dwTypeData = (LPWSTR)buffer; + + if (!InsertMenuItem ( hMenu, indexMenu, TRUE, &mii ) ) + return HRESULT_FROM_WIN32(GetLastError()); + + return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, uID - idCmdFirst ); +} + +// +// FUNCTION: CExportContextMenuHandler::InvokeCommand +// +// PURPOSE: This method is called when a user clicks a menu item to tell +// the handler to run the associated command. The lpcmi parameter +// points to a structure that contains the needed information. +// +IFACEMETHODIMP CExportContextMenuHandler::InvokeCommand(LPCMINVOKECOMMANDINFO pici) +{ + HRESULT hr = E_FAIL; + + // Commands can be indicated by offset id or command verb string + // We support only the id form + if (IS_INTRESOURCE(pici->lpVerb)) + { + switch(LOWORD(pici->lpVerb)) + { + case IDM_EXPORT: + try + { + for (int i = 0; i < m_files.size(); i++) + { + // In the multi-file case, we know only that at least one of the files has our context menu, + // so we need to check the extension of each file to see if a propertyhandler is configured for it + if (m_files.size() > 1) + if (0 == m_checker.HasPropertyHandler(m_files[i])) + continue; + + // Build an XML document containing thw metadata + xml_document doc; + ExportMetadata(&doc, m_files[i], true); + + // writing to a string rather than directly to the stream is odd, but writing directly does not compile + // (trying to access a private constructor on traits - a typically arcane template issue) + wstring s; + print(std::back_inserter(s), doc, 0); + + wstring szXmlTarget = m_files[i]; + szXmlTarget += MetadataFileSuffix; + + // Now write from the XML string to a file stream + // This used to be STL, but wofstream by default writes 8-bit encoded files, and changing that is complex + FILE *pfile; + errno_t err = _wfopen_s(&pfile, szXmlTarget.c_str(), L"w+, ccs=UTF-16LE"); + if (0 == err) + { + fwrite(s.c_str(), sizeof(WCHAR), s.length(), pfile); + fclose(pfile); + } + else + throw CPHException(err, E_FAIL, IDS_E_FILEOPEN_1, err); + } + + hr = S_OK; + } + catch(CPHException& e) + { + WCHAR buffer[MAX_PATH]; + AccessResourceString(IDS_EXPORT_FAILED, buffer, MAX_PATH); + MessageBox(NULL, e.GetMessage(), buffer, MB_OK); + hr = e.GetHResult(); + } + break; + } + } + + return hr; +} + + +// +// FUNCTION: CCExportContextMenuHandler::GetCommandString +// +// PURPOSE: This method can be called +// to request the verb string that is assigned to a command. +// Either ANSI or Unicode verb strings can be requested. +// We only implement support for the Unicode values of +// uFlags, because only those have been used in Windows Explorer +// since Windows 2000. +// +IFACEMETHODIMP CExportContextMenuHandler::GetCommandString(UINT_PTR idCommand, + UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax) +{ + HRESULT hr = E_INVALIDARG; + + switch (uFlags) + { + // GCS_VERBW is an optional feature that enables a caller to + // discover the canonical name for the verb passed in through + // idCommand. + case GCS_VERBW: + switch (idCommand) + { + case IDM_EXPORT: + hr = StringCchCopyW(reinterpret_cast(pszName), cchMax, ExportVerb); + break; + } + } + + // If the command (idCommand) is not supported by this context menu + // extension handler, return E_INVALIDARG. + + return hr; +} + + +#pragma region Registration and other COM mechanics + +HRESULT CExportContextMenuHandler_CreateInstance(REFIID riid, void **ppv) +{ + HRESULT hr = E_OUTOFMEMORY; + CExportContextMenuHandler *pirm = new (std::nothrow) CExportContextMenuHandler(); + if (pirm) + { + hr = pirm->QueryInterface(riid, ppv); + pirm->Release(); + } + return hr; +} + +HRESULT RegisterExportContextMenuHandler() +{ + // register the context menu handler COM object + CRegisterExtension re(__uuidof(CExportContextMenuHandler), HKEY_LOCAL_MACHINE); + HRESULT hr = re.RegisterInProcServer(PropertyHandlerDescription, L"Both"); + + return hr; +} + +HRESULT UnregisterExportContextMenuHandler() +{ + // Unregister the context menu handler COM object. + CRegisterExtension re(__uuidof(CExportContextMenuHandler), HKEY_LOCAL_MACHINE); + HRESULT hr = re.UnRegisterObject(); + + return hr; +} +#pragma endregion \ No newline at end of file diff --git a/ContextMenuHandler/dll.h b/ContextMenuHandler/dll.h index 42684d7..fcd8ec9 100644 --- a/ContextMenuHandler/dll.h +++ b/ContextMenuHandler/dll.h @@ -22,13 +22,18 @@ template void SafeRelease(T **ppT) #ifdef _WIN64 class DECLSPEC_UUID("28D14D00-2D80-4956-9657-9D50C8BB47A5") CContextMenuHandler; +class DECLSPEC_UUID("DE4C4CAF-C564-4EEA-9FF7-C46FB8023818") CExportContextMenuHandler; #else class DECLSPEC_UUID("DA38301B-BE91-4397-B2C8-E27A0BD80CC5") CContextMenuHandler; +class DECLSPEC_UUID("5A677F18-527D-42B3-BAA0-9785D3A8256F") CExportContextMenuHandler; #endif HRESULT CContextMenuHandler_CreateInstance(REFIID riid, void **ppv); +HRESULT CExportContextMenuHandler_CreateInstance(REFIID riid, void **ppv); void DllAddRef(); void DllRelease(); HRESULT RegisterContextMenuHandler(); HRESULT UnregisterContextMenuHandler(); +HRESULT RegisterExportContextMenuHandler(); +HRESULT UnregisterExportContextMenuHandler(); diff --git a/PropertyHandler/FileMetaPropertyHandler.rc b/PropertyHandler/FileMetaPropertyHandler.rc index a09f733..e22f87d 100644 Binary files a/PropertyHandler/FileMetaPropertyHandler.rc and b/PropertyHandler/FileMetaPropertyHandler.rc differ diff --git a/PropertyHandler/PropertyHandler.cpp b/PropertyHandler/PropertyHandler.cpp index c409021..4e7df7d 100644 --- a/PropertyHandler/PropertyHandler.cpp +++ b/PropertyHandler/PropertyHandler.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "dll.h" #include "RegisterExtension.h" @@ -119,9 +120,12 @@ HRESULT CPropertyHandler::GetValue(REFPROPERTYKEY key, PROPVARIANT *pPropVar) PropVariantInit(pPropVar); HRESULT hr = OpenStore(FALSE); // Take the File Meta property value first, and if there isn't one, + // see if this is a check for the software product name, which we use as a marker, or if not // try for a chained property value hr = SUCCEEDED(hr) ? _pStore->GetValue(key, pPropVar) : hr; - if (_pChainedPropStore != NULL && SUCCEEDED(hr) && pPropVar->vt == VT_EMPTY) + if (SUCCEEDED(hr) && pPropVar->vt == VT_EMPTY && key == PKEY_Software_ProductName) + hr = InitPropVariantFromString(L"FileMetadata", pPropVar); + else if (_pChainedPropStore != NULL && SUCCEEDED(hr) && pPropVar->vt == VT_EMPTY) hr = _pChainedPropStore->GetValue(key, pPropVar); return hr; } diff --git a/Setup/Product.wxs b/Setup/Product.wxs index 6668330..797b212 100644 --- a/Setup/Product.wxs +++ b/Setup/Product.wxs @@ -6,7 +6,7 @@ Also thanks to Alek Davis for the 32/64-bit strategy: http://alekdavis.blogspot. --> - + @@ -34,7 +34,7 @@ Also thanks to Alek Davis for the 32/64-bit strategy: http://alekdavis.blogspot. + Language="1033" Version="1.6.7318.0" Manufacturer="Dijji" UpgradeCode="$(var.ProductUpgradeCode)"> = 600)]]> diff --git a/TestDriver/MainWindow.xaml b/TestDriver/MainWindow.xaml index 93b76d0..9f74f37 100644 --- a/TestDriver/MainWindow.xaml +++ b/TestDriver/MainWindow.xaml @@ -39,10 +39,12 @@ - +