Code:
/ 4.0 / 4.0 / DEVDIV_TFS / Dev10 / Releases / RTMRel / wpf / src / Framework / System / Windows / Shell / JumpList.cs / 1305600 / JumpList.cs
/**************************************************************************\ Copyright Microsoft Corporation. All Rights Reserved. \**************************************************************************/ namespace System.Windows.Shell { using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Security; using System.Security.Permissions; using System.Text; using System.Threading; using System.Windows; using System.Windows.Markup; using MS.Internal; using MS.Internal.AppModel; using MS.Internal.PresentationFramework; using MS.Internal.Interop; using MS.Win32; using HRESULT = MS.Internal.Interop.HRESULT; ////// The list of possible reasons why a JumpItem would be rejected from a JumpList when applied. /// public enum JumpItemRejectionReason { ////// Unknown reason. This should not be used. /// None, ////// The item was rejected because it was invalid for a jump list. E.g. the file path didn't exist. /// ////// If the application is running on a system where jump lists are not available (like XP or Vista) /// items will get rejected with this reason. /// InvalidItem, ////// The item was rejected because the program was not registered to handle the file extension. /// NoRegisteredHandler, ////// The item was rejected because the user had explicitly removed it since the last time a JumpList was applied. /// RemovedByUser, } ////// EventArgs for JumpList.JumpItemsRejected event. /// public sealed class JumpItemsRejectedEventArgs : EventArgs { public JumpItemsRejectedEventArgs() : this(null, null) { } public JumpItemsRejectedEventArgs(IListrejectedItems, IList reasons) { // If one of the collections is null then the other has to be, too. if ((rejectedItems == null && reasons != null) || (reasons == null && rejectedItems != null) || (rejectedItems != null && reasons != null && rejectedItems.Count != reasons.Count)) { throw new ArgumentException(SR.Get(SRID.JumpItemsRejectedEventArgs_CountMismatch)); } // We don't want the contents of the list getting modified in the event handler, // so use a read-only copy if (rejectedItems != null) { RejectedItems = new List (rejectedItems).AsReadOnly(); RejectionReasons = new List (reasons).AsReadOnly(); } else { RejectedItems = new List ().AsReadOnly(); RejectionReasons = new List ().AsReadOnly(); } } public IList RejectedItems { get; private set; } public IList RejectionReasons { get; private set; } } /// /// EventArgs for JumpList.JumpItemsRemovedByUser event. /// public sealed class JumpItemsRemovedEventArgs : EventArgs { public JumpItemsRemovedEventArgs() : this(null) { } public JumpItemsRemovedEventArgs(IListremovedItems) { if (removedItems != null) { RemovedItems = new List (removedItems).AsReadOnly(); } else { RemovedItems = new List ().AsReadOnly(); } } public IList RemovedItems { get; private set; } } /// /// Manage the tasks and files that Shell associates with this application. /// This allows modification of the Jump List UI in Windows 7 that appears on the Taskbar and Start Menu. /// ////// This class is unsafe to use in partial trust. It modifies the information Shell presents to the user about the application. /// [UIPermission(SecurityAction.LinkDemand, Window=UIPermissionWindow.AllWindows)] [ContentProperty("JumpItems")] public sealed class JumpList : ISupportInitialize { ////// Critical: Calls critical native methods. Class-level LinkDemand doesn't apply to static ctor. /// TreatAsSafe: Added explicit demand for AllWindows to the static ctor. /// [SecurityCritical, SecurityTreatAsSafe] [UIPermission(SecurityAction.Demand, Window=UIPermissionWindow.AllWindows)] static JumpList() { // Passing NULL for the HMODULE returns the running executable path. _FullName = UnsafeNativeMethods.GetModuleFileName(new HandleRef()); } ////// Add the item at the specified file path to the application's JumpList's recent items. /// ////// This makes the item eligible for inclusion in the special Recent and Frequent categories. /// ////// Critical: Calls critical SHAddToRecentDocs. /// PublicOK: The class is only available in full trust. /// [SecurityCritical] public static void AddToRecentCategory(string itemPath) { Verify.FileExists(itemPath, "itemPath"); itemPath = Path.GetFullPath(itemPath); NativeMethods2.SHAddToRecentDocs(itemPath); } ////// Add the item to the application's JumpList's recent items. /// ////// This makes the item eligible for inclusion in the special Recent and Frequent categories. /// ////// Critical: Calls critical SHAddToRecentDocs. /// PublicOK: The class is only available in full trust. /// [SecurityCritical] public static void AddToRecentCategory(JumpPath jumpPath) { Verify.IsNotNull(jumpPath, "jumpPath"); AddToRecentCategory(jumpPath.Path); } ////// Add the task at the specified file path to the application's JumpList's recent items. /// ////// This makes the item eligible for inclusion in the special Recent and Frequent categories. /// ////// Critical: Calls critical SHAddToRecentDocs. /// PublicOK: The class is only available in full trust. /// [SecurityCritical] public static void AddToRecentCategory(JumpTask jumpTask) { Verify.IsNotNull(jumpTask, "jumpTask"); // SHAddToRecentDocs only allows IShellLinks in Windows 7 and later. // Silently fail this if that's not the case. // We don't give feedback on success here, so this is okay. if (Utilities.IsOSWindows7OrNewer) { IShellLinkW shellLink = CreateLinkFromJumpTask(jumpTask, false); try { if (shellLink != null) { NativeMethods2.SHAddToRecentDocs(shellLink); } } finally { Utilities.SafeRelease(ref shellLink); } } } private class _RejectedJumpItemPair { public JumpItem JumpItem { get; set; } public JumpItemRejectionReason Reason { get; set; } } private class _ShellObjectPair { // JumpPath/JumpTask public JumpItem JumpItem { get; set; } // IShellItem/IShellLink public object ShellObject { get; set; } ////// Releases all native references in a list of _ShellObjectPairs. /// ////// Critical: Calls critical Utilities.SafeRelease. /// /// The list from which to release the resources. [SecurityCritical] public static void ReleaseShellObjects(List<_ShellObjectPair> list) { if (list != null) { foreach (_ShellObjectPair shellMap in list) { object o = shellMap.ShellObject; shellMap.ShellObject = null; Utilities.SafeRelease(ref o); } } } } #region Attached Property Methods ////// Set the JumpList attached property on an Application. /// public static void SetJumpList(Application application, JumpList value) { Verify.IsNotNull(application, "application"); lock (s_lock) { // If this was associated with a different application, remove the association. JumpList oldValue; if (s_applicationMap.TryGetValue(application, out oldValue) && oldValue != null) { oldValue._application = null; } // Associate the jumplist with the application so we can retrieve it later. s_applicationMap[application] = value; if (value != null) { value._application = application; } } if (value != null) { // Changes will only get applied if the list isn't in an ISupportInitialize block. value.ApplyFromApplication(); } } ////// Get the JumpList attached property for an Application. /// public static JumpList GetJumpList(Application application) { Verify.IsNotNull(application, "application"); JumpList value; s_applicationMap.TryGetValue(application, out value); return value; } #endregion // static lock to ensure integrity when modifying instances as attached properties on Application. private static readonly object s_lock = new object(); private static readonly Dictionarys_applicationMap = new Dictionary (); private Application _application; // Used to enforce the ISupportInitialize contract. It's not required to BeginInit to use this class, // but it is useful for XAML scenarios so we can apply the changes when both EndInit has been called // and the Application attached property has been set. private bool? _initializing; // The internal list of JumpItems in this JumpList private List _jumpItems; /// /// Critical: This class cannot be used from partial trust. /// PublicOK: The class is only available in full trust. /// [SecurityCritical] public JumpList() : this(null, false, false) { // Restore the ability to use ISupportInitialize. _initializing = null; } ////// Critical: This class cannot be used from partial trust. /// PublicOK: The class is only available in full trust. /// [SecurityCritical] public JumpList(IEnumerableitems, bool showFrequent, bool showRecent) { if (items != null) { _jumpItems = new List (items); } else { _jumpItems = new List (); } ShowFrequentCategory = showFrequent; ShowRecentCategory = showRecent; // Using this constructor precludes using ISupportInitialize. _initializing = false; } /// /// Whether to show the special "Frequent" category. /// ////// This category is managed by the Shell and keeps track of items that are frequently accessed by this program. /// Applications can request that specific items are included here by calling JumpList.AddToRecentCategory. /// Because of duplication, applications generally should not have both ShowRecentCategory and ShowFrequentCategory set at the same time. /// public bool ShowFrequentCategory { get; set; } ////// Whether to show the special "Recent" category. /// ////// This category is managed by the Shell and keeps track of items that have been recently accessed by this program. /// Applications can request that specific items are included here by calling JumpList.AddToRecentCategory /// Because of duplication, applications generally should not have both ShowRecentCategory and ShowFrequentCategory set at the same time. /// public bool ShowRecentCategory { get; set; } ////// The list of JumpItems to be in the JumpList. After a call to Apply this list will contain only those items that were successfully added. /// ////// This object is not guaranteed to retain its identity after a call to Apply or other implicit setting of the JumpList. /// It should be requeried at such times. /// public ListJumpItems { get { return _jumpItems; } } private bool IsUnmodified { get { return _initializing == null && JumpItems.Count == 0 && !ShowRecentCategory && !ShowFrequentCategory; } } #region ISupportInitialize Members /// /// Prepare the JumpList for modification. /// ////// This works in concert with the Application.JumpList attached property. The JumpList will automatically be applied /// to the current application when attached and a corresponding call to EndInit is made. /// Nested calls to BeginInit are not allowed. /// public void BeginInit() { if (!IsUnmodified) { throw new InvalidOperationException(SR.Get(SRID.JumpList_CantNestBeginInitCalls)); } _initializing = true; } ////// Signal the end of initialization of this JumpList. If it is attached to the current Application, apply the contents of the jump list. /// ////// Calls to EndInit must be paired with calls to BeginInit. /// public void EndInit() { if (_initializing != true) { throw new NotSupportedException(SR.Get(SRID.JumpList_CantCallUnbalancedEndInit)); } _initializing = false; // EndInit only implicitly applies the list if the current Application has been set as an attached property. ApplyFromApplication(); } #endregion ////// Get the AppUserModelId for the running process. /// ////// This is a Shell property that currently is only used as part of a heuristic /// for what taskbar item an HWND should be associated with, e.g. you can put /// windows from multiple processes into the same group, or you can prevent glomming /// of HWNDs that would otherwise be shown together. /// /// Even though this property isn't exposed on the public WPF OM /// we still want to make sure that the jump list gets associated with /// the current running app even if the client has explicitly changed the id. /// /// It's straightforward to p/invoke to set these for the running application or /// the HWND. Not so much for this object. /// ////// Critical: Calls critical GetCurrentProcessExplicitAppUserModelID. /// private static string _RuntimeId { [SecurityCritical] get { string appId; HRESULT hr = NativeMethods2.GetCurrentProcessExplicitAppUserModelID(out appId); if (hr == HRESULT.E_FAIL) { // This is how Shell signals that the app id hasn't been set. hr = HRESULT.S_OK; appId = null; } hr.ThrowIfFailed(); return appId; } } ////// Critical: Calls critical method ApplyList. /// PublicOK: This class is only available in full trust. /// [SecurityCritical] public void Apply() { if (_initializing == true) { throw new InvalidOperationException(SR.Get(SRID.JumpList_CantApplyUntilEndInit)); } // After this attempting to use ISupportInitialize is invalid. _initializing = false; ApplyList(); } ////// Critical: Calls critical method ApplyList. /// TreatAsSafe: This class is only available in full trust. /// [SecurityCritical, SecurityTreatAsSafe] private void ApplyFromApplication() { // If we're here and the caller has modified the JumpList without using ISupportInitialize // we still want to apply the changes. if (_initializing != true && !IsUnmodified) { _initializing = false; } if (_application == Application.Current && _initializing == false) { // If we're applying due to being an attached property, then don't apply // unless we're really on the current application and wait until EndInit // has been called, or the list has otherwise been modified. ApplyList(); } } ////// Critical: Uses native methods to apply JumpItem data to the Shell's JumpList for the current application. /// [SecurityCritical] private void ApplyList() { Debug.Assert(_initializing == false); Verify.IsApartmentState(ApartmentState.STA); // We don't want to force applications to conditionally check this before constructing a JumpList, // but if we're not on 7 then this isn't going to work. Fail fast. if (!Utilities.IsOSWindows7OrNewer) { RejectEverything(); return; } ListsuccessList; List<_RejectedJumpItemPair> rejectedList; // Declare these outside the try block so we can cleanup native resources in the _ShellObjectPairs. List > categories = null; List<_ShellObjectPair> removedList = null; var destinationList = (ICustomDestinationList)Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid(CLSID.DestinationList))); try { // Even though we're not exposing Shell's AppModelId concept, we'll still respect it // since it's easy to clients to p/invoke to set it for Application and Window, but not for JumpLists string appId = _RuntimeId; if (!string.IsNullOrEmpty(appId)) { destinationList.SetAppID(appId); } // The number ot items visible on a jump list is a user setting. Shell doesn't reject items based on overflow. // We don't bother checking against it because the app can query the setting and manage overflow based on it // if they really care. We'll happily add too many items with the hope that if the user changes the setting // items will be recovered from the overflow. uint slotsVisible; Guid removedIid = new Guid(IID.ObjectArray); var objectsRemoved = (IObjectArray)destinationList.BeginList(out slotsVisible, ref removedIid); // Keep track of the items that were previously removed by the user. // We don't want to pend any items that are contained in this list. // It's possible that this contains null JumpItems when we were unable to do the conversion. removedList = GenerateJumpItems(objectsRemoved); // Keep track of the items that have been successfully pended. // IMPORTANT: Ensure at the end of this that if the list is applied again that it would // result in items being added to the JumpList in the same order. // This doesn't mean that they'll be ordered the same as how the user added them // (e.g. categories will be coalesced), but the categories should appear in the same order. // Since when we call AddCategory we're doing it in reverse order, AddCategory augments // the items in the list in reverse as well. At the end the final list is reversed. successList = new List
(JumpItems.Count); // Keep track of the items that we couldn't pend, and why. rejectedList = new List<_RejectedJumpItemPair>(JumpItems.Count); // Need to group the JumpItems based on their categories. // The special "Tasks" category doesn't actually have a name and should be first so it's unconditionally added. categories = new List >() { new List<_ShellObjectPair>() }; // This is not thread-safe. // We're traversing the original list so we're vulnerable to another thread modifying it during the enumeration. foreach (var jumpItem in JumpItems) { if (jumpItem == null) { // App added a null jump item? Just go through the normal failure mechanisms. rejectedList.Add(new _RejectedJumpItemPair{ JumpItem = jumpItem, Reason = JumpItemRejectionReason.InvalidItem }); continue; } object shellObject = null; try { shellObject = GetShellObjectForJumpItem(jumpItem); // If for some reason we couldn't create the item add it to the rejected list. if (shellObject == null) { rejectedList.Add(new _RejectedJumpItemPair { Reason = JumpItemRejectionReason.InvalidItem, JumpItem = jumpItem }); continue; } // Don't add this item if it's in the list of items previously removed by the user. if (ListContainsShellObject(removedList, shellObject)) { rejectedList.Add(new _RejectedJumpItemPair { Reason = JumpItemRejectionReason.RemovedByUser, JumpItem = jumpItem }); continue; } var shellMap = new _ShellObjectPair { JumpItem = jumpItem, ShellObject = shellObject }; if (string.IsNullOrEmpty(jumpItem.CustomCategory)) { // No custom category, so add to the Tasks list. categories[0].Add(shellMap); } else { // Find the appropriate category and add to that list. // If it doesn't exist, add a new category for it. bool categoryExists = false; foreach (var list in categories) { // The first item in the category list can be used to check the name. if (list.Count > 0 && list[0].JumpItem.CustomCategory == jumpItem.CustomCategory) { list.Add(shellMap); categoryExists = true; break; } } if (!categoryExists) { categories.Add(new List<_ShellObjectPair>() { shellMap }); } } // Shell interface is now owned by the category list. shellObject = null; } finally { Utilities.SafeRelease(ref shellObject); } } // Jump List categories get added top-down, except for "Tasks" which is special and always at the bottom. // We want the Recent/Frequent to always be at the top so they get added first. // Logically the categories are added bottom-up, but their contents are top-down, // so we reverse the order we add the categories to the destinationList. // To preserve the item ordering AddCategory also adds items in reverse. // We need to reverse the final list when everything is done. categories.Reverse(); if (ShowFrequentCategory) { destinationList.AppendKnownCategory(KDC.FREQUENT); } if (ShowRecentCategory) { destinationList.AppendKnownCategory(KDC.RECENT); } // Now that all the JumpItems are grouped add them to the custom destinations list. foreach (List<_ShellObjectPair> categoryList in categories) { if (categoryList.Count > 0) { string categoryHeader = categoryList[0].JumpItem.CustomCategory; AddCategory(destinationList, categoryHeader, categoryList, successList, rejectedList); } } destinationList.CommitList(); } catch { // It's not okay to throw an exception here. If Shell is rejecting the JumpList for some reason // we don't want to be responsible for the app throwing an exception in its startup path. // For common use patterns there isn't really any user code on the stack, so there isn't // an opportunity to catch this in the app. // We can instead handle this. // Try to notify the developer if they're hitting this. if (TraceShell.IsEnabled) { TraceShell.Trace(TraceEventType.Error, TraceShell.RejectingJumpItemsBecauseCatastrophicFailure); } RejectEverything(); return; } finally { // Deterministically release native resources. Utilities.SafeRelease(ref destinationList); if (categories != null) { foreach (List<_ShellObjectPair> list in categories) { _ShellObjectPair.ReleaseShellObjects(list); } } // Note that this only clears the ShellObjects, not the JumpItems. // We still need the JumpItems out of this list for the JumpItemsRemovedByUser event. _ShellObjectPair.ReleaseShellObjects(removedList); } // Swap the current list with what we were able to successfully place into the JumpList. // Reverse it first to ensure that the items are in a repeatable order. successList.Reverse(); _jumpItems = successList; // Raise the events for rejected and removed. EventHandler
rejectedHandler = JumpItemsRejected; EventHandler removedHandler = JumpItemsRemovedByUser; if (rejectedList.Count > 0 && rejectedHandler != null) { var items = new List (rejectedList.Count); var reasons = new List (rejectedList.Count); foreach (_RejectedJumpItemPair rejectionPair in rejectedList) { items.Add(rejectionPair.JumpItem); reasons.Add(rejectionPair.Reason); } rejectedHandler(this, new JumpItemsRejectedEventArgs(items, reasons)); } if (removedList.Count > 0 && removedHandler != null) { var items = new List (removedList.Count); foreach (_ShellObjectPair shellMap in removedList) { // It's possible that not every shell object could be converted to a JumpItem. if (shellMap.JumpItem != null) { items.Add(shellMap.JumpItem); } } if (items.Count > 0) { removedHandler(this, new JumpItemsRemovedEventArgs(items)); } } } /// /// Critical: This accesses methods on native interfaces IShellLink and IShellItem. /// TreatAsSafe: This is checking for equality in a provided list. This information is safe. /// [SecurityCritical, SecurityTreatAsSafe] private static bool ListContainsShellObject(List<_ShellObjectPair> removedList, object shellObject) { Debug.Assert(removedList != null); Debug.Assert(shellObject != null); if (removedList.Count == 0) { return false; } // Casts in .Net don't AddRef. Don't need to release these. var shellItem = shellObject as IShellItem; if (shellItem != null) { foreach (var shellMap in removedList) { var removedItem = shellMap.ShellObject as IShellItem; if (removedItem != null) { if (0 == shellItem.Compare(removedItem, SICHINT.CANONICAL | SICHINT.TEST_FILESYSPATH_IF_NOT_EQUAL)) { return true; } } } return false; } var shellLink = shellObject as IShellLinkW; if (shellLink != null) { foreach (var shellMap in removedList) { var removedLink = shellMap.ShellObject as IShellLinkW; if (removedLink != null) { // There's no intrinsic comparison function for ShellLinks. // Talking to the Shell guys, the way they compare these is to catenate a string with // a normalized version of the app path and the unmodified args. // If the two strings ordinally compare, they're the same. string removedLinkString = ShellLinkToString(removedLink); string linkString = ShellLinkToString(shellLink); if (removedLinkString == linkString) { return true; } } } return false; } // Unlikely. It's not a supported shell interface? return false; } ////// ////// This returns a native COM object that should be deterministically released by the caller, when possible. /// ////// Critical: Calls critical methods CreateItemForJumpPath and CreateLinkforJumpTask. /// [SecurityCritical] private static object GetShellObjectForJumpItem(JumpItem jumpItem) { var jumpPath = jumpItem as JumpPath; var jumpTask = jumpItem as JumpTask; // Either of these create functions could return null if the item is invalid but they shouldn't throw. if (jumpPath != null) { return CreateItemFromJumpPath(jumpPath); } else if (jumpTask != null) { return CreateLinkFromJumpTask(jumpTask, true); } // Unsupported type? Debug.Assert(false); return null; } ////// Critical: Creates instances of native interfaces IShellItem and IShellLink. /// [SecurityCritical] private static List<_ShellObjectPair> GenerateJumpItems(IObjectArray shellObjects) { Debug.Assert(shellObjects != null); var retList = new List<_ShellObjectPair>(); Guid unknownIid = new Guid(IID.Unknown); uint count = shellObjects.GetCount(); for (uint i = 0; i < count; ++i) { // This is potentially a heterogenous list, so get as an IUnknown and QI afterwards. object unk = shellObjects.GetAt(i, ref unknownIid); JumpItem item = null; try { item = GetJumpItemForShellObject(unk); } catch (Exception e) { if (e is NullReferenceException || e is System.Runtime.InteropServices.SEHException) { throw; } // If we failed the conversion we still want to keep the shell interface for comparision. // Just leave the JumpItem property as null. } retList.Add(new _ShellObjectPair { ShellObject = unk, JumpItem = item }); } return retList; } ////// Critical: Calls critical AddCategory. /// [SecurityCritical] private static void AddCategory(ICustomDestinationList cdl, string category, List<_ShellObjectPair> jumpItems, ListsuccessList, List<_RejectedJumpItemPair> rejectionList) { AddCategory(cdl, category, jumpItems, successList, rejectionList, true); } /// /// Critical: Calls methods on native interfaces ICustomDestinationList, IShellItem, and IShellLink. /// [SecurityCritical] private static void AddCategory(ICustomDestinationList cdl, string category, List<_ShellObjectPair> jumpItems, ListsuccessList, List<_RejectedJumpItemPair> rejectionList, bool isHeterogenous) { Debug.Assert(jumpItems.Count != 0); Debug.Assert(cdl != null); HRESULT hr; var shellObjectCollection = (IObjectCollection)Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid(CLSID.EnumerableObjectCollection))); foreach (var itemMap in jumpItems) { shellObjectCollection.AddObject(itemMap.ShellObject); } if (string.IsNullOrEmpty(category)) { hr = cdl.AddUserTasks((IObjectArray)shellObjectCollection); } else { hr = cdl.AppendCategory(category, (IObjectArray)shellObjectCollection); } if (hr.Succeeded) { // Woot! Add these items to the list. // Do it in reverse order so Apply has the items in the correct order. for (int i = jumpItems.Count; --i >= 0;) { successList.Add(jumpItems[i].JumpItem); } } else { // If the list contained items that could not be added because this object isn't a handler // then drop all ShellItems and retry without them. if (isHeterogenous && hr == HRESULT.DESTS_E_NO_MATCHING_ASSOC_HANDLER) { if (TraceShell.IsEnabled) { TraceShell.Trace(TraceEventType.Error, TraceShell.RejectingJumpListCategoryBecauseNoRegisteredHandler(category)); } Utilities.SafeRelease(ref shellObjectCollection); var linksOnlyList = new List<_ShellObjectPair>(); foreach (var itemMap in jumpItems) { if (itemMap.JumpItem is JumpPath) { rejectionList.Add(new _RejectedJumpItemPair { JumpItem = itemMap.JumpItem, Reason = JumpItemRejectionReason.NoRegisteredHandler }); } else { linksOnlyList.Add(itemMap); } } if (linksOnlyList.Count > 0) { // There's not a reason I know of that we should reject a list of only links... Debug.Assert(jumpItems.Count != linksOnlyList.Count); AddCategory(cdl, category, linksOnlyList, successList, rejectionList, false); } } else { Debug.Assert(HRESULT.DESTS_E_NO_MATCHING_ASSOC_HANDLER != hr); // If we failed for some other reason, just reject everything. foreach (var item in jumpItems) { rejectionList.Add(new _RejectedJumpItemPair { JumpItem = item.JumpItem, Reason = JumpItemRejectionReason.InvalidItem }); } } } } /// /// Critical: Assigned in an elevated context. /// [SecurityCritical] private static readonly string _FullName; #region Converter methods ////// Critical: Creates instance of native interface IShellLinkW /// [SecurityCritical] private static IShellLinkW CreateLinkFromJumpTask(JumpTask jumpTask, bool allowSeparators) { Debug.Assert(jumpTask != null); // Title is generally required. If it's missing we need to treat this like a separator. // Everything else can still appear on separator elements, // but separators can only exist in the Tasks category. if (string.IsNullOrEmpty(jumpTask.Title)) { if (!allowSeparators || !string.IsNullOrEmpty(jumpTask.CustomCategory)) { // Just treat this situation as an InvalidItem. return null; } } var link = (IShellLinkW)Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid(CLSID.ShellLink))); try { string appPath = _FullName; if (!string.IsNullOrEmpty(jumpTask.ApplicationPath)) { appPath = jumpTask.ApplicationPath; } link.SetPath(appPath); // This is optional. Don't set it if the app hasn't explicitly requested it. if (!string.IsNullOrEmpty(jumpTask.WorkingDirectory)) { // Don't verify this. It's possible that the directory doesn't exist now, but it will later. // Shell handles this fine when we try to set an improperly formatted path. link.SetWorkingDirectory(jumpTask.WorkingDirectory); } if (!string.IsNullOrEmpty(jumpTask.Arguments)) { link.SetArguments(jumpTask.Arguments); } // -1 is a sentinel value indicating not to use the icon. if (jumpTask.IconResourceIndex != -1) { string resourcePath = _FullName; if (!string.IsNullOrEmpty(jumpTask.IconResourcePath)) { // Shell bug (Windows 7 595770): IShellLink doesn't correctly limit icon location path to MAX_PATH. // It's really too bad we have to enforce this here. When the shortcut gets // serialized it streams the full string. On deserialization it only retrieves // MAX_PATH for this field leaving junk behind for subsequent gets, leading to data corruption. // Because we don't want to allow the app to do create something that we know may // be corrupt we have to enforce this ourselves. If Shell fixes this later then // we need to remove this check to let them handle this as they see fit. // If they fix it by supporting longer paths, then we're artificially constraining this value... if (jumpTask.IconResourcePath.Length >= Win32Constant.MAX_PATH) { // we could throw the exception here, but we're already globally catching everything. return null; } resourcePath = jumpTask.IconResourcePath; } link.SetIconLocation(resourcePath, jumpTask.IconResourceIndex); } if (!string.IsNullOrEmpty(jumpTask.Description)) { link.SetDescription(jumpTask.Description); } IPropertyStore propStore = (IPropertyStore)link; var pv = new PROPVARIANT(); try { PKEY pkey = default(PKEY); if (!string.IsNullOrEmpty(jumpTask.Title)) { pv.SetValue(jumpTask.Title); pkey = PKEY.Title; } else { pv.SetValue(true); pkey = PKEY.AppUserModel_IsDestListSeparator; } propStore.SetValue(ref pkey, pv); } finally { Utilities.SafeDispose(ref pv); } propStore.Commit(); IShellLinkW retLink = link; link = null; return retLink; } catch (Exception) { // IShellLinkW::Set* methods tend to return E_FAIL when trying to set invalid data. // The create methods don't explicitly check for these kinds of errors. // If we aren't able to create the item for any reason just return null to indicate an invalid item. return null; } finally { Utilities.SafeRelease(ref link); } } ////// Critical: Creates instance of native interface IShellItem2. /// [SecurityCritical] private static IShellItem2 CreateItemFromJumpPath(JumpPath jumpPath) { Debug.Assert(jumpPath != null); try { // This will return null if the path doesn't exist. return ShellUtil.GetShellItemForPath(Path.GetFullPath(jumpPath.Path)); } catch (Exception) { // Don't propagate exceptions here. If we couldn't create the item, it's just invalid. } return null; } ////// Critical: Accesses methods on native interfaces IShellItem and IShellLink. /// [SecurityCritical] private static JumpItem GetJumpItemForShellObject(object shellObject) { var shellItem = shellObject as IShellItem2; var shellLink = shellObject as IShellLinkW; if (shellItem != null) { JumpPath path = new JumpPath { Path = shellItem.GetDisplayName(SIGDN.DESKTOPABSOLUTEPARSING), }; return path; } if (shellLink != null) { var pathBuilder = new StringBuilder(Win32Constant.MAX_PATH); shellLink.GetPath(pathBuilder, pathBuilder.Capacity, null, SLGP.RAWPATH); var argsBuilder = new StringBuilder(Win32Constant.INFOTIPSIZE); shellLink.GetArguments(argsBuilder, argsBuilder.Capacity); var descBuilder = new StringBuilder(Win32Constant.INFOTIPSIZE); shellLink.GetDescription(descBuilder, descBuilder.Capacity); var iconBuilder = new StringBuilder(Win32Constant.MAX_PATH); int iconIndex; shellLink.GetIconLocation(iconBuilder, iconBuilder.Capacity, out iconIndex); var dirBuilder = new StringBuilder(Win32Constant.MAX_PATH); shellLink.GetWorkingDirectory(dirBuilder, dirBuilder.Capacity); JumpTask task = new JumpTask { // Set ApplicationPath and IconResources, even if they're from the current application. // This means that equivalent JumpTasks won't necessarily compare property-for-property. ApplicationPath = pathBuilder.ToString(), Arguments = argsBuilder.ToString(), Description = descBuilder.ToString(), IconResourceIndex = iconIndex, IconResourcePath = iconBuilder.ToString(), WorkingDirectory = dirBuilder.ToString(), }; using (PROPVARIANT pv = new PROPVARIANT()) { var propStore = (IPropertyStore)shellLink; PKEY pkeyTitle = PKEY.Title; propStore.GetValue(ref pkeyTitle, pv); // PKEY_Title should be an LPWSTR if it's not empty. task.Title = pv.GetValue() ?? ""; } return task; } // Unsupported type? Debug.Assert(false); return null; } ////// Generate a unique string for the ShellLink that can be used for equality checks. /// [SecurityCritical] private static string ShellLinkToString(IShellLinkW shellLink) { var pathBuilder = new StringBuilder(Win32Constant.MAX_PATH); shellLink.GetPath(pathBuilder, pathBuilder.Capacity, null, SLGP.RAWPATH); string title = null; // Need to use the property store to get the title for the link. using (PROPVARIANT pv = new PROPVARIANT()) { var propStore = (IPropertyStore)shellLink; PKEY pkeyTitle = PKEY.Title; propStore.GetValue(ref pkeyTitle, pv); // PKEY_Title should be an LPWSTR if it's not empty. title = pv.GetValue() ?? ""; } var argsBuilder = new StringBuilder(Win32Constant.INFOTIPSIZE); shellLink.GetArguments(argsBuilder, argsBuilder.Capacity); // Path and title should be case insensitive. // Shell treats arguments as case sensitive because apps can handle those differently. return pathBuilder.ToString().ToUpperInvariant() + title.ToUpperInvariant() + argsBuilder.ToString(); } #endregion private void RejectEverything() { EventHandlerhandler = JumpItemsRejected; if (handler == null) { _jumpItems.Clear(); return; } if (_jumpItems.Count > 0) { var reasons = new List (JumpItems.Count); for (int i = 0; i < JumpItems.Count; ++i) { reasons.Add(JumpItemRejectionReason.InvalidItem); } // We're rejecting everything, // so create an event args with the original list and then clear it. var args = new JumpItemsRejectedEventArgs(JumpItems, reasons); _jumpItems.Clear(); handler(this, args); } } public event EventHandler JumpItemsRejected; public event EventHandler JumpItemsRemovedByUser; } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved. /**************************************************************************\ Copyright Microsoft Corporation. All Rights Reserved. \**************************************************************************/ namespace System.Windows.Shell { using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Security; using System.Security.Permissions; using System.Text; using System.Threading; using System.Windows; using System.Windows.Markup; using MS.Internal; using MS.Internal.AppModel; using MS.Internal.PresentationFramework; using MS.Internal.Interop; using MS.Win32; using HRESULT = MS.Internal.Interop.HRESULT; /// /// The list of possible reasons why a JumpItem would be rejected from a JumpList when applied. /// public enum JumpItemRejectionReason { ////// Unknown reason. This should not be used. /// None, ////// The item was rejected because it was invalid for a jump list. E.g. the file path didn't exist. /// ////// If the application is running on a system where jump lists are not available (like XP or Vista) /// items will get rejected with this reason. /// InvalidItem, ////// The item was rejected because the program was not registered to handle the file extension. /// NoRegisteredHandler, ////// The item was rejected because the user had explicitly removed it since the last time a JumpList was applied. /// RemovedByUser, } ////// EventArgs for JumpList.JumpItemsRejected event. /// public sealed class JumpItemsRejectedEventArgs : EventArgs { public JumpItemsRejectedEventArgs() : this(null, null) { } public JumpItemsRejectedEventArgs(IListrejectedItems, IList reasons) { // If one of the collections is null then the other has to be, too. if ((rejectedItems == null && reasons != null) || (reasons == null && rejectedItems != null) || (rejectedItems != null && reasons != null && rejectedItems.Count != reasons.Count)) { throw new ArgumentException(SR.Get(SRID.JumpItemsRejectedEventArgs_CountMismatch)); } // We don't want the contents of the list getting modified in the event handler, // so use a read-only copy if (rejectedItems != null) { RejectedItems = new List (rejectedItems).AsReadOnly(); RejectionReasons = new List (reasons).AsReadOnly(); } else { RejectedItems = new List ().AsReadOnly(); RejectionReasons = new List ().AsReadOnly(); } } public IList RejectedItems { get; private set; } public IList RejectionReasons { get; private set; } } /// /// EventArgs for JumpList.JumpItemsRemovedByUser event. /// public sealed class JumpItemsRemovedEventArgs : EventArgs { public JumpItemsRemovedEventArgs() : this(null) { } public JumpItemsRemovedEventArgs(IListremovedItems) { if (removedItems != null) { RemovedItems = new List (removedItems).AsReadOnly(); } else { RemovedItems = new List ().AsReadOnly(); } } public IList RemovedItems { get; private set; } } /// /// Manage the tasks and files that Shell associates with this application. /// This allows modification of the Jump List UI in Windows 7 that appears on the Taskbar and Start Menu. /// ////// This class is unsafe to use in partial trust. It modifies the information Shell presents to the user about the application. /// [UIPermission(SecurityAction.LinkDemand, Window=UIPermissionWindow.AllWindows)] [ContentProperty("JumpItems")] public sealed class JumpList : ISupportInitialize { ////// Critical: Calls critical native methods. Class-level LinkDemand doesn't apply to static ctor. /// TreatAsSafe: Added explicit demand for AllWindows to the static ctor. /// [SecurityCritical, SecurityTreatAsSafe] [UIPermission(SecurityAction.Demand, Window=UIPermissionWindow.AllWindows)] static JumpList() { // Passing NULL for the HMODULE returns the running executable path. _FullName = UnsafeNativeMethods.GetModuleFileName(new HandleRef()); } ////// Add the item at the specified file path to the application's JumpList's recent items. /// ////// This makes the item eligible for inclusion in the special Recent and Frequent categories. /// ////// Critical: Calls critical SHAddToRecentDocs. /// PublicOK: The class is only available in full trust. /// [SecurityCritical] public static void AddToRecentCategory(string itemPath) { Verify.FileExists(itemPath, "itemPath"); itemPath = Path.GetFullPath(itemPath); NativeMethods2.SHAddToRecentDocs(itemPath); } ////// Add the item to the application's JumpList's recent items. /// ////// This makes the item eligible for inclusion in the special Recent and Frequent categories. /// ////// Critical: Calls critical SHAddToRecentDocs. /// PublicOK: The class is only available in full trust. /// [SecurityCritical] public static void AddToRecentCategory(JumpPath jumpPath) { Verify.IsNotNull(jumpPath, "jumpPath"); AddToRecentCategory(jumpPath.Path); } ////// Add the task at the specified file path to the application's JumpList's recent items. /// ////// This makes the item eligible for inclusion in the special Recent and Frequent categories. /// ////// Critical: Calls critical SHAddToRecentDocs. /// PublicOK: The class is only available in full trust. /// [SecurityCritical] public static void AddToRecentCategory(JumpTask jumpTask) { Verify.IsNotNull(jumpTask, "jumpTask"); // SHAddToRecentDocs only allows IShellLinks in Windows 7 and later. // Silently fail this if that's not the case. // We don't give feedback on success here, so this is okay. if (Utilities.IsOSWindows7OrNewer) { IShellLinkW shellLink = CreateLinkFromJumpTask(jumpTask, false); try { if (shellLink != null) { NativeMethods2.SHAddToRecentDocs(shellLink); } } finally { Utilities.SafeRelease(ref shellLink); } } } private class _RejectedJumpItemPair { public JumpItem JumpItem { get; set; } public JumpItemRejectionReason Reason { get; set; } } private class _ShellObjectPair { // JumpPath/JumpTask public JumpItem JumpItem { get; set; } // IShellItem/IShellLink public object ShellObject { get; set; } ////// Releases all native references in a list of _ShellObjectPairs. /// ////// Critical: Calls critical Utilities.SafeRelease. /// /// The list from which to release the resources. [SecurityCritical] public static void ReleaseShellObjects(List<_ShellObjectPair> list) { if (list != null) { foreach (_ShellObjectPair shellMap in list) { object o = shellMap.ShellObject; shellMap.ShellObject = null; Utilities.SafeRelease(ref o); } } } } #region Attached Property Methods ////// Set the JumpList attached property on an Application. /// public static void SetJumpList(Application application, JumpList value) { Verify.IsNotNull(application, "application"); lock (s_lock) { // If this was associated with a different application, remove the association. JumpList oldValue; if (s_applicationMap.TryGetValue(application, out oldValue) && oldValue != null) { oldValue._application = null; } // Associate the jumplist with the application so we can retrieve it later. s_applicationMap[application] = value; if (value != null) { value._application = application; } } if (value != null) { // Changes will only get applied if the list isn't in an ISupportInitialize block. value.ApplyFromApplication(); } } ////// Get the JumpList attached property for an Application. /// public static JumpList GetJumpList(Application application) { Verify.IsNotNull(application, "application"); JumpList value; s_applicationMap.TryGetValue(application, out value); return value; } #endregion // static lock to ensure integrity when modifying instances as attached properties on Application. private static readonly object s_lock = new object(); private static readonly Dictionarys_applicationMap = new Dictionary (); private Application _application; // Used to enforce the ISupportInitialize contract. It's not required to BeginInit to use this class, // but it is useful for XAML scenarios so we can apply the changes when both EndInit has been called // and the Application attached property has been set. private bool? _initializing; // The internal list of JumpItems in this JumpList private List _jumpItems; /// /// Critical: This class cannot be used from partial trust. /// PublicOK: The class is only available in full trust. /// [SecurityCritical] public JumpList() : this(null, false, false) { // Restore the ability to use ISupportInitialize. _initializing = null; } ////// Critical: This class cannot be used from partial trust. /// PublicOK: The class is only available in full trust. /// [SecurityCritical] public JumpList(IEnumerableitems, bool showFrequent, bool showRecent) { if (items != null) { _jumpItems = new List (items); } else { _jumpItems = new List (); } ShowFrequentCategory = showFrequent; ShowRecentCategory = showRecent; // Using this constructor precludes using ISupportInitialize. _initializing = false; } /// /// Whether to show the special "Frequent" category. /// ////// This category is managed by the Shell and keeps track of items that are frequently accessed by this program. /// Applications can request that specific items are included here by calling JumpList.AddToRecentCategory. /// Because of duplication, applications generally should not have both ShowRecentCategory and ShowFrequentCategory set at the same time. /// public bool ShowFrequentCategory { get; set; } ////// Whether to show the special "Recent" category. /// ////// This category is managed by the Shell and keeps track of items that have been recently accessed by this program. /// Applications can request that specific items are included here by calling JumpList.AddToRecentCategory /// Because of duplication, applications generally should not have both ShowRecentCategory and ShowFrequentCategory set at the same time. /// public bool ShowRecentCategory { get; set; } ////// The list of JumpItems to be in the JumpList. After a call to Apply this list will contain only those items that were successfully added. /// ////// This object is not guaranteed to retain its identity after a call to Apply or other implicit setting of the JumpList. /// It should be requeried at such times. /// public ListJumpItems { get { return _jumpItems; } } private bool IsUnmodified { get { return _initializing == null && JumpItems.Count == 0 && !ShowRecentCategory && !ShowFrequentCategory; } } #region ISupportInitialize Members /// /// Prepare the JumpList for modification. /// ////// This works in concert with the Application.JumpList attached property. The JumpList will automatically be applied /// to the current application when attached and a corresponding call to EndInit is made. /// Nested calls to BeginInit are not allowed. /// public void BeginInit() { if (!IsUnmodified) { throw new InvalidOperationException(SR.Get(SRID.JumpList_CantNestBeginInitCalls)); } _initializing = true; } ////// Signal the end of initialization of this JumpList. If it is attached to the current Application, apply the contents of the jump list. /// ////// Calls to EndInit must be paired with calls to BeginInit. /// public void EndInit() { if (_initializing != true) { throw new NotSupportedException(SR.Get(SRID.JumpList_CantCallUnbalancedEndInit)); } _initializing = false; // EndInit only implicitly applies the list if the current Application has been set as an attached property. ApplyFromApplication(); } #endregion ////// Get the AppUserModelId for the running process. /// ////// This is a Shell property that currently is only used as part of a heuristic /// for what taskbar item an HWND should be associated with, e.g. you can put /// windows from multiple processes into the same group, or you can prevent glomming /// of HWNDs that would otherwise be shown together. /// /// Even though this property isn't exposed on the public WPF OM /// we still want to make sure that the jump list gets associated with /// the current running app even if the client has explicitly changed the id. /// /// It's straightforward to p/invoke to set these for the running application or /// the HWND. Not so much for this object. /// ////// Critical: Calls critical GetCurrentProcessExplicitAppUserModelID. /// private static string _RuntimeId { [SecurityCritical] get { string appId; HRESULT hr = NativeMethods2.GetCurrentProcessExplicitAppUserModelID(out appId); if (hr == HRESULT.E_FAIL) { // This is how Shell signals that the app id hasn't been set. hr = HRESULT.S_OK; appId = null; } hr.ThrowIfFailed(); return appId; } } ////// Critical: Calls critical method ApplyList. /// PublicOK: This class is only available in full trust. /// [SecurityCritical] public void Apply() { if (_initializing == true) { throw new InvalidOperationException(SR.Get(SRID.JumpList_CantApplyUntilEndInit)); } // After this attempting to use ISupportInitialize is invalid. _initializing = false; ApplyList(); } ////// Critical: Calls critical method ApplyList. /// TreatAsSafe: This class is only available in full trust. /// [SecurityCritical, SecurityTreatAsSafe] private void ApplyFromApplication() { // If we're here and the caller has modified the JumpList without using ISupportInitialize // we still want to apply the changes. if (_initializing != true && !IsUnmodified) { _initializing = false; } if (_application == Application.Current && _initializing == false) { // If we're applying due to being an attached property, then don't apply // unless we're really on the current application and wait until EndInit // has been called, or the list has otherwise been modified. ApplyList(); } } ////// Critical: Uses native methods to apply JumpItem data to the Shell's JumpList for the current application. /// [SecurityCritical] private void ApplyList() { Debug.Assert(_initializing == false); Verify.IsApartmentState(ApartmentState.STA); // We don't want to force applications to conditionally check this before constructing a JumpList, // but if we're not on 7 then this isn't going to work. Fail fast. if (!Utilities.IsOSWindows7OrNewer) { RejectEverything(); return; } ListsuccessList; List<_RejectedJumpItemPair> rejectedList; // Declare these outside the try block so we can cleanup native resources in the _ShellObjectPairs. List > categories = null; List<_ShellObjectPair> removedList = null; var destinationList = (ICustomDestinationList)Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid(CLSID.DestinationList))); try { // Even though we're not exposing Shell's AppModelId concept, we'll still respect it // since it's easy to clients to p/invoke to set it for Application and Window, but not for JumpLists string appId = _RuntimeId; if (!string.IsNullOrEmpty(appId)) { destinationList.SetAppID(appId); } // The number ot items visible on a jump list is a user setting. Shell doesn't reject items based on overflow. // We don't bother checking against it because the app can query the setting and manage overflow based on it // if they really care. We'll happily add too many items with the hope that if the user changes the setting // items will be recovered from the overflow. uint slotsVisible; Guid removedIid = new Guid(IID.ObjectArray); var objectsRemoved = (IObjectArray)destinationList.BeginList(out slotsVisible, ref removedIid); // Keep track of the items that were previously removed by the user. // We don't want to pend any items that are contained in this list. // It's possible that this contains null JumpItems when we were unable to do the conversion. removedList = GenerateJumpItems(objectsRemoved); // Keep track of the items that have been successfully pended. // IMPORTANT: Ensure at the end of this that if the list is applied again that it would // result in items being added to the JumpList in the same order. // This doesn't mean that they'll be ordered the same as how the user added them // (e.g. categories will be coalesced), but the categories should appear in the same order. // Since when we call AddCategory we're doing it in reverse order, AddCategory augments // the items in the list in reverse as well. At the end the final list is reversed. successList = new List
(JumpItems.Count); // Keep track of the items that we couldn't pend, and why. rejectedList = new List<_RejectedJumpItemPair>(JumpItems.Count); // Need to group the JumpItems based on their categories. // The special "Tasks" category doesn't actually have a name and should be first so it's unconditionally added. categories = new List >() { new List<_ShellObjectPair>() }; // This is not thread-safe. // We're traversing the original list so we're vulnerable to another thread modifying it during the enumeration. foreach (var jumpItem in JumpItems) { if (jumpItem == null) { // App added a null jump item? Just go through the normal failure mechanisms. rejectedList.Add(new _RejectedJumpItemPair{ JumpItem = jumpItem, Reason = JumpItemRejectionReason.InvalidItem }); continue; } object shellObject = null; try { shellObject = GetShellObjectForJumpItem(jumpItem); // If for some reason we couldn't create the item add it to the rejected list. if (shellObject == null) { rejectedList.Add(new _RejectedJumpItemPair { Reason = JumpItemRejectionReason.InvalidItem, JumpItem = jumpItem }); continue; } // Don't add this item if it's in the list of items previously removed by the user. if (ListContainsShellObject(removedList, shellObject)) { rejectedList.Add(new _RejectedJumpItemPair { Reason = JumpItemRejectionReason.RemovedByUser, JumpItem = jumpItem }); continue; } var shellMap = new _ShellObjectPair { JumpItem = jumpItem, ShellObject = shellObject }; if (string.IsNullOrEmpty(jumpItem.CustomCategory)) { // No custom category, so add to the Tasks list. categories[0].Add(shellMap); } else { // Find the appropriate category and add to that list. // If it doesn't exist, add a new category for it. bool categoryExists = false; foreach (var list in categories) { // The first item in the category list can be used to check the name. if (list.Count > 0 && list[0].JumpItem.CustomCategory == jumpItem.CustomCategory) { list.Add(shellMap); categoryExists = true; break; } } if (!categoryExists) { categories.Add(new List<_ShellObjectPair>() { shellMap }); } } // Shell interface is now owned by the category list. shellObject = null; } finally { Utilities.SafeRelease(ref shellObject); } } // Jump List categories get added top-down, except for "Tasks" which is special and always at the bottom. // We want the Recent/Frequent to always be at the top so they get added first. // Logically the categories are added bottom-up, but their contents are top-down, // so we reverse the order we add the categories to the destinationList. // To preserve the item ordering AddCategory also adds items in reverse. // We need to reverse the final list when everything is done. categories.Reverse(); if (ShowFrequentCategory) { destinationList.AppendKnownCategory(KDC.FREQUENT); } if (ShowRecentCategory) { destinationList.AppendKnownCategory(KDC.RECENT); } // Now that all the JumpItems are grouped add them to the custom destinations list. foreach (List<_ShellObjectPair> categoryList in categories) { if (categoryList.Count > 0) { string categoryHeader = categoryList[0].JumpItem.CustomCategory; AddCategory(destinationList, categoryHeader, categoryList, successList, rejectedList); } } destinationList.CommitList(); } catch { // It's not okay to throw an exception here. If Shell is rejecting the JumpList for some reason // we don't want to be responsible for the app throwing an exception in its startup path. // For common use patterns there isn't really any user code on the stack, so there isn't // an opportunity to catch this in the app. // We can instead handle this. // Try to notify the developer if they're hitting this. if (TraceShell.IsEnabled) { TraceShell.Trace(TraceEventType.Error, TraceShell.RejectingJumpItemsBecauseCatastrophicFailure); } RejectEverything(); return; } finally { // Deterministically release native resources. Utilities.SafeRelease(ref destinationList); if (categories != null) { foreach (List<_ShellObjectPair> list in categories) { _ShellObjectPair.ReleaseShellObjects(list); } } // Note that this only clears the ShellObjects, not the JumpItems. // We still need the JumpItems out of this list for the JumpItemsRemovedByUser event. _ShellObjectPair.ReleaseShellObjects(removedList); } // Swap the current list with what we were able to successfully place into the JumpList. // Reverse it first to ensure that the items are in a repeatable order. successList.Reverse(); _jumpItems = successList; // Raise the events for rejected and removed. EventHandler
rejectedHandler = JumpItemsRejected; EventHandler removedHandler = JumpItemsRemovedByUser; if (rejectedList.Count > 0 && rejectedHandler != null) { var items = new List (rejectedList.Count); var reasons = new List (rejectedList.Count); foreach (_RejectedJumpItemPair rejectionPair in rejectedList) { items.Add(rejectionPair.JumpItem); reasons.Add(rejectionPair.Reason); } rejectedHandler(this, new JumpItemsRejectedEventArgs(items, reasons)); } if (removedList.Count > 0 && removedHandler != null) { var items = new List (removedList.Count); foreach (_ShellObjectPair shellMap in removedList) { // It's possible that not every shell object could be converted to a JumpItem. if (shellMap.JumpItem != null) { items.Add(shellMap.JumpItem); } } if (items.Count > 0) { removedHandler(this, new JumpItemsRemovedEventArgs(items)); } } } /// /// Critical: This accesses methods on native interfaces IShellLink and IShellItem. /// TreatAsSafe: This is checking for equality in a provided list. This information is safe. /// [SecurityCritical, SecurityTreatAsSafe] private static bool ListContainsShellObject(List<_ShellObjectPair> removedList, object shellObject) { Debug.Assert(removedList != null); Debug.Assert(shellObject != null); if (removedList.Count == 0) { return false; } // Casts in .Net don't AddRef. Don't need to release these. var shellItem = shellObject as IShellItem; if (shellItem != null) { foreach (var shellMap in removedList) { var removedItem = shellMap.ShellObject as IShellItem; if (removedItem != null) { if (0 == shellItem.Compare(removedItem, SICHINT.CANONICAL | SICHINT.TEST_FILESYSPATH_IF_NOT_EQUAL)) { return true; } } } return false; } var shellLink = shellObject as IShellLinkW; if (shellLink != null) { foreach (var shellMap in removedList) { var removedLink = shellMap.ShellObject as IShellLinkW; if (removedLink != null) { // There's no intrinsic comparison function for ShellLinks. // Talking to the Shell guys, the way they compare these is to catenate a string with // a normalized version of the app path and the unmodified args. // If the two strings ordinally compare, they're the same. string removedLinkString = ShellLinkToString(removedLink); string linkString = ShellLinkToString(shellLink); if (removedLinkString == linkString) { return true; } } } return false; } // Unlikely. It's not a supported shell interface? return false; } ////// ////// This returns a native COM object that should be deterministically released by the caller, when possible. /// ////// Critical: Calls critical methods CreateItemForJumpPath and CreateLinkforJumpTask. /// [SecurityCritical] private static object GetShellObjectForJumpItem(JumpItem jumpItem) { var jumpPath = jumpItem as JumpPath; var jumpTask = jumpItem as JumpTask; // Either of these create functions could return null if the item is invalid but they shouldn't throw. if (jumpPath != null) { return CreateItemFromJumpPath(jumpPath); } else if (jumpTask != null) { return CreateLinkFromJumpTask(jumpTask, true); } // Unsupported type? Debug.Assert(false); return null; } ////// Critical: Creates instances of native interfaces IShellItem and IShellLink. /// [SecurityCritical] private static List<_ShellObjectPair> GenerateJumpItems(IObjectArray shellObjects) { Debug.Assert(shellObjects != null); var retList = new List<_ShellObjectPair>(); Guid unknownIid = new Guid(IID.Unknown); uint count = shellObjects.GetCount(); for (uint i = 0; i < count; ++i) { // This is potentially a heterogenous list, so get as an IUnknown and QI afterwards. object unk = shellObjects.GetAt(i, ref unknownIid); JumpItem item = null; try { item = GetJumpItemForShellObject(unk); } catch (Exception e) { if (e is NullReferenceException || e is System.Runtime.InteropServices.SEHException) { throw; } // If we failed the conversion we still want to keep the shell interface for comparision. // Just leave the JumpItem property as null. } retList.Add(new _ShellObjectPair { ShellObject = unk, JumpItem = item }); } return retList; } ////// Critical: Calls critical AddCategory. /// [SecurityCritical] private static void AddCategory(ICustomDestinationList cdl, string category, List<_ShellObjectPair> jumpItems, ListsuccessList, List<_RejectedJumpItemPair> rejectionList) { AddCategory(cdl, category, jumpItems, successList, rejectionList, true); } /// /// Critical: Calls methods on native interfaces ICustomDestinationList, IShellItem, and IShellLink. /// [SecurityCritical] private static void AddCategory(ICustomDestinationList cdl, string category, List<_ShellObjectPair> jumpItems, ListsuccessList, List<_RejectedJumpItemPair> rejectionList, bool isHeterogenous) { Debug.Assert(jumpItems.Count != 0); Debug.Assert(cdl != null); HRESULT hr; var shellObjectCollection = (IObjectCollection)Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid(CLSID.EnumerableObjectCollection))); foreach (var itemMap in jumpItems) { shellObjectCollection.AddObject(itemMap.ShellObject); } if (string.IsNullOrEmpty(category)) { hr = cdl.AddUserTasks((IObjectArray)shellObjectCollection); } else { hr = cdl.AppendCategory(category, (IObjectArray)shellObjectCollection); } if (hr.Succeeded) { // Woot! Add these items to the list. // Do it in reverse order so Apply has the items in the correct order. for (int i = jumpItems.Count; --i >= 0;) { successList.Add(jumpItems[i].JumpItem); } } else { // If the list contained items that could not be added because this object isn't a handler // then drop all ShellItems and retry without them. if (isHeterogenous && hr == HRESULT.DESTS_E_NO_MATCHING_ASSOC_HANDLER) { if (TraceShell.IsEnabled) { TraceShell.Trace(TraceEventType.Error, TraceShell.RejectingJumpListCategoryBecauseNoRegisteredHandler(category)); } Utilities.SafeRelease(ref shellObjectCollection); var linksOnlyList = new List<_ShellObjectPair>(); foreach (var itemMap in jumpItems) { if (itemMap.JumpItem is JumpPath) { rejectionList.Add(new _RejectedJumpItemPair { JumpItem = itemMap.JumpItem, Reason = JumpItemRejectionReason.NoRegisteredHandler }); } else { linksOnlyList.Add(itemMap); } } if (linksOnlyList.Count > 0) { // There's not a reason I know of that we should reject a list of only links... Debug.Assert(jumpItems.Count != linksOnlyList.Count); AddCategory(cdl, category, linksOnlyList, successList, rejectionList, false); } } else { Debug.Assert(HRESULT.DESTS_E_NO_MATCHING_ASSOC_HANDLER != hr); // If we failed for some other reason, just reject everything. foreach (var item in jumpItems) { rejectionList.Add(new _RejectedJumpItemPair { JumpItem = item.JumpItem, Reason = JumpItemRejectionReason.InvalidItem }); } } } } /// /// Critical: Assigned in an elevated context. /// [SecurityCritical] private static readonly string _FullName; #region Converter methods ////// Critical: Creates instance of native interface IShellLinkW /// [SecurityCritical] private static IShellLinkW CreateLinkFromJumpTask(JumpTask jumpTask, bool allowSeparators) { Debug.Assert(jumpTask != null); // Title is generally required. If it's missing we need to treat this like a separator. // Everything else can still appear on separator elements, // but separators can only exist in the Tasks category. if (string.IsNullOrEmpty(jumpTask.Title)) { if (!allowSeparators || !string.IsNullOrEmpty(jumpTask.CustomCategory)) { // Just treat this situation as an InvalidItem. return null; } } var link = (IShellLinkW)Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid(CLSID.ShellLink))); try { string appPath = _FullName; if (!string.IsNullOrEmpty(jumpTask.ApplicationPath)) { appPath = jumpTask.ApplicationPath; } link.SetPath(appPath); // This is optional. Don't set it if the app hasn't explicitly requested it. if (!string.IsNullOrEmpty(jumpTask.WorkingDirectory)) { // Don't verify this. It's possible that the directory doesn't exist now, but it will later. // Shell handles this fine when we try to set an improperly formatted path. link.SetWorkingDirectory(jumpTask.WorkingDirectory); } if (!string.IsNullOrEmpty(jumpTask.Arguments)) { link.SetArguments(jumpTask.Arguments); } // -1 is a sentinel value indicating not to use the icon. if (jumpTask.IconResourceIndex != -1) { string resourcePath = _FullName; if (!string.IsNullOrEmpty(jumpTask.IconResourcePath)) { // Shell bug (Windows 7 595770): IShellLink doesn't correctly limit icon location path to MAX_PATH. // It's really too bad we have to enforce this here. When the shortcut gets // serialized it streams the full string. On deserialization it only retrieves // MAX_PATH for this field leaving junk behind for subsequent gets, leading to data corruption. // Because we don't want to allow the app to do create something that we know may // be corrupt we have to enforce this ourselves. If Shell fixes this later then // we need to remove this check to let them handle this as they see fit. // If they fix it by supporting longer paths, then we're artificially constraining this value... if (jumpTask.IconResourcePath.Length >= Win32Constant.MAX_PATH) { // we could throw the exception here, but we're already globally catching everything. return null; } resourcePath = jumpTask.IconResourcePath; } link.SetIconLocation(resourcePath, jumpTask.IconResourceIndex); } if (!string.IsNullOrEmpty(jumpTask.Description)) { link.SetDescription(jumpTask.Description); } IPropertyStore propStore = (IPropertyStore)link; var pv = new PROPVARIANT(); try { PKEY pkey = default(PKEY); if (!string.IsNullOrEmpty(jumpTask.Title)) { pv.SetValue(jumpTask.Title); pkey = PKEY.Title; } else { pv.SetValue(true); pkey = PKEY.AppUserModel_IsDestListSeparator; } propStore.SetValue(ref pkey, pv); } finally { Utilities.SafeDispose(ref pv); } propStore.Commit(); IShellLinkW retLink = link; link = null; return retLink; } catch (Exception) { // IShellLinkW::Set* methods tend to return E_FAIL when trying to set invalid data. // The create methods don't explicitly check for these kinds of errors. // If we aren't able to create the item for any reason just return null to indicate an invalid item. return null; } finally { Utilities.SafeRelease(ref link); } } ////// Critical: Creates instance of native interface IShellItem2. /// [SecurityCritical] private static IShellItem2 CreateItemFromJumpPath(JumpPath jumpPath) { Debug.Assert(jumpPath != null); try { // This will return null if the path doesn't exist. return ShellUtil.GetShellItemForPath(Path.GetFullPath(jumpPath.Path)); } catch (Exception) { // Don't propagate exceptions here. If we couldn't create the item, it's just invalid. } return null; } ////// Critical: Accesses methods on native interfaces IShellItem and IShellLink. /// [SecurityCritical] private static JumpItem GetJumpItemForShellObject(object shellObject) { var shellItem = shellObject as IShellItem2; var shellLink = shellObject as IShellLinkW; if (shellItem != null) { JumpPath path = new JumpPath { Path = shellItem.GetDisplayName(SIGDN.DESKTOPABSOLUTEPARSING), }; return path; } if (shellLink != null) { var pathBuilder = new StringBuilder(Win32Constant.MAX_PATH); shellLink.GetPath(pathBuilder, pathBuilder.Capacity, null, SLGP.RAWPATH); var argsBuilder = new StringBuilder(Win32Constant.INFOTIPSIZE); shellLink.GetArguments(argsBuilder, argsBuilder.Capacity); var descBuilder = new StringBuilder(Win32Constant.INFOTIPSIZE); shellLink.GetDescription(descBuilder, descBuilder.Capacity); var iconBuilder = new StringBuilder(Win32Constant.MAX_PATH); int iconIndex; shellLink.GetIconLocation(iconBuilder, iconBuilder.Capacity, out iconIndex); var dirBuilder = new StringBuilder(Win32Constant.MAX_PATH); shellLink.GetWorkingDirectory(dirBuilder, dirBuilder.Capacity); JumpTask task = new JumpTask { // Set ApplicationPath and IconResources, even if they're from the current application. // This means that equivalent JumpTasks won't necessarily compare property-for-property. ApplicationPath = pathBuilder.ToString(), Arguments = argsBuilder.ToString(), Description = descBuilder.ToString(), IconResourceIndex = iconIndex, IconResourcePath = iconBuilder.ToString(), WorkingDirectory = dirBuilder.ToString(), }; using (PROPVARIANT pv = new PROPVARIANT()) { var propStore = (IPropertyStore)shellLink; PKEY pkeyTitle = PKEY.Title; propStore.GetValue(ref pkeyTitle, pv); // PKEY_Title should be an LPWSTR if it's not empty. task.Title = pv.GetValue() ?? ""; } return task; } // Unsupported type? Debug.Assert(false); return null; } ////// Generate a unique string for the ShellLink that can be used for equality checks. /// [SecurityCritical] private static string ShellLinkToString(IShellLinkW shellLink) { var pathBuilder = new StringBuilder(Win32Constant.MAX_PATH); shellLink.GetPath(pathBuilder, pathBuilder.Capacity, null, SLGP.RAWPATH); string title = null; // Need to use the property store to get the title for the link. using (PROPVARIANT pv = new PROPVARIANT()) { var propStore = (IPropertyStore)shellLink; PKEY pkeyTitle = PKEY.Title; propStore.GetValue(ref pkeyTitle, pv); // PKEY_Title should be an LPWSTR if it's not empty. title = pv.GetValue() ?? ""; } var argsBuilder = new StringBuilder(Win32Constant.INFOTIPSIZE); shellLink.GetArguments(argsBuilder, argsBuilder.Capacity); // Path and title should be case insensitive. // Shell treats arguments as case sensitive because apps can handle those differently. return pathBuilder.ToString().ToUpperInvariant() + title.ToUpperInvariant() + argsBuilder.ToString(); } #endregion private void RejectEverything() { EventHandlerhandler = JumpItemsRejected; if (handler == null) { _jumpItems.Clear(); return; } if (_jumpItems.Count > 0) { var reasons = new List (JumpItems.Count); for (int i = 0; i < JumpItems.Count; ++i) { reasons.Add(JumpItemRejectionReason.InvalidItem); } // We're rejecting everything, // so create an event args with the original list and then clear it. var args = new JumpItemsRejectedEventArgs(JumpItems, reasons); _jumpItems.Clear(); handler(this, args); } } public event EventHandler JumpItemsRejected; public event EventHandler JumpItemsRemovedByUser; } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved.
Link Menu
This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- IPGlobalProperties.cs
- UInt64.cs
- Set.cs
- TargetControlTypeCache.cs
- SafeRegistryKey.cs
- ScrollPatternIdentifiers.cs
- _NetworkingPerfCounters.cs
- AssemblyResourceLoader.cs
- DbParameterHelper.cs
- DataGridColumnHeadersPresenterAutomationPeer.cs
- BaseDataListDesigner.cs
- AnnotationAuthorChangedEventArgs.cs
- HandlerFactoryWrapper.cs
- CodeConstructor.cs
- ProjectionPathSegment.cs
- MonthCalendarDesigner.cs
- RouteItem.cs
- RC2.cs
- AmbientLight.cs
- MetadataStore.cs
- RegionInfo.cs
- XmlElement.cs
- HttpHandlerAction.cs
- SqlDataSourceQuery.cs
- RoleBoolean.cs
- ToolStripProfessionalLowResolutionRenderer.cs
- ToolboxItemLoader.cs
- CancellationHandlerDesigner.cs
- rsa.cs
- ListDictionaryInternal.cs
- HttpCachePolicy.cs
- FontNamesConverter.cs
- DataServiceCollectionOfT.cs
- RoleServiceManager.cs
- GenerateTemporaryAssemblyTask.cs
- SystemIcmpV6Statistics.cs
- ScaleTransform.cs
- ManagementEventWatcher.cs
- XmlQualifiedNameTest.cs
- ToolboxItem.cs
- HyperLink.cs
- IdnMapping.cs
- TransformerConfigurationWizardBase.cs
- LogicalExpr.cs
- TextInfo.cs
- PersistChildrenAttribute.cs
- StructuredType.cs
- SqlEnums.cs
- NamespaceInfo.cs
- StatusBar.cs
- ApplicationManager.cs
- ZoneIdentityPermission.cs
- PackWebRequestFactory.cs
- HashAlgorithm.cs
- Parameter.cs
- CompositeCollection.cs
- NonVisualControlAttribute.cs
- TreeViewItem.cs
- HtmlProps.cs
- DoubleAnimationClockResource.cs
- PackageDigitalSignature.cs
- InkCanvas.cs
- IdentityNotMappedException.cs
- HandlerBase.cs
- NamespaceTable.cs
- StringCollectionEditor.cs
- ResourceDescriptionAttribute.cs
- AssemblyAttributesGoHere.cs
- Drawing.cs
- GridEntryCollection.cs
- PolicyStatement.cs
- DictionaryMarkupSerializer.cs
- ContractComponent.cs
- ButtonBaseAdapter.cs
- MobileRedirect.cs
- HtmlLink.cs
- CriticalFinalizerObject.cs
- SecurityContext.cs
- Geometry.cs
- InvalidProgramException.cs
- EditorZone.cs
- CallSiteHelpers.cs
- StylusPlugin.cs
- RootBrowserWindowProxy.cs
- EditorPartChrome.cs
- HttpContextWrapper.cs
- DependencyObjectPropertyDescriptor.cs
- BitmapEffectInputData.cs
- Comparer.cs
- LocatorPart.cs
- XamlBrushSerializer.cs
- GridPattern.cs
- IDReferencePropertyAttribute.cs
- CalendarDesigner.cs
- GridViewRowPresenterBase.cs
- DelegateHelpers.Generated.cs
- KeyTime.cs
- PhoneCall.cs
- OpCopier.cs
- ColumnWidthChangedEvent.cs