using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Windows.Forms; namespace Clipman { internal sealed class ClipmanApplicationContext : ApplicationContext { private const int ShowHotkeyId = 1001; private const int ToggleHotkeyId = 1002; private const int SaveCurrentClipboardHotkeyId = 1003; private const int ToggleHotkeyAlternateId = 1004; private const int QuickClipHotkeyId = 1005; private const int QuickCopyHotkeyBaseId = 2000; private const int SecretHotkeyBaseId = 3000; private const int MaximumClipboardSnapshotTextLength = 32 * 1024 * 1024; private const int MaximumClipboardSnapshotStreamLength = 64 * 1024 * 1024; private readonly string appDirectory; private readonly SettingsStore settingsStore; private SoundService sounds; private readonly MessageWindow messageWindow; private readonly Control invoker; private readonly NotifyIcon notifyIcon; private readonly EventWaitHandle closeEvent; private readonly EventWaitHandle showEvent; private readonly EventWaitHandle recoverEvent; private readonly EventWaitHandle pauseEvent; private readonly EventWaitHandle resumeEvent; private readonly EventWaitHandle toggleEvent; private readonly EventWaitHandle historyChangedEvent; private readonly EventWaitHandle historyAddedEvent; private readonly Thread closeThread; private readonly Thread showThread; private readonly Thread recoverThread; private readonly Thread pauseThread; private readonly Thread resumeThread; private readonly Thread toggleThread; private readonly Thread historyChangedThread; private readonly Thread historyAddedThread; private readonly ClipboardFloodGuardRegistry clipboardFloodGuards = new ClipboardFloodGuardRegistry(); private readonly ClipboardNotificationState clipboardNotifications = new ClipboardNotificationState(); private readonly ClipMergeDetector clipMergeDetector = new ClipMergeDetector(); private readonly WebsiteTitleDiagnostics websiteTitleDiagnostics = new WebsiteTitleDiagnostics(); private readonly object automaticWebsiteTitleLock = new object(); private readonly Queue automaticWebsiteTitleQueue = new Queue(); private readonly HashSet automaticWebsiteTitleEntryIds = new HashSet(StringComparer.Ordinal); private bool automaticWebsiteTitleWorkerRunning; private FileSystemWatcher sharedStateWatcher; private FileSystemWatcher executableWatcher; private System.Threading.Timer sharedStateTimer; private System.Threading.Timer updateCheckTimer; private readonly System.Windows.Forms.Timer clipboardFloodRecoveryTimer; private AppSettings settings; private ClipStore store; private FileClipboardEventStore fileEventStore; private SecretStore secretStore; private HistoryForm historyForm; private PreferencesForm preferencesForm; private SecretsForm secretsForm; private bool showHotkeyRegistered; private bool toggleHotkeyRegistered; private bool saveCurrentClipboardHotkeyRegistered; private bool quickClipHotkeyRegistered; private bool toggleAlternateHotkeyRegistered; private readonly Dictionary quickCopyHotkeyEntryIds = new Dictionary(); private readonly Dictionary secretHotkeyEntryIds = new Dictionary(); private int quickCopyHotkeysRegistered; private int secretHotkeysRegistered; private string lastClipboardPrivacySignal = "None"; private string lastHandledCloseRequestId = string.Empty; private string lastAutoCopiedRemoteEntryId = string.Empty; private long lastAutoCopiedRemoteEntryStamp; private string databasePassword = string.Empty; private IntPtr previousForegroundWindow = IntPtr.Zero; private string lastReceivedHistoryTab = HistoryTabs.Text; private bool receivedHistoryTabPending; private int storageRetryInProgress; private sealed class AutomaticWebsiteTitleRequest { public string EntryId { get; set; } public string OriginalText { get; set; } public Uri Uri { get; set; } } public ClipmanApplicationContext() { RichImageFileDropData.Cleanup(); appDirectory = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar); settingsStore = new SettingsStore(appDirectory, Program.WriteRuntimeLog); settings = settingsStore.Load(); ResolveDatabaseLocation(); ResolveDatabasePassword(); SeedServerCacheFromConfiguredDatabase(); SharedUpdateStateStore.PublishCurrentBuild(SharedExecutableSettingsDirectory(), Application.ExecutablePath); sounds = new SoundService(appDirectory, settingsStore.SettingsDirectory); store = new ClipStore(EffectiveTextHistoryDatabasePath(), CurrentDatabasePassword, CurrentDeviceName()); store.Changed += StoreChanged; store.StorageStateChanged += StoreStorageStateChanged; ConfigureTextHistoryServerStorage(); ResetRemoteAutoCopyBaseline(); fileEventStore = new FileClipboardEventStore(settingsStore.DefaultFileHistoryDatabasePath(), CurrentDatabasePassword); fileEventStore.Changed += FileEventStoreChanged; secretStore = new SecretStore(DefaultSecretsDatabasePath(), CurrentDatabasePassword); invoker = new Control(); invoker.CreateControl(); clipboardFloodRecoveryTimer = new System.Windows.Forms.Timer { Interval = ClipboardFloodGuard.DefaultQuietMilliseconds }; clipboardFloodRecoveryTimer.Tick += ClipboardFloodRecoveryTimerTick; messageWindow = new MessageWindow(this); NativeMethods.AddClipboardFormatListener(messageWindow.Handle); notifyIcon = new NotifyIcon { Text = TrayText(), Icon = BuildIcon(settings.Active), Visible = true, ContextMenuStrip = BuildTrayMenu() }; notifyIcon.DoubleClick += (s, e) => ToggleHistoryWindow(); if (settings.CaptureClipboardOnStartup) { TryCaptureClipboardOnStartup(); } RegisterHotkeys(); ApplyStartupRegistration(false); PlayLaunchStateSound(); closeEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.CloseEventName); showEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.ShowEventName); recoverEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.RecoverEventName); pauseEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.PauseEventName); resumeEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.ResumeEventName); toggleEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.ToggleEventName); historyChangedEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.HistoryChangedEventName); historyAddedEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.HistoryAddedEventName); closeThread = new Thread(WaitForClose) { IsBackground = true, Name = "Clipman close event listener" }; showThread = new Thread(WaitForShow) { IsBackground = true, Name = "Clipman show event listener" }; recoverThread = new Thread(WaitForRecover) { IsBackground = true, Name = "Clipman storage recovery listener" }; pauseThread = new Thread(() => WaitForState(pauseEvent, false)) { IsBackground = true, Name = "Clipman pause event listener" }; resumeThread = new Thread(() => WaitForState(resumeEvent, true)) { IsBackground = true, Name = "Clipman resume event listener" }; toggleThread = new Thread(WaitForToggle) { IsBackground = true, Name = "Clipman toggle event listener" }; historyChangedThread = new Thread(WaitForHistoryChanged) { IsBackground = true, Name = "Clipman history-change event listener" }; historyAddedThread = new Thread(WaitForHistoryAdded) { IsBackground = true, Name = "Clipman history-add event listener" }; closeThread.Start(); showThread.Start(); recoverThread.Start(); pauseThread.Start(); resumeThread.Start(); toggleThread.Start(); historyChangedThread.Start(); historyAddedThread.Start(); if (InstanceStateStore.HasPendingCommandEntry()) { historyAddedEvent.Set(); } StartSharedUpdateWatchers(); ScheduleSharedUpdateCheck(5000); ScheduleUpdateChecks(); WarnAboutPasswordlessServerConfiguration(); } private void WarnAboutPasswordlessServerConfiguration() { if (!IsServerStorageEnabled() || string.IsNullOrWhiteSpace(settings.ServerUrl) || string.IsNullOrWhiteSpace(settings.ServerToken) || !string.IsNullOrEmpty(CurrentDatabasePassword()) || invoker == null || !invoker.IsHandleCreated) { return; } invoker.BeginInvoke(new Action(() => { MessageBox.Show( "Clipman Server now requires a history password. Server synchronization has not started, and your local cached history has not been changed.\r\n\r\nOpen Storage and Password preferences and enter a unique password shared only with your own Clipman clients.", "Clipman Server history password", MessageBoxButtons.OK, MessageBoxIcon.Information); ShowPreferencesFromTray(); })); } public void ShowHistory() { if (historyForm != null && !historyForm.IsDisposed && historyForm.RestoreQuickClip()) return; var historyWasVisible = historyForm != null && !historyForm.IsDisposed && historyForm.Visible; if (!historyWasVisible) { RememberPreviousForegroundWindow(); } var created = false; if (historyForm == null || historyForm.IsDisposed) { historyForm = new HistoryForm(store, settings, SaveSettings, RegisterHotkeys, CopyEntryToClipboard, CopyEntriesToClipboard, CopyPlainTextToClipboard, PasteClipboardIntoPreviousApplication, SaveCurrentClipboardToHistory, GetRecentClipboardEvents, DeleteRecentClipboardEvents, ClearRecentClipboardEvents, RemoveUnavailableRecentClipboardEvents, ToggleRecentClipboardEventPinned, MoveRecentClipboardEvents, ClearTextHistory, ShowPreferences, ShowSecrets, ToggleActive, ExitThread, () => sounds.Skip(settings.SoundsEnabled), BuildDiagnosticsText, HistorySteadyStatusText); created = true; } if (historyForm.WindowState == FormWindowState.Minimized) { historyForm.WindowState = FormWindowState.Normal; } if (settings.DynamicHistoryMode && receivedHistoryTabPending) { historyForm.SelectHistoryTabForOpen(lastReceivedHistoryTab); receivedHistoryTabPending = false; } var handle = historyForm.Handle; historyForm.Show(); NativeMethods.ShowWindow(handle, NativeMethods.SW_SHOWNORMAL); historyForm.BringToFront(); historyForm.Activate(); NativeMethods.SetForegroundWindow(handle); if (created) { historyForm.TopMost = true; historyForm.TopMost = false; } historyForm.FocusHistoryList(created); } public void ShowQuickClip() { if (historyForm != null && !historyForm.IsDisposed && historyForm.ToggleQuickClip()) return; var historyWasVisible = historyForm != null && !historyForm.IsDisposed && historyForm.Visible; ShowHistory(); historyForm.ShowQuickClip(() => { if (!historyWasVisible && !historyForm.IsDisposed) historyForm.Hide(); }); } public void ToggleHistoryWindow() { if (historyForm != null && !historyForm.IsDisposed && historyForm.Visible && historyForm.WindowState != FormWindowState.Minimized) { historyForm.HideWithQuickClip(); return; } ShowHistory(); } public void ToggleActive() { SetActive(!settings.Active, true); } private void SetActive(bool active, bool playSound) { settings.Active = active; SaveSettings(); UpdateTray(); if (preferencesForm != null && !preferencesForm.IsDisposed) { preferencesForm.SetActiveChecked(settings.Active); } if (playSound) { if (settings.Active) sounds.On(settings.SoundsEnabled); else sounds.Off(settings.SoundsEnabled); } } public void ShowPreferences() { ShowPreferences(true); } private void ShowPreferencesFromTray() { ShowPreferences(false); } private void ShowPreferences(bool showHistoryIfHidden) { if (preferencesForm != null && !preferencesForm.IsDisposed) { FocusPreferencesForm(); return; } if (historyForm == null || historyForm.IsDisposed || !historyForm.Visible) { if (showHistoryIfHidden) { ShowHistory(); } } preferencesForm = new PreferencesForm(settings, ApplyPreferences, CopySensitiveTextToClipboard, OpenSyncRulesEditor); preferencesForm.FormClosed += (s, e) => preferencesForm = null; if (historyForm != null && !historyForm.IsDisposed && historyForm.Visible) { preferencesForm.ShowDialog(historyForm); } else { preferencesForm.ShowDialog(); } preferencesForm = null; } /// /// Opens the modal sync rules editor from Preferences. Returns whether it applied a change, /// so Preferences can announce success the same way other preference changes do. /// private bool OpenSyncRulesEditor() { using (var form = new SyncRulesForm(store, CurrentDeviceName())) { if (preferencesForm != null && !preferencesForm.IsDisposed) { form.ShowDialog(preferencesForm); } else { form.ShowDialog(); } if (form.Applied) UpdateTray(); return form.Applied; } } private void FocusPreferencesForm() { if (preferencesForm == null || preferencesForm.IsDisposed) return; FocusPreferencesFormNow(); BeginDelayedPreferencesFocus(80); BeginDelayedPreferencesFocus(250); BeginDelayedPreferencesFocus(600); } private void BeginDelayedPreferencesFocus(int delayMilliseconds) { var timer = new System.Windows.Forms.Timer { Interval = delayMilliseconds }; timer.Tick += (s, e) => { timer.Stop(); timer.Dispose(); FocusPreferencesFormNow(); }; timer.Start(); } private void FocusPreferencesFormNow() { if (preferencesForm == null || preferencesForm.IsDisposed) return; if (preferencesForm.WindowState == FormWindowState.Minimized) { preferencesForm.WindowState = FormWindowState.Normal; } if (!preferencesForm.Visible) { return; } var handle = preferencesForm.Handle; if (handle != IntPtr.Zero) { NativeMethods.ShowWindow(handle, NativeMethods.SW_RESTORE); NativeMethods.SetForegroundWindow(handle); } preferencesForm.Activate(); preferencesForm.BringToFront(); preferencesForm.Focus(); preferencesForm.BeginInvoke(new Action(() => { if (preferencesForm == null || preferencesForm.IsDisposed) return; if (preferencesForm.WindowState == FormWindowState.Minimized) { preferencesForm.WindowState = FormWindowState.Normal; } var delayedHandle = preferencesForm.Handle; if (delayedHandle != IntPtr.Zero) { NativeMethods.ShowWindow(delayedHandle, NativeMethods.SW_RESTORE); NativeMethods.SetForegroundWindow(delayedHandle); } preferencesForm.Activate(); preferencesForm.BringToFront(); preferencesForm.Focus(); })); } private void ApplyPreferences(AppSettings updated) { var nextDatabasePassword = databasePassword; if (updated.PasswordClearRequested) { nextDatabasePassword = string.Empty; } else if (!string.IsNullOrEmpty(updated.PlainDatabasePassword)) { nextDatabasePassword = updated.PlainDatabasePassword; } if (!updated.DatabaseEncryptionEnabled) { nextDatabasePassword = string.Empty; updated.ProtectedDatabasePassword = string.Empty; updated.RememberDatabasePassword = false; } else if (updated.RememberDatabasePassword && string.IsNullOrWhiteSpace(updated.ProtectedDatabasePassword) && !string.IsNullOrEmpty(nextDatabasePassword)) { updated.ProtectedDatabasePassword = DatabasePasswordProtector.Protect(nextDatabasePassword); } else if (!updated.RememberDatabasePassword) { updated.ProtectedDatabasePassword = string.Empty; } var databaseChanged = !string.Equals(settings.DatabasePath, updated.DatabasePath, StringComparison.OrdinalIgnoreCase) || !string.Equals(settings.StorageMode, updated.StorageMode, StringComparison.OrdinalIgnoreCase) || !string.Equals(settings.ServerUrl, updated.ServerUrl, StringComparison.OrdinalIgnoreCase) || !string.Equals(settings.ServerToken, updated.ServerToken, StringComparison.Ordinal) || !string.Equals(settings.ServerCaCertPem, updated.ServerCaCertPem, StringComparison.Ordinal) || !string.Equals(settings.ServerCaHost, updated.ServerCaHost, StringComparison.OrdinalIgnoreCase); var wasServerStorageEnabled = IsServerStorageEnabled(); var serverCacheBeforeStorageChange = wasServerStorageEnabled ? ServerCacheDatabasePath() : string.Empty; var activeChanged = settings.Active != updated.Active; var sendToChanged = settings.SendToEnabled != updated.SendToEnabled; var encryptionChanged = settings.DatabaseEncryptionEnabled != updated.DatabaseEncryptionEnabled || settings.RememberDatabasePassword != updated.RememberDatabasePassword || !string.Equals(settings.ProtectedDatabasePassword, updated.ProtectedDatabasePassword, StringComparison.Ordinal) || !string.IsNullOrEmpty(updated.PlainDatabasePassword) || updated.PasswordClearRequested; var startupChanged = settings.RunAtStartup != updated.RunAtStartup; var updatePolicyChanged = !string.Equals(settings.UpdateCheckFrequency, updated.UpdateCheckFrequency, StringComparison.OrdinalIgnoreCase) || settings.InstallUpdatesSilently != updated.InstallUpdatesSilently; var autoRemoteCopyTurnedOn = !settings.AutoCopyLatestRemoteText && updated.AutoCopyLatestRemoteText; var saveListPositionTurnedOff = settings.SaveListPosition && !updated.SaveListPosition; var linksHistoryVisibilityChanged = settings.LinksHistoryEnabled != updated.LinksHistoryEnabled; var richTextHistoryVisibilityChanged = settings.RichTextHistoryEnabled != updated.RichTextHistoryEnabled; var oldSettingsDirectory = settingsStore.SettingsDirectory; settingsStore.Save(updated); databasePassword = nextDatabasePassword; settings.ShowHistoryHotkey = updated.ShowHistoryHotkey; settings.ToggleActiveHotkey = updated.ToggleActiveHotkey; settings.SaveCurrentClipboardHotkey = updated.SaveCurrentClipboardHotkey; settings.QuickClipHotkey = updated.QuickClipHotkey; settings.QuickCopyHotkeys = updated.QuickCopyHotkeys == null ? new List() : updated.QuickCopyHotkeys.Select(b => new QuickCopyBinding { EntryId = b.EntryId, Hotkey = b.Hotkey, Mode = QuickPasteModes.Normalize(b.Mode) }).ToList(); settings.AutoCopyLatestRemoteText = updated.AutoCopyLatestRemoteText; settings.PasteAfterEnter = updated.PasteAfterEnter; settings.DynamicHistoryMode = updated.DynamicHistoryMode; settings.RemoveDuplicates = updated.RemoveDuplicates; settings.SoundsEnabled = updated.SoundsEnabled; settings.ClipMergeEnabled = updated.ClipMergeEnabled; settings.ClipMergeWindowMilliseconds = ClipMergeDetector.NormalizeWindow(updated.ClipMergeWindowMilliseconds); settings.ClipMergeSeparatorMode = updated.ClipMergeSeparatorMode; settings.ClipMergeCustomSeparator = updated.ClipMergeCustomSeparator; settings.MultipleEntrySeparatorMode = updated.MultipleEntrySeparatorMode; settings.MultipleEntryCustomSeparator = updated.MultipleEntryCustomSeparator; settings.SaveListPosition = updated.SaveListPosition; settings.Active = updated.Active; settings.DatabasePath = updated.DatabasePath; settings.UseDefaultDatabasePath = updated.UseDefaultDatabasePath; settings.StorageMode = updated.StorageMode; settings.ServerUrl = updated.ServerUrl; settings.ServerToken = updated.ServerToken; settings.ServerCaCertPem = updated.ServerCaCertPem; settings.ServerCaHost = updated.ServerCaHost; settings.MaxHistoryEntries = updated.MaxHistoryEntries; settings.MaxHistoryDays = updated.MaxHistoryDays; settings.IgnoredProcesses = updated.IgnoredProcesses; settings.SortMode = updated.SortMode; settings.SortDescending = updated.SortDescending; settings.SendToEnabled = updated.SendToEnabled; settings.ShowHistoryAfterSendTo = updated.ShowHistoryAfterSendTo; settings.GroupFilter = updated.GroupFilter; settings.HistoryFilterType = updated.HistoryFilterType; settings.DeviceFilter = updated.DeviceFilter; settings.DeviceName = updated.DeviceName; settings.ConfirmDeletions = updated.ConfirmDeletions; settings.ConfirmSingleModifierHotkeys = updated.ConfirmSingleModifierHotkeys; settings.ConfirmWebsiteTitleRequests = updated.ConfirmWebsiteTitleRequests; settings.AutoNameCopiedWebsiteLinks = updated.AutoNameCopiedWebsiteLinks; settings.DuplicateMode = updated.DuplicateMode; settings.AutoGroupByApp = updated.AutoGroupByApp; settings.AutoRemoveUrlTracking = updated.AutoRemoveUrlTracking; settings.LinksHistoryEnabled = updated.LinksHistoryEnabled; settings.RichTextHistoryEnabled = updated.RichTextHistoryEnabled; settings.IncludeImagesInRichText = updated.RichTextHistoryEnabled && updated.IncludeImagesInRichText; settings.AutoAddImageFilesToRichText = settings.IncludeImagesInRichText && updated.AutoAddImageFilesToRichText; settings.LastSelectedHistoryTab = HistoryTabs.Normalize(updated.LastSelectedHistoryTab, settings.LinksHistoryEnabled, settings.RichTextHistoryEnabled); settings.HistoryTabOrder = HistoryTabs.NormalizeOrder(updated.HistoryTabOrder); settings.AutoRemoveUnavailableFileHistoryEvents = updated.AutoRemoveUnavailableFileHistoryEvents; settings.DiagnosticsFileHistoryLimit = updated.DiagnosticsFileHistoryLimit; settings.SensitiveDataMode = SensitiveDataExclusion.NormalizeMode(updated.SensitiveDataMode); settings.SensitiveDataPresetIds = updated.SensitiveDataPresetIds == null ? new List() : new List(updated.SensitiveDataPresetIds); settings.RunAtStartup = updated.RunAtStartup; settings.CaptureClipboardOnStartup = updated.CaptureClipboardOnStartup; settings.UpdateCheckFrequency = updated.UpdateCheckFrequency; settings.InstallUpdatesSilently = updated.InstallUpdatesSilently; settings.DatabaseEncryptionEnabled = updated.DatabaseEncryptionEnabled; settings.RememberDatabasePassword = updated.RememberDatabasePassword; settings.ProtectedDatabasePassword = updated.ProtectedDatabasePassword; settings.LastPreferencesTab = updated.LastPreferencesTab; store.SetMachineName(CurrentDeviceName()); if (saveListPositionTurnedOff) { settings.LastSelectedIndex = -1; } var settingsDirectoryChanged = !string.Equals(oldSettingsDirectory, settingsStore.SettingsDirectory, StringComparison.OrdinalIgnoreCase); if (settingsDirectoryChanged) { SharedUpdateStateStore.PublishCurrentBuild(SharedExecutableSettingsDirectory(), Application.ExecutablePath); RestartSharedUpdateWatchers(); sounds = new SoundService(appDirectory, settingsStore.SettingsDirectory); ReopenFileHistoryStore(); ReopenSecretStore(); } if (sendToChanged) { try { SendToInstaller.SetInstalled(settings.SendToEnabled); } catch (Exception ex) { MessageBox.Show("Could not update the Send To shortcut.\r\n\r\n" + ex.Message, "Clipman Send To", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } if (databaseChanged) { if (wasServerStorageEnabled && !IsServerStorageEnabled()) { MergeServerCacheIntoConfiguredDatabase(serverCacheBeforeStorageChange); } ResolveDatabasePassword(); ResolveDatabaseLocation(); SeedServerCacheFromConfiguredDatabase(); store.SetDatabasePath(EffectiveTextHistoryDatabasePath(), CurrentDatabasePassword); ConfigureTextHistoryServerStorage(); ResetRemoteAutoCopyBaseline(); ReopenSecretStore(); } else if (encryptionChanged) { if (!updated.PasswordClearRequested) { ResolveDatabasePassword(); } store.ChangeDatabasePassword(); ConfigureTextHistoryServerStorage(); if (!settingsDirectoryChanged) { fileEventStore.ChangeDatabasePassword(); } if (secretStore != null) { secretStore.ChangeDatabasePassword(); } ReopenSecretStore(); } RegisterHotkeys(); if (startupChanged) { ApplyStartupRegistration(true); } if (updatePolicyChanged) { ScheduleUpdateChecks(); } if (autoRemoteCopyTurnedOn) { ResetRemoteAutoCopyBaseline(); } UpdateTray(); if (activeChanged) { if (settings.Active) sounds.On(settings.SoundsEnabled); else sounds.Off(settings.SoundsEnabled); } if (historyForm != null && !historyForm.IsDisposed && (databaseChanged || linksHistoryVisibilityChanged || richTextHistoryVisibilityChanged)) { historyForm.RefreshTabsAndReload(); } } private void ShowSecrets() { var password = CurrentDatabasePassword(); if (string.IsNullOrEmpty(password)) { MessageBox.Show( "Secrets require a history password. Open Preferences, set a history password, and enable Remember history password on this computer if you want secrets available after restart.", "Clipman Secrets", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (!ConfirmSecretsPassword(password)) { return; } if (secretStore == null) { secretStore = new SecretStore(DefaultSecretsDatabasePath(), CurrentDatabasePassword); } if (secretsForm == null || secretsForm.IsDisposed) { secretsForm = new SecretsForm(secretStore, RegisterHotkeys, QuickPasteSecret); secretsForm.FormClosed += (s, e) => secretsForm = null; } if (historyForm != null && !historyForm.IsDisposed && historyForm.Visible) { secretsForm.ShowDialog(historyForm); } else { secretsForm.ShowDialog(); } secretsForm = null; } private bool ConfirmSecretsPassword(string currentPassword) { var entered = PasswordPromptForm.Ask( "Unlock Clipman Secrets", "Enter the current history password to open Secrets."); if (entered.Length == 0) return false; if (string.Equals(entered, currentPassword, StringComparison.Ordinal)) return true; MessageBox.Show( "The history password did not match. Secrets were not opened.", "Clipman Secrets", MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; } public void CopyEntryToClipboard(ClipEntry entry) { if (entry == null) return; SetClipboardIgnoringNotification(() => SetEntryClipboard(entry)); store.MarkUsed(entry.Id); sounds.Copy(settings.SoundsEnabled); } private void QueueAutomaticWebsiteTitle(ClipEntry entry, bool deliberate) { if (deliberate || !settings.AutoNameCopiedWebsiteLinks || entry == null || !string.IsNullOrWhiteSpace(entry.Name) || string.IsNullOrWhiteSpace(entry.Id)) return; Uri uri; string reason; if (!LinkPresentation.TryGetUri(entry, out uri) || !LinkTitleFetcher.CanOffer(uri, out reason)) return; var startWorker = false; lock (automaticWebsiteTitleLock) { if (automaticWebsiteTitleEntryIds.Contains(entry.Id) || automaticWebsiteTitleQueue.Count >= 8) return; automaticWebsiteTitleEntryIds.Add(entry.Id); automaticWebsiteTitleQueue.Enqueue(new AutomaticWebsiteTitleRequest { EntryId = entry.Id, OriginalText = entry.Text ?? string.Empty, Uri = uri }); if (!automaticWebsiteTitleWorkerRunning) { automaticWebsiteTitleWorkerRunning = true; startWorker = true; } } if (startWorker) ThreadPool.QueueUserWorkItem(_ => ProcessAutomaticWebsiteTitles()); } private void ProcessAutomaticWebsiteTitles() { while (true) { AutomaticWebsiteTitleRequest request; lock (automaticWebsiteTitleLock) { if (automaticWebsiteTitleQueue.Count == 0) { automaticWebsiteTitleWorkerRunning = false; return; } request = automaticWebsiteTitleQueue.Dequeue(); } if (!settings.AutoNameCopiedWebsiteLinks) { lock (automaticWebsiteTitleLock) automaticWebsiteTitleEntryIds.Remove(request.EntryId); continue; } var result = LinkTitleFetcher.Fetch(request.Uri); BeginInvokeIfReady(() => CompleteAutomaticWebsiteTitle(request, result)); } } private void CompleteAutomaticWebsiteTitle(AutomaticWebsiteTitleRequest request, LinkTitleFetchResult result) { lock (automaticWebsiteTitleLock) automaticWebsiteTitleEntryIds.Remove(request.EntryId); if (!settings.AutoNameCopiedWebsiteLinks) return; if (!result.Success) { websiteTitleDiagnostics.RecordFailure(result.Error); return; } if (!store.TrySetNameIfUnchanged(request.EntryId, request.OriginalText, result.Title)) { websiteTitleDiagnostics.RecordStaleResult(); } } public void CopyEntriesToClipboard(List entries) { if (entries == null || entries.Count == 0) return; var data = new DataObject(); data.SetText( string.Join( MultipleEntrySeparator.Resolve(settings.MultipleEntrySeparatorMode, settings.MultipleEntryCustomSeparator), entries.Select(ResolvedEntryText)), TextDataFormat.UnicodeText); data.SetData(ClipmanClipboardData.EntriesFormat, ClipmanClipboardData.SerializeEntries(entries)); SetClipboardIgnoringNotification(() => Clipboard.SetDataObject(data, true)); store.MarkUsedMany(entries.Select(entry => entry.Id)); sounds.Copy(settings.SoundsEnabled); } private bool CopyPlainTextToClipboard(string text, List entries) { try { SetClipboardIgnoringNotification(() => Clipboard.SetText(text ?? string.Empty, TextDataFormat.UnicodeText)); store.MarkUsedMany((entries ?? new List()).Where(entry => entry != null).Select(entry => entry.Id)); sounds.Copy(settings.SoundsEnabled); return true; } catch (Exception ex) { sounds.Skip(settings.SoundsEnabled); Program.WriteRuntimeLog("Clipman could not copy plain text to the clipboard.", ex); return false; } } private void CopySensitiveTextToClipboard(string text) { SetClipboardIgnoringNotification(() => Clipboard.SetText(text ?? string.Empty, TextDataFormat.UnicodeText)); } internal void HandleHotkey(int id) { if (id == ShowHotkeyId) { ToggleHistoryWindow(); } else if (id == ToggleHotkeyId || id == ToggleHotkeyAlternateId) { ToggleActive(); } else if (id == SaveCurrentClipboardHotkeyId) { SaveCurrentClipboardToHistory(); } else if (id == QuickClipHotkeyId) { ShowQuickClip(); } else if (quickCopyHotkeyEntryIds.ContainsKey(id)) { QuickPasteEntry(quickCopyHotkeyEntryIds[id]); } else if (secretHotkeyEntryIds.ContainsKey(id)) { QuickPasteSecret(secretStore.GetEntryById(secretHotkeyEntryIds[id])); } } private void SaveCurrentClipboardToHistory() { HandleClipboardUpdate(true); } private void TryCaptureClipboardOnStartup() { try { HandleClipboardUpdate(startupCapture: true); } catch (ExternalException ex) { if ((uint)ex.ErrorCode != 0x800401D0U) throw; Program.WriteRuntimeLog("The clipboard was busy during startup; Clipman will retry the initial capture.", ex); clipboardFloodRecoveryTimer.Stop(); clipboardFloodRecoveryTimer.Start(); } } internal void HandleClipboardUpdate(bool deliberate = false, uint clipboardSequence = 0, bool recovery = false, bool startupCapture = false, long? observedMilliseconds = null) { var captureMilliseconds = observedMilliseconds ?? ClipboardFloodGuard.MonotonicMilliseconds(); if (!deliberate && clipboardSequence == 0) { clipboardSequence = NativeMethods.GetClipboardSequenceNumber(); } if (!deliberate && !clipboardNotifications.ShouldProcess(clipboardSequence, recovery)) { return; } var sourceProcessName = ClipboardOwnerProcessName(); if (string.IsNullOrWhiteSpace(sourceProcessName)) { sourceProcessName = ForegroundProcessName(); } if (!deliberate && IsClipmanProcess(sourceProcessName)) { return; } if (!deliberate) { var floodDecision = clipboardFloodGuards.Observe( NormalizeProcessName(sourceProcessName), ClipboardFloodGuard.MonotonicMilliseconds()); if (floodDecision != ClipboardFloodDecision.Allow) { clipboardFloodRecoveryTimer.Stop(); clipboardFloodRecoveryTimer.Start(); return; } clipboardFloodRecoveryTimer.Stop(); } if (!deliberate && !settings.Active) { clipMergeDetector.Reset(); sounds.Skip(settings.SoundsEnabled); return; } if (!deliberate && IsIgnoredProcess(sourceProcessName)) { clipMergeDetector.Reset(); sounds.Skip(settings.SoundsEnabled); return; } var privacySignal = ClipboardPrivacySignals.Detect(); if (privacySignal != null) { clipMergeDetector.Reset(); lastClipboardPrivacySignal = privacySignal.Reason; sounds.Exclude(settings.SoundsEnabled); return; } lastClipboardPrivacySignal = "None"; if (settings.RichTextHistoryEnabled && settings.IncludeImagesInRichText && !Clipboard.ContainsText(TextDataFormat.UnicodeText) && RichImageData.ClipboardHasStandaloneImage()) { clipMergeDetector.Reset(); var imageCapture = RichImageData.CaptureFromClipboard(FriendlyProcessName(sourceProcessName)); if (imageCapture == null) { sounds.Skip(settings.SoundsEnabled); return; } if (store.EmbeddedImageByteCount() + imageCapture.ImageBytes > RichImageData.MaximumDatabaseImageBytes) { Program.WriteRuntimeLog("Clipman did not store a clipboard image because the 8 MiB history image budget is full.", null); sounds.Skip(settings.SoundsEnabled); return; } var imageGroup = settings.AutoGroupByApp ? FriendlyProcessName(sourceProcessName) : string.Empty; store.AddText(imageCapture.Text, DuplicateModeForCapture(startupCapture, settings.DuplicateMode), settings.MaxHistoryEntries, settings.MaxHistoryDays, imageGroup, imageCapture.RichText); if (IsStorageUnavailable()) { sounds.Skip(settings.SoundsEnabled); UpdateTray(); return; } RememberReceivedHistoryTab(HistoryTabs.RichText); sounds.Copy(settings.SoundsEnabled); return; } var hasUnicodeText = Clipboard.ContainsText(TextDataFormat.UnicodeText); string urlOnlyText = null; if (!hasUnicodeText) { try { urlOnlyText = ClipboardUrlData.TryRead(Clipboard.GetDataObject()); } catch { } } ClipboardEventSummary fileSummary = null; try { fileSummary = ReadClipboardEventSummary(sourceProcessName); } catch { } if (fileSummary != null && urlOnlyText == null) { if (startupCapture && fileEventStore.Contains(fileSummary)) { return; } HandleCapturedFileEvent(fileSummary, sourceProcessName, deliberate, clipboardSequence, captureMilliseconds); if (settings.RichTextHistoryEnabled && settings.IncludeImagesInRichText && settings.AutoAddImageFilesToRichText) { string imageFilePath; string ignoredFailureMessage; if (RichImageData.TryGetSingleClipboardImageFile(out imageFilePath, out ignoredFailureMessage)) { AddCopiedImageFileToRichTextAsync(imageFilePath, sourceProcessName); } } return; } if (!hasUnicodeText && urlOnlyText == null) { clipMergeDetector.Reset(); sounds.Skip(settings.SoundsEnabled); return; } var text = urlOnlyText; try { if (hasUnicodeText) text = Clipboard.GetText(TextDataFormat.UnicodeText); if (string.IsNullOrEmpty(text)) { text = ClipboardUrlData.TryRead(Clipboard.GetDataObject()); } } catch { return; } if (string.IsNullOrEmpty(text)) { clipMergeDetector.Reset(); if (deliberate) { sounds.Skip(settings.SoundsEnabled); } return; } var richText = settings.RichTextHistoryEnabled ? RichTextData.CaptureFromClipboard() : null; var rawText = text; if (!deliberate && store.HasRecentlyTouchedRemoteText(text, CurrentDeviceName(), 90000)) { return; } var sensitiveMatch = deliberate ? null : SensitiveDataExclusion.FindMatch(text, settings); if (sensitiveMatch != null) { clipMergeDetector.Reset(); sounds.Exclude(settings.SoundsEnabled); return; } if (!deliberate && settings.AutoRemoveUrlTracking) { var cleaned = UrlTrackingCleaner.CleanText(text); if (!string.Equals(cleaned, text, StringComparison.Ordinal)) { text = cleaned; richText = null; try { SetClipboardIgnoringNotification(() => Clipboard.SetText(text, TextDataFormat.UnicodeText)); } catch { } } } var group = settings.AutoGroupByApp ? FriendlyProcessName(sourceProcessName) : string.Empty; var textObservation = new ClipMergeObservation { Kind = ClipMergeKind.Text, Signature = rawText, SourceApplication = NormalizeProcessName(sourceProcessName), ChangeIdentifier = clipboardSequence, Payload = text }; var mergeDecision = clipMergeDetector.Observe( textObservation, captureMilliseconds, settings.ClipMergeEnabled, settings.ClipMergeWindowMilliseconds, deliberate); if (mergeDecision.SuppressDuplicate) { return; } ClipEntry storedEntry; if (mergeDecision.ShouldMerge) { var baseText = Convert.ToString(mergeDecision.Base.Payload) ?? string.Empty; var mergedText = baseText + ClipMergeDetector.ResolveSeparator(settings.ClipMergeSeparatorMode, settings.ClipMergeCustomSeparator) + text; storedEntry = store.MergeCapturedText( mergeDecision.Base.HistoryId, mergeDecision.FirstTap.HistoryId, mergedText, settings.MaxHistoryEntries, settings.MaxHistoryDays, group); if (storedEntry != null) { try { SetClipboardIgnoringNotification(() => Clipboard.SetText(mergedText, TextDataFormat.UnicodeText)); } catch { } textObservation.Signature = mergedText; textObservation.Payload = mergedText; clipMergeDetector.CompleteMerge(textObservation, storedEntry.Id); RememberReceivedHistoryTab(LinkClassifier.IsLinkOnlyText(mergedText) ? HistoryTabs.Links : HistoryTabs.Text); sounds.Merge(settings.SoundsEnabled); return; } } storedEntry = store.AddText(text, DuplicateModeForCapture(startupCapture, settings.DuplicateMode), settings.MaxHistoryEntries, settings.MaxHistoryDays, group, richText); clipMergeDetector.SetCurrentHistoryId(storedEntry == null ? string.Empty : storedEntry.Id); if (IsStorageUnavailable()) { sounds.Skip(settings.SoundsEnabled); UpdateTray(); return; } RememberReceivedHistoryTab(richText != null ? HistoryTabs.RichText : LinkClassifier.IsLinkOnlyText(text) ? HistoryTabs.Links : HistoryTabs.Text); sounds.Copy(settings.SoundsEnabled); QueueAutomaticWebsiteTitle(storedEntry, deliberate); } internal static string DuplicateModeForCapture(bool startupCapture, string configuredMode) { return startupCapture ? "Ignore" : configuredMode; } private void AddCopiedImageFileToRichTextAsync(string path, string sourceProcessName) { ThreadPool.QueueUserWorkItem(_ => { var capture = RichImageData.CaptureFromFile(path); if (capture == null) { Program.WriteRuntimeLog("Clipman could not add the copied image file to Rich Text history because it was unavailable or invalid.", null); return; } BeginInvokeIfReady(() => { if (!settings.RichTextHistoryEnabled || !settings.IncludeImagesInRichText || !settings.AutoAddImageFilesToRichText) return; if (store.EmbeddedImageByteCount() + capture.ImageBytes > RichImageData.MaximumDatabaseImageBytes) { Program.WriteRuntimeLog("Clipman did not add a copied image file to Rich Text history because the 8 MiB history image budget is full.", null); return; } var group = settings.AutoGroupByApp ? FriendlyProcessName(sourceProcessName) : string.Empty; store.AddText(capture.Text, settings.DuplicateMode, settings.MaxHistoryEntries, settings.MaxHistoryDays, group, capture.RichText); if (IsStorageUnavailable()) UpdateTray(); }); }); } private void ClipboardFloodRecoveryTimerTick(object sender, EventArgs e) { clipboardFloodRecoveryTimer.Stop(); try { HandleClipboardUpdate(false, NativeMethods.GetClipboardSequenceNumber(), true); } catch (ExternalException ex) { if ((uint)ex.ErrorCode != 0x800401D0U) throw; clipboardFloodRecoveryTimer.Start(); } } private void RememberReceivedHistoryTab(string tabId) { if (!settings.DynamicHistoryMode) return; lastReceivedHistoryTab = tabId; receivedHistoryTabPending = true; } private void HandleCapturedFileEvent(ClipboardEventSummary summary, string sourceProcessName, bool deliberate, uint clipboardSequence, long observedMilliseconds) { if (summary == null || summary.Files == null || summary.Files.Count == 0) return; if (string.IsNullOrWhiteSpace(summary.Operation)) summary.Operation = "Copy"; var observation = new ClipMergeObservation { Kind = ClipMergeKind.Files, Signature = FileMergeSignature(summary.Files, summary.Operation), SourceApplication = NormalizeProcessName(sourceProcessName), Operation = summary.Operation, ChangeIdentifier = clipboardSequence, Payload = CloneClipboardEvent(summary) }; var decision = clipMergeDetector.Observe( observation, observedMilliseconds, settings.ClipMergeEnabled, settings.ClipMergeWindowMilliseconds, deliberate); if (decision.SuppressDuplicate) { return; } if (decision.ShouldMerge) { var baseSummary = decision.Base.Payload as ClipboardEventSummary; if (baseSummary != null) { if (string.Equals(summary.Operation, "Move", StringComparison.OrdinalIgnoreCase) && (!ClipMergeFilePolicy.AreSourcesAvailable(baseSummary.Files) || !ClipMergeFilePolicy.AreSourcesAvailable(summary.Files))) { clipMergeDetector.RetainFirstTap(decision.FirstTap); return; } var mergedFiles = baseSummary.Files .Concat(summary.Files) .Where(path => !string.IsNullOrWhiteSpace(path)) .Select(path => path.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); var merged = new ClipboardEventSummary { CapturedAt = DateTime.Now, Source = summary.Source, Operation = summary.Operation, SourceMachine = CurrentDeviceName(), ContainsText = true, FileCount = mergedFiles.Count, Files = mergedFiles, Formats = baseSummary.Formats.Concat(summary.Formats).Distinct(StringComparer.OrdinalIgnoreCase).ToList() }; var saved = fileEventStore.MergeCapturedEvents(decision.Base.HistoryId, decision.FirstTap.HistoryId, merged); if (saved != null && string.IsNullOrWhiteSpace(fileEventStore.LastStorageError)) { WriteMergedFilesToClipboard(mergedFiles, merged.Operation); observation.Signature = FileMergeSignature(mergedFiles, merged.Operation); observation.Payload = CloneClipboardEvent(merged); clipMergeDetector.CompleteMerge(observation, saved.Id); RememberReceivedHistoryTab(HistoryTabs.Files); sounds.Merge(settings.SoundsEnabled); return; } } } var stored = fileEventStore.Add(summary); clipMergeDetector.SetCurrentHistoryId(stored == null ? string.Empty : stored.Id); if (settings.AutoRemoveUnavailableFileHistoryEvents) { fileEventStore.RemoveUnavailableEvents(); } if (string.IsNullOrWhiteSpace(fileEventStore.LastStorageError)) { RememberReceivedHistoryTab(HistoryTabs.Files); sounds.Copy(settings.SoundsEnabled); } else { sounds.Skip(settings.SoundsEnabled); } } private void WriteMergedFilesToClipboard(IEnumerable paths, string operation) { var files = (paths ?? Enumerable.Empty()).Where(path => !string.IsNullOrWhiteSpace(path)).ToList(); if (files.Count == 0) return; var fileList = new StringCollection(); fileList.AddRange(files.ToArray()); var data = new DataObject(); data.SetFileDropList(fileList); data.SetText(string.Join(Environment.NewLine, files), TextDataFormat.UnicodeText); var effect = string.Equals(operation, "Move", StringComparison.OrdinalIgnoreCase) ? 2 : 1; data.SetData("Preferred DropEffect", false, new MemoryStream(BitConverter.GetBytes(effect))); try { SetClipboardIgnoringNotification(() => Clipboard.SetDataObject(data, true)); } catch { } } private static string FileMergeSignature(IEnumerable paths, string operation) { return (operation ?? string.Empty).Trim().ToUpperInvariant() + "\n" + string.Join("\n", (paths ?? Enumerable.Empty()) .Where(path => !string.IsNullOrWhiteSpace(path)) .Select(path => path.Trim().ToUpperInvariant()) .OrderBy(path => path, StringComparer.Ordinal)); } private List GetRecentClipboardEvents() { return fileEventStore.GetEvents(settings.FileHistorySortMode, settings.FileHistorySortDescending); } private int DeleteRecentClipboardEvents(List ids) { return fileEventStore.DeleteMany(ids); } private int ClearRecentClipboardEvents() { return fileEventStore.Clear(); } private int RemoveUnavailableRecentClipboardEvents() { return fileEventStore.RemoveUnavailableEvents(); } private bool ToggleRecentClipboardEventPinned(string id) { return fileEventStore.TogglePinned(id); } private void MoveRecentClipboardEvents(List ids, int direction) { fileEventStore.MoveEvents(ids, direction); } private bool ClearTextHistory() { var password = CurrentDatabasePassword(); if (!string.IsNullOrEmpty(password)) { var entered = PasswordPromptForm.Ask( "Clear Clipman history", "Enter the history password to clear all saved text clipboard entries."); if (!string.Equals(entered, password, StringComparison.Ordinal)) { MessageBox.Show("The history password did not match. Clipboard history was not cleared.", "Clipman", MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; } } var message = "Clear all saved text clipboard history?" + Environment.NewLine + Environment.NewLine + "This will remove every text entry from the current Clipman database. A timestamped backup will be created first."; if (MessageBox.Show(message, "Clear Clipman history", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes) { return false; } BackupDatabase(settings.DatabasePath, "before-clear-history"); store.ReplaceAll(new List()); return true; } private static void BackupDatabase(string databasePath, string reason) { if (string.IsNullOrWhiteSpace(databasePath) || !File.Exists(databasePath)) return; var directory = Path.GetDirectoryName(databasePath); var name = Path.GetFileNameWithoutExtension(databasePath); var extension = Path.GetExtension(databasePath); if (string.IsNullOrEmpty(directory)) directory = AppDomain.CurrentDomain.BaseDirectory; if (string.IsNullOrEmpty(name)) name = "clipman-history"; if (string.IsNullOrEmpty(extension)) extension = ".clipdb"; var safeReason = string.IsNullOrWhiteSpace(reason) ? "backup" : reason.Trim(); var stamp = DateTime.Now.ToString("yyyyMMdd-HHmmss"); var backupPath = Path.Combine(directory, name + "." + safeReason + "-" + stamp + extension); File.Copy(databasePath, backupPath, false); } private static ClipboardEventSummary CloneClipboardEvent(ClipboardEventSummary source) { if (source == null) return null; return new ClipboardEventSummary { Id = source.Id, CapturedAt = source.CapturedAt, Source = source.Source ?? string.Empty, Operation = source.Operation ?? string.Empty, SourceMachine = source.SourceMachine ?? string.Empty, ContainsText = source.ContainsText, FileCount = source.FileCount, Files = source.Files == null ? new List() : source.Files.ToList(), Formats = source.Formats == null ? new List() : source.Formats.ToList(), Pinned = source.Pinned, ManualOrder = source.ManualOrder }; } private ClipboardEventSummary ReadClipboardEventSummary(string sourceProcessName) { var data = Clipboard.GetDataObject(); if (data == null) return null; var formats = data.GetFormats(false) ?? new string[0]; var hasText = data.GetDataPresent(DataFormats.UnicodeText, false) || data.GetDataPresent(DataFormats.Text, false); var hasFiles = data.GetDataPresent(DataFormats.FileDrop, false); if (!hasFiles && hasText) { return null; } var summary = new ClipboardEventSummary { CapturedAt = DateTime.Now, Source = FriendlyProcessName(sourceProcessName), SourceMachine = CurrentDeviceName(), ContainsText = hasText, Formats = formats.ToList(), Operation = ClipboardDropEffect(data) }; if (hasFiles) { var files = data.GetData(DataFormats.FileDrop, false) as string[]; if (files != null) { summary.Files = files.ToList(); summary.FileCount = files.Length; } } return summary; } private static string ClipboardDropEffect(IDataObject data) { try { if (!data.GetDataPresent("Preferred DropEffect", false)) return string.Empty; var stream = data.GetData("Preferred DropEffect", false) as MemoryStream; if (stream == null || stream.Length < 4) return string.Empty; var bytes = stream.ToArray(); var value = BitConverter.ToInt32(bytes, 0); return DescribeDropEffect(value); } catch { return string.Empty; } } internal static string DescribeDropEffect(int value) { var parts = new List(); if ((value & 1) == 1) parts.Add("Copy"); if ((value & 2) == 2) parts.Add("Move"); if ((value & 4) == 4) parts.Add("Link"); var knownBits = value & 7; if (parts.Count > 0 && knownBits == value) { return string.Join(" or ", parts); } switch (value) { case 0: return string.Empty; default: return "Unknown operation " + value; } } private ContextMenuStrip BuildTrayMenu() { var menu = new ContextMenuStrip(); var storageMessage = StorageUnavailableMessage(); if (!string.IsNullOrWhiteSpace(storageMessage)) { var unavailable = new ToolStripMenuItem("Storage unavailable") { Enabled = false, ToolTipText = storageMessage }; menu.Items.Add(unavailable); menu.Items.Add("&Retry storage", null, (s, e) => RetryStorage()); menu.Items.Add("-"); } menu.Items.Add("&Show or hide history\t" + settings.ShowHistoryHotkey, null, (s, e) => ToggleHistoryWindow()); menu.Items.Add((settings.Active ? "Turn &off" : "Turn &on") + "\t" + settings.ToggleActiveHotkey, null, (s, e) => ToggleActive()); var saveClipboardText = "Save current &clipboard to history"; if (!string.IsNullOrWhiteSpace(settings.SaveCurrentClipboardHotkey)) { saveClipboardText += "\t" + settings.SaveCurrentClipboardHotkey; } menu.Items.Add(saveClipboardText, null, (s, e) => SaveCurrentClipboardToHistory()); var quickClipText = "New &Quick Clip"; if (!string.IsNullOrWhiteSpace(settings.QuickClipHotkey)) { quickClipText += "\t" + settings.QuickClipHotkey; } menu.Items.Add(quickClipText, null, (s, e) => ShowQuickClip()); menu.Items.Add("&Secrets...\tCtrl+Shift+E", null, (s, e) => ShowSecrets()); menu.Items.Add("&Preferences...", null, (s, e) => ShowPreferencesFromTray()); menu.Items.Add("Open &settings folder\tCtrl+Shift+O", null, (s, e) => OpenSettingsFolder()); menu.Items.Add("-"); menu.Items.Add("E&xit", null, (s, e) => ExitThread()); return menu; } private void OpenSettingsFolder() { try { Directory.CreateDirectory(settingsStore.SettingsDirectory); Process.Start(new ProcessStartInfo { FileName = settingsStore.SettingsDirectory, UseShellExecute = true }); } catch (Exception ex) { MessageBox.Show("Clipman could not open the settings folder.\r\n\r\n" + ex.Message, "Clipman", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } private void RetryStorage() { if (Interlocked.CompareExchange(ref storageRetryInProgress, 1, 0) != 0) return; ThreadPool.QueueUserWorkItem(delegate { var result = StorageRetryOperation.Execute(store.Reload, fileEventStore.Reload); if (result.TextHistoryError != null) { store.RecordServerRetryFailure(result.TextHistoryError); } if (!result.Succeeded) Program.WriteRuntimeLog("The explicit storage retry failed.", result.Error); Interlocked.Exchange(ref storageRetryInProgress, 0); try { if (invoker != null && invoker.IsHandleCreated) { invoker.BeginInvoke(new Action(delegate { UpdateTray(); if (!result.Succeeded) { MessageBox.Show( "Clipman could not reconnect to storage. It will keep using the local cache and retry automatically.\r\n\r\n" + result.Error.Message, "Clipman storage unavailable", MessageBoxButtons.OK, MessageBoxIcon.Warning); } })); } } catch (InvalidOperationException) { } }); } private void RegisterHotkeys() { NativeMethods.UnregisterHotKey(messageWindow.Handle, ShowHotkeyId); NativeMethods.UnregisterHotKey(messageWindow.Handle, ToggleHotkeyId); NativeMethods.UnregisterHotKey(messageWindow.Handle, SaveCurrentClipboardHotkeyId); NativeMethods.UnregisterHotKey(messageWindow.Handle, ToggleHotkeyAlternateId); NativeMethods.UnregisterHotKey(messageWindow.Handle, QuickClipHotkeyId); foreach (var hotkeyId in quickCopyHotkeyEntryIds.Keys.ToList()) { NativeMethods.UnregisterHotKey(messageWindow.Handle, hotkeyId); } foreach (var hotkeyId in secretHotkeyEntryIds.Keys.ToList()) { NativeMethods.UnregisterHotKey(messageWindow.Handle, hotkeyId); } quickCopyHotkeyEntryIds.Clear(); secretHotkeyEntryIds.Clear(); showHotkeyRegistered = false; toggleHotkeyRegistered = false; saveCurrentClipboardHotkeyRegistered = false; quickClipHotkeyRegistered = false; toggleAlternateHotkeyRegistered = false; quickCopyHotkeysRegistered = 0; secretHotkeysRegistered = 0; PruneInvalidQuickPasteBindings(); HotkeyDefinition show; if (HotkeyDefinition.TryParse(settings.ShowHistoryHotkey, out show)) { showHotkeyRegistered = NativeMethods.RegisterHotKey(messageWindow.Handle, ShowHotkeyId, GlobalHotkeyModifiers(show.Modifiers), show.Key); } HotkeyDefinition toggle; if (HotkeyDefinition.TryParse(settings.ToggleActiveHotkey, out toggle)) { toggleHotkeyRegistered = NativeMethods.RegisterHotKey(messageWindow.Handle, ToggleHotkeyId, GlobalHotkeyModifiers(toggle.Modifiers), toggle.Key); if (settings.ToggleActiveHotkey.Trim().Equals("Ctrl+Alt+`", StringComparison.OrdinalIgnoreCase)) { toggleAlternateHotkeyRegistered = NativeMethods.RegisterHotKey(messageWindow.Handle, ToggleHotkeyAlternateId, GlobalHotkeyModifiers(toggle.Modifiers), Keys.Oem8); } } HotkeyDefinition saveCurrentClipboard; if (HotkeyDefinition.TryParse(settings.SaveCurrentClipboardHotkey, out saveCurrentClipboard)) { saveCurrentClipboardHotkeyRegistered = NativeMethods.RegisterHotKey(messageWindow.Handle, SaveCurrentClipboardHotkeyId, GlobalHotkeyModifiers(saveCurrentClipboard.Modifiers), saveCurrentClipboard.Key); } HotkeyDefinition quickClip; if (HotkeyDefinition.TryParse(settings.QuickClipHotkey, out quickClip)) { quickClipHotkeyRegistered = NativeMethods.RegisterHotKey(messageWindow.Handle, QuickClipHotkeyId, GlobalHotkeyModifiers(quickClip.Modifiers), quickClip.Key); } var quickCopyId = QuickCopyHotkeyBaseId; var usedHotkeys = new HashSet(StringComparer.OrdinalIgnoreCase); usedHotkeys.Add((settings.ShowHistoryHotkey ?? string.Empty).Trim()); usedHotkeys.Add((settings.ToggleActiveHotkey ?? string.Empty).Trim()); if (!string.IsNullOrWhiteSpace(settings.SaveCurrentClipboardHotkey)) { usedHotkeys.Add(settings.SaveCurrentClipboardHotkey.Trim()); } if (!string.IsNullOrWhiteSpace(settings.QuickClipHotkey)) { usedHotkeys.Add(settings.QuickClipHotkey.Trim()); } foreach (var binding in (settings.QuickCopyHotkeys ?? new List()) .Where(b => b != null && !string.IsNullOrWhiteSpace(b.EntryId) && !string.IsNullOrWhiteSpace(b.Hotkey)) .OrderBy(b => b.EntryId, StringComparer.OrdinalIgnoreCase)) { var normalizedHotkey = binding.Hotkey.Trim(); if (!usedHotkeys.Add(normalizedHotkey)) continue; HotkeyDefinition quickCopy; if (!HotkeyDefinition.TryParse(normalizedHotkey, out quickCopy)) continue; while (quickCopyHotkeyEntryIds.ContainsKey(quickCopyId)) { quickCopyId++; } if (NativeMethods.RegisterHotKey(messageWindow.Handle, quickCopyId, GlobalHotkeyModifiers(quickCopy.Modifiers), quickCopy.Key)) { quickCopyHotkeyEntryIds[quickCopyId] = binding.EntryId.Trim(); quickCopyHotkeysRegistered++; } quickCopyId++; } var secretId = SecretHotkeyBaseId; foreach (var secret in GetSecretEntriesSafe() .Where(s => s != null && !string.IsNullOrWhiteSpace(s.Id) && !string.IsNullOrWhiteSpace(s.Hotkey)) .OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase)) { var normalizedHotkey = secret.Hotkey.Trim(); if (!usedHotkeys.Add(normalizedHotkey)) continue; HotkeyDefinition secretHotkey; if (!HotkeyDefinition.TryParse(normalizedHotkey, out secretHotkey)) continue; while (secretHotkeyEntryIds.ContainsKey(secretId)) { secretId++; } if (NativeMethods.RegisterHotKey(messageWindow.Handle, secretId, GlobalHotkeyModifiers(secretHotkey.Modifiers), secretHotkey.Key)) { secretHotkeyEntryIds[secretId] = secret.Id.Trim(); secretHotkeysRegistered++; } secretId++; } } private static NativeMethods.Modifiers GlobalHotkeyModifiers(NativeMethods.Modifiers modifiers) { return modifiers | NativeMethods.Modifiers.NoRepeat; } private void PruneInvalidQuickPasteBindings() { if (settings.QuickCopyHotkeys == null) { settings.QuickCopyHotkeys = new List(); return; } var cleaned = new List(); var usedEntryIds = new HashSet(StringComparer.OrdinalIgnoreCase); var usedHotkeys = new HashSet(StringComparer.OrdinalIgnoreCase); var changed = false; foreach (var binding in settings.QuickCopyHotkeys) { var entryId = binding == null ? string.Empty : (binding.EntryId ?? string.Empty).Trim(); var hotkey = binding == null ? string.Empty : (binding.Hotkey ?? string.Empty).Trim(); HotkeyDefinition parsed; var entry = string.IsNullOrWhiteSpace(entryId) ? null : store.GetEntryById(entryId); if (string.IsNullOrWhiteSpace(entryId) || string.IsNullOrWhiteSpace(hotkey) || !HotkeyDefinition.TryParse(hotkey, out parsed) || entry == null || string.IsNullOrEmpty(entry.Text) || !usedEntryIds.Add(entryId) || !usedHotkeys.Add(hotkey)) { changed = true; continue; } if (!string.Equals(binding.EntryId, entryId, StringComparison.Ordinal) || !string.Equals(binding.Hotkey, hotkey, StringComparison.Ordinal)) { changed = true; } cleaned.Add(new QuickCopyBinding { EntryId = entryId, Hotkey = hotkey, Mode = QuickPasteModes.Normalize(binding.Mode) }); } if (changed || cleaned.Count != settings.QuickCopyHotkeys.Count) { settings.QuickCopyHotkeys = cleaned; settingsStore.Save(settings); } } private void SaveSettings() { settingsStore.Save(settings); } private void PlayLaunchStateSound() { if (settings.Active) { sounds.On(settings.SoundsEnabled); } else { sounds.Off(settings.SoundsEnabled); } } private string ForegroundProcessName() { try { uint processId; var hwnd = NativeMethods.GetForegroundWindow(); if (hwnd == IntPtr.Zero) return string.Empty; NativeMethods.GetWindowThreadProcessId(hwnd, out processId); if (processId == 0) return string.Empty; if ((int)processId == Process.GetCurrentProcess().Id) return "clipman"; var process = Process.GetProcessById((int)processId); return NormalizeProcessName(process.ProcessName); } catch { return string.Empty; } } private string ClipboardOwnerProcessName() { try { uint processId; var hwnd = NativeMethods.GetClipboardOwner(); if (hwnd == IntPtr.Zero) return string.Empty; NativeMethods.GetWindowThreadProcessId(hwnd, out processId); if (processId == 0) return string.Empty; if ((int)processId == Process.GetCurrentProcess().Id) return "clipman"; var process = Process.GetProcessById((int)processId); return NormalizeProcessName(process.ProcessName); } catch { return string.Empty; } } private bool IsIgnoredProcess(string processName) { processName = NormalizeProcessName(processName); if (IsClipmanProcess(processName)) return true; if (settings.IgnoredProcesses == null || settings.IgnoredProcesses.Count == 0) return false; return settings.IgnoredProcesses.Any(p => IgnoredProcessMatches(NormalizeProcessName(p), processName)); } private static bool IgnoredProcessMatches(string ignoredProcessName, string processName) { if (string.IsNullOrWhiteSpace(ignoredProcessName) || string.IsNullOrWhiteSpace(processName)) return false; if (string.Equals(ignoredProcessName, processName, StringComparison.OrdinalIgnoreCase)) return true; return processName.StartsWith(ignoredProcessName + ".", StringComparison.OrdinalIgnoreCase) || processName.StartsWith(ignoredProcessName + "-", StringComparison.OrdinalIgnoreCase) || processName.StartsWith(ignoredProcessName + "_", StringComparison.OrdinalIgnoreCase) || processName.StartsWith(ignoredProcessName + " ", StringComparison.OrdinalIgnoreCase); } private static bool IsClipmanProcess(string processName) { return string.Equals(NormalizeProcessName(processName), "clipman", StringComparison.OrdinalIgnoreCase); } private static string NormalizeProcessName(string processName) { if (string.IsNullOrWhiteSpace(processName)) return string.Empty; var trimmed = processName.Trim(); return trimmed.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ? Path.GetFileNameWithoutExtension(trimmed) : trimmed; } private static string FriendlyProcessName(string processName) { var normalized = NormalizeProcessName(processName); if (string.IsNullOrWhiteSpace(normalized)) return string.Empty; return normalized.Substring(0, 1).ToUpperInvariant() + (normalized.Length > 1 ? normalized.Substring(1) : string.Empty); } private string ClipboardFloodProtectionStatus(long nowMilliseconds) { var activeSources = clipboardFloodGuards.ActiveSources(nowMilliseconds) .Select(FriendlyProcessName) .Where(name => !string.IsNullOrWhiteSpace(name)) .OrderBy(name => name, StringComparer.OrdinalIgnoreCase) .ToList(); return activeSources.Count == 0 ? "ready" : "suppressing rapid updates from " + string.Join(", ", activeSources); } private static string FormatDiagnosticTime(long unixMs) { if (unixMs <= 0) return "Never"; return TimeUtil.FromUnixMs(unixMs).ToString("yyyy-MM-dd HH:mm:ss"); } private string BuildDiagnosticsText() { var diagnosticNowMilliseconds = ClipboardFloodGuard.MonotonicMilliseconds(); var sharedExecutableSettingsDirectory = SharedExecutableSettingsDirectory(); var sharedUpdateCoordinationEnabled = SharedUpdateStateStore.CanCoordinateExecutable( sharedExecutableSettingsDirectory, Application.ExecutablePath); var sharedState = sharedUpdateCoordinationEnabled ? SharedUpdateStateStore.Load(sharedExecutableSettingsDirectory) : new SharedUpdateState(); var serverStatus = store.GetServerSyncStatus(); var entries = store.GetEntries(); var richEntries = entries.Where(e => RichTextData.Normalize(e.RichText) != null).ToList(); var richHtmlEntries = richEntries.Count(e => !string.IsNullOrEmpty(e.RichText.HtmlFragment)); var richRtfEntries = richEntries.Count(e => !string.IsNullOrEmpty(e.RichText.RtfBase64)); long richBytes = richEntries.Sum(e => (long)Encoding.UTF8.GetByteCount(e.RichText.HtmlFragment ?? string.Empty) + RichTextData.DecodedRtfByteCount(e.RichText.RtfBase64)); var ignored = settings.IgnoredProcesses == null || settings.IgnoredProcesses.Count == 0 ? "None" : string.Join(", ", settings.IgnoredProcesses); return "Clipman diagnostics\r\n\r\n" + "Active: " + settings.Active + "\r\n" + "Sounds enabled: " + settings.SoundsEnabled + "\r\n" + "Storage mode: " + settings.StorageMode + "\r\n" + "Server host: " + (string.IsNullOrWhiteSpace(settings.ServerUrl) ? "not set" : ServerSettingsSanitizer.CleanTransportUrl(settings.ServerUrl)) + "\r\n" + "Server sync enabled: " + serverStatus.Enabled + "\r\n" + "Server sync configured: " + serverStatus.Configured + "\r\n" + "Server sync revision: " + (string.IsNullOrWhiteSpace(serverStatus.Revision) ? "None" : serverStatus.Revision) + "\r\n" + "Server sync last poll: " + FormatDiagnosticTime(serverStatus.LastPollUnixMs) + "\r\n" + "Server sync last success: " + FormatDiagnosticTime(serverStatus.LastSuccessUnixMs) + "\r\n" + "Server sync last upload: " + FormatDiagnosticTime(serverStatus.LastUploadUnixMs) + "\r\n" + "Server sync next retry: " + FormatDiagnosticTime(serverStatus.NextPollUnixMs) + "\r\n" + "Server sync consecutive failures: " + serverStatus.ConsecutiveFailures + "\r\n" + "Database path: " + settings.DatabasePath + "\r\n" + "Database path type: " + (settings.UseDefaultDatabasePath ? "Default beside Clipman" : "Explicit") + "\r\n" + "Database storage status: " + (string.IsNullOrWhiteSpace(store.LastStorageError) ? "OK" : "Unavailable: " + store.LastStorageError) + "\r\n" + "Entries: " + entries.Count + "\r\n" + "Rich text history: " + (settings.RichTextHistoryEnabled ? "enabled" : "disabled") + "\r\n" + "Rich text images: " + (settings.IncludeImagesInRichText ? "enabled" : "disabled") + "\r\n" + "Automatically add copied image files to Rich Text: " + (settings.AutoAddImageFilesToRichText ? "enabled" : "disabled") + "\r\n" + "Rich text payloads: " + richEntries.Count + " entries (HTML " + richHtmlEntries + ", RTF " + richRtfEntries + ", " + richBytes + " bytes)\r\n" + "File history path: " + fileEventStore.DatabasePath + "\r\n" + "File history storage status: " + (string.IsNullOrWhiteSpace(fileEventStore.LastStorageError) ? "OK" : "Unavailable: " + fileEventStore.LastStorageError) + "\r\n" + "File history events: " + fileEventStore.GetEvents().Count + "\r\n" + "File history diagnostics limit: " + settings.DiagnosticsFileHistoryLimit + "\r\n" + "Auto remove unavailable file history events: " + settings.AutoRemoveUnavailableFileHistoryEvents + "\r\n" + "Show history hotkey: " + settings.ShowHistoryHotkey + " (" + (showHotkeyRegistered ? "registered" : "not registered") + ")\r\n" + "Toggle hotkey: " + settings.ToggleActiveHotkey + " (" + (toggleHotkeyRegistered ? "registered" : "not registered") + ")\r\n" + "Save current clipboard hotkey: " + (string.IsNullOrWhiteSpace(settings.SaveCurrentClipboardHotkey) ? "Not assigned" : settings.SaveCurrentClipboardHotkey + " (" + (saveCurrentClipboardHotkeyRegistered ? "registered" : "not registered") + ")") + "\r\n" + "Quick Clip hotkey: " + (string.IsNullOrWhiteSpace(settings.QuickClipHotkey) ? "Not assigned" : settings.QuickClipHotkey + " (" + (quickClipHotkeyRegistered ? "registered" : "not registered") + ")") + "\r\n" + "Toggle alternate UK key: " + (toggleAlternateHotkeyRegistered ? "registered" : "not registered or not needed") + "\r\n" + "Quick Paste bindings: " + ((settings.QuickCopyHotkeys == null ? 0 : settings.QuickCopyHotkeys.Count) + " configured, " + quickCopyHotkeysRegistered + " registered") + "\r\n" + "Secrets: " + (GetSecretEntriesSafe().Count + " configured, " + secretHotkeysRegistered + " hotkeys registered") + "\r\n" + "Auto-copy received and command text: " + (settings.AutoCopyLatestRemoteText ? "on" : "off") + "\r\n" + "Paste after Enter: " + (settings.PasteAfterEnter ? "on" : "off") + "\r\n" + "Dynamic history mode: " + (settings.DynamicHistoryMode ? "on" : "off") + "\r\n" + "Build stamp: " + BuildInfo.BuildStampUtcMs + "\r\n" + "Executable hash: " + SharedUpdateStateStore.CurrentExeHash() + "\r\n" + "Shared executable update coordination: " + (sharedUpdateCoordinationEnabled ? "enabled for install-local Settings" : "disabled for separately stored settings") + "\r\n" + "Shared update state path: " + (sharedUpdateCoordinationEnabled ? SharedUpdateStateStore.StatePath(sharedExecutableSettingsDirectory) : "Not used") + "\r\n" + "Shared update state build stamp: " + (sharedState == null ? 0 : sharedState.BuildStampUtcMs) + "\r\n" + "Remove duplicates: " + settings.RemoveDuplicates + "\r\n" + "Duplicate mode: " + settings.DuplicateMode + "\r\n" + "Auto group by app: " + settings.AutoGroupByApp + "\r\n" + "Auto remove URL tracking: " + settings.AutoRemoveUrlTracking + "\r\n" + "Sensitive data mode: " + SensitiveDataExclusion.NormalizeMode(settings.SensitiveDataMode) + "\r\n" + "Sensitive data presets: " + SensitiveDataPresetSummary() + "\r\n" + "Last clipboard privacy signal: " + lastClipboardPrivacySignal + "\r\n" + "Clipboard flood protection: " + ClipboardFloodProtectionStatus(diagnosticNowMilliseconds) + "\r\n" + "Clipboard flood protection activations: " + clipboardFloodGuards.SuppressionCount + "\r\n" + "Clipboard flood events suppressed: " + clipboardFloodGuards.SuppressedEventCount + "\r\n" + "Run at startup: " + settings.RunAtStartup + "\r\n" + "Add clipboard item on startup: " + settings.CaptureClipboardOnStartup + "\r\n" + "Startup registration present: " + StartupRegistration.IsEnabled() + "\r\n" + "Update check frequency: " + settings.UpdateCheckFrequency + "\r\n" + "Install updates silently: " + settings.InstallUpdatesSilently + "\r\n" + "Database encryption enabled: " + settings.DatabaseEncryptionEnabled + "\r\n" + "Runtime crash log: " + Program.RuntimeLogPath() + "\r\n" + websiteTitleDiagnostics.Summary() + "\r\n" + "User sound override folder: " + Path.Combine(settingsStore.SettingsDirectory, "sounds") + "\r\n" + "Group filter: " + settings.GroupFilter + "\r\n" + "Foreground process: " + FriendlyProcessName(ForegroundProcessName()) + "\r\n" + "Clipboard owner process: " + FriendlyProcessName(ClipboardOwnerProcessName()) + "\r\n" + "Maximum entries: " + (settings.MaxHistoryEntries <= 0 ? "No limit" : settings.MaxHistoryEntries.ToString()) + "\r\n" + "Maximum age: " + (settings.MaxHistoryDays <= 0 ? "No limit" : settings.MaxHistoryDays + " days") + "\r\n" + "Ignored applications: " + ignored + "\r\n\r\n" + BuildRecentClipboardEventsText(); } private string SensitiveDataPresetSummary() { if (settings.SensitiveDataPresetIds == null || settings.SensitiveDataPresetIds.Count == 0) { return "None"; } var names = SensitiveDataExclusion.BuiltInPresets .Where(p => settings.SensitiveDataPresetIds.Any(id => string.Equals(id, p.Id, StringComparison.OrdinalIgnoreCase))) .Select(p => p.Name) .ToList(); return names.Count == 0 ? "None" : string.Join(", ", names); } private string BuildRecentClipboardEventsText() { var snapshots = fileEventStore.GetEvents(); var total = snapshots.Count; var limit = settings.DiagnosticsFileHistoryLimit; if (limit < 0) limit = 0; if (limit > 200) limit = 200; if (total == 0) { return "File and non-text clipboard events: none recorded."; } if (limit == 0) { return "File and non-text clipboard events: " + total + " recorded. Details omitted by diagnostics preference."; } var lines = new List { "File and non-text clipboard events: showing " + Math.Min(limit, total) + " of " + total + "." }; foreach (var item in snapshots.Take(limit)) { var title = item.CapturedAt.ToString("yyyy-MM-dd HH:mm:ss") + " | Source: " + (string.IsNullOrWhiteSpace(item.Source) ? "Unknown" : item.Source); if (!string.IsNullOrWhiteSpace(item.Operation)) { title += " | Operation: " + item.Operation; } if (item.FileCount > 0) { title += " | Files: " + item.FileCount; } if (item.ContainsText) { title += " | Also contains text"; } lines.Add(title); if (item.Files != null && item.Files.Count > 0) { foreach (var file in item.Files.Take(20)) { lines.Add(" " + file); } if (item.FileCount > 20) { lines.Add(" ... " + (item.FileCount - 20) + " more file(s)"); } } else if (item.Formats != null && item.Formats.Count > 0) { lines.Add(" Formats: " + string.Join(", ", item.Formats.Take(12))); } } if (total > limit) { lines.Add("... " + (total - limit) + " more event(s) omitted by diagnostics preference."); } return string.Join("\r\n", lines); } private void StoreChanged(object sender, EventArgs e) { if (store.LastChangeWasExternal) { if (invoker != null && invoker.IsHandleCreated) { invoker.BeginInvoke(new Action(MaybeAutoCopyLatestRemoteEntry)); } } if (historyForm != null && !historyForm.IsDisposed) { historyForm.BeginInvoke(new Action(() => historyForm.Reload())); } } private void QuickPasteEntry(string entryId) { var entry = store.GetEntryById(entryId); if (entry == null || string.IsNullOrEmpty(entry.Text)) { sounds.Skip(settings.SoundsEnabled); return; } try { var mode = QuickPasteModeForEntry(entryId); if (mode == QuickPasteModes.CopyOnly) { SetClipboardIgnoringNotification(() => SetEntryClipboard(entry)); store.MarkUsed(entry.Id); sounds.Copy(settings.SoundsEnabled); return; } IDataObject previousClipboard = null; if (mode == QuickPasteModes.PasteRestore) { try { previousClipboard = SnapshotClipboardData(); } catch { previousClipboard = null; } } SetClipboardIgnoringNotification(() => SetEntryClipboard(entry)); store.MarkUsed(entry.Id); sounds.Copy(settings.SoundsEnabled); if (mode == QuickPasteModes.PasteKeep) { BeginPasteOnly(); } else { BeginPasteThenRestore(previousClipboard); } } catch { sounds.Skip(settings.SoundsEnabled); } } private string QuickPasteModeForEntry(string entryId) { if (string.IsNullOrWhiteSpace(entryId) || settings.QuickCopyHotkeys == null) return QuickPasteModes.PasteRestore; var binding = settings.QuickCopyHotkeys.FirstOrDefault(b => b != null && string.Equals(b.EntryId, entryId, StringComparison.OrdinalIgnoreCase)); return binding == null ? QuickPasteModes.PasteRestore : QuickPasteModes.Normalize(binding.Mode); } private void QuickPasteSecret(SecretEntry secret) { if (secret == null || string.IsNullOrEmpty(secret.Value)) { sounds.Skip(settings.SoundsEnabled); return; } try { IDataObject previousClipboard = null; try { previousClipboard = SnapshotClipboardData(); } catch { previousClipboard = null; } SetClipboardIgnoringNotification(() => Clipboard.SetText(secret.Value, TextDataFormat.UnicodeText)); sounds.Copy(settings.SoundsEnabled); BeginPasteThenRestore(previousClipboard); } catch { sounds.Skip(settings.SoundsEnabled); } } private List GetSecretEntriesSafe() { if (secretStore == null) return new List(); try { return secretStore.GetEntries(); } catch (Exception ex) { Debug.WriteLine("Could not read secrets store: " + ex); return new List(); } } private void BeginPasteOnly() { ThreadPool.QueueUserWorkItem(_ => { WaitForHotkeyModifiersReleased(); BeginInvokeIfReady(SendCtrlVPaste); }); } private void RememberPreviousForegroundWindow() { var foreground = NativeMethods.GetForegroundWindow(); if (foreground == IntPtr.Zero) return; uint processId; NativeMethods.GetWindowThreadProcessId(foreground, out processId); if (processId == (uint)Process.GetCurrentProcess().Id) return; previousForegroundWindow = foreground; } private void PasteClipboardIntoPreviousApplication() { var target = previousForegroundWindow; previousForegroundWindow = IntPtr.Zero; if (target == IntPtr.Zero || !NativeMethods.IsWindow(target)) { sounds.Skip(settings.SoundsEnabled); return; } ThreadPool.QueueUserWorkItem(_ => { WaitForHotkeyModifiersReleased(); BeginInvokeIfReady(() => ActivatePreviousWindowAndPaste(target, 2)); }); } private void ActivatePreviousWindowAndPaste(IntPtr target, int attemptsRemaining) { if (!NativeMethods.IsWindow(target)) { sounds.Skip(settings.SoundsEnabled); return; } NativeMethods.SetForegroundWindow(target); var timer = new System.Windows.Forms.Timer { Interval = 100 }; timer.Tick += (sender, args) => { timer.Stop(); timer.Dispose(); if (NativeMethods.GetForegroundWindow() == target) { SendCtrlVPaste(); } else if (attemptsRemaining > 0) { ActivatePreviousWindowAndPaste(target, attemptsRemaining - 1); } else { sounds.Skip(settings.SoundsEnabled); } }; timer.Start(); } private void BeginPasteThenRestore(IDataObject previousClipboard) { ThreadPool.QueueUserWorkItem(_ => { WaitForHotkeyModifiersReleased(); BeginInvokeIfReady(SendCtrlVPaste); Thread.Sleep(600); BeginInvokeIfReady(() => RestoreClipboard(previousClipboard)); }); } private static IDataObject SnapshotClipboardData() { return SnapshotClipboardData(Clipboard.GetDataObject()); } internal static IDataObject SnapshotClipboardData(IDataObject source) { if (source == null) return null; var snapshot = new DataObject(); var copied = false; copied |= SnapshotText(source, snapshot, DataFormats.UnicodeText, TextDataFormat.UnicodeText); copied |= SnapshotText(source, snapshot, DataFormats.Text, TextDataFormat.Text); copied |= SnapshotText(source, snapshot, DataFormats.OemText, TextDataFormat.Text); copied |= SnapshotText(source, snapshot, DataFormats.Rtf, TextDataFormat.Rtf); copied |= SnapshotText(source, snapshot, DataFormats.Html, TextDataFormat.Html); copied |= SnapshotText(source, snapshot, DataFormats.CommaSeparatedValue, TextDataFormat.CommaSeparatedValue); var url = ClipboardUrlData.TryRead(source); if (url != null) { if (!snapshot.GetDataPresent(DataFormats.UnicodeText, false)) { snapshot.SetText(url, TextDataFormat.UnicodeText); } snapshot.SetData("UniformResourceLocatorW", false, new MemoryStream(Encoding.Unicode.GetBytes(url + "\0"))); copied = true; } try { if (source.GetDataPresent(DataFormats.FileDrop, false)) { var files = source.GetData(DataFormats.FileDrop, false) as string[]; if (files != null && files.Length > 0) { var fileList = new StringCollection(); fileList.AddRange(files); snapshot.SetFileDropList(fileList); copied = true; } } } catch { } copied |= SnapshotImage(source, snapshot); copied |= SnapshotStream(source, snapshot, DataFormats.WaveAudio, MaximumClipboardSnapshotStreamLength); // Explorer uses this small stream to distinguish a cut from a copy. It is // independently cloneable, unlike delayed OLE formats such as metafiles. copied |= SnapshotStream(source, snapshot, "Preferred DropEffect", 64); return copied ? snapshot : null; } private static bool SnapshotImage(IDataObject source, DataObject snapshot) { try { if (!source.GetDataPresent(DataFormats.Bitmap, false)) return false; var image = source.GetData(DataFormats.Bitmap, false) as Image; if (image == null) return false; snapshot.SetImage(new Bitmap(image)); return true; } catch { return false; } } private static bool SnapshotStream(IDataObject source, DataObject snapshot, string format, int maximumLength) { try { if (!source.GetDataPresent(format, false)) return false; var data = source.GetData(format, false); var stream = data as Stream; if (stream != null) { if (stream.CanSeek && stream.Length > maximumLength) return false; var copy = new MemoryStream(); var originalPosition = stream.CanSeek ? stream.Position : 0; try { if (stream.CanSeek) stream.Position = 0; CopyBounded(stream, copy, maximumLength); } finally { if (stream.CanSeek) stream.Position = originalPosition; } copy.Position = 0; snapshot.SetData(format, false, copy); return true; } var bytes = data as byte[]; if (bytes != null && bytes.Length <= maximumLength) { snapshot.SetData(format, false, bytes.ToArray()); return true; } } catch { } return false; } private static bool SnapshotText(IDataObject source, DataObject snapshot, string dataFormat, TextDataFormat textFormat) { try { if (!source.GetDataPresent(dataFormat, false)) return false; var text = source.GetData(dataFormat, false) as string; if (text == null || text.Length > MaximumClipboardSnapshotTextLength) return false; snapshot.SetText(text, textFormat); return true; } catch { return false; } } private static void CopyBounded(Stream source, Stream destination, int maximumLength) { var buffer = new byte[81920]; var total = 0; while (true) { var read = source.Read(buffer, 0, Math.Min(buffer.Length, maximumLength - total + 1)); if (read <= 0) return; total += read; if (total > maximumLength) throw new InvalidDataException("Clipboard format exceeds the snapshot limit."); destination.Write(buffer, 0, read); } } private void BeginInvokeIfReady(Action action) { if (action == null) return; if (invoker == null || invoker.IsDisposed || !invoker.IsHandleCreated) return; try { invoker.BeginInvoke(action); } catch { } } private static void WaitForHotkeyModifiersReleased() { var deadline = DateTime.UtcNow.AddMilliseconds(1200); while (DateTime.UtcNow < deadline && AnyHotkeyModifierDown()) { Thread.Sleep(20); } Thread.Sleep(40); } private static bool AnyHotkeyModifierDown() { return IsKeyDown(NativeMethods.VK_CONTROL) || IsKeyDown(NativeMethods.VK_SHIFT) || IsKeyDown(NativeMethods.VK_MENU) || IsKeyDown(NativeMethods.VK_LWIN) || IsKeyDown(NativeMethods.VK_RWIN); } private static bool IsKeyDown(int virtualKey) { return (NativeMethods.GetAsyncKeyState(virtualKey) & unchecked((short)0x8000)) != 0; } private static void SendCtrlVPaste() { if (!KeyboardInput.SendControlVPaste()) { throw new InvalidOperationException("Windows did not accept the paste keyboard input."); } } private void RestoreClipboard(IDataObject previousClipboard) { try { if (previousClipboard == null) { SetClipboardIgnoringNotification(Clipboard.Clear); } else { SetClipboardIgnoringNotification(() => Clipboard.SetDataObject(previousClipboard, true)); } } catch { } } private void StoreStorageStateChanged(object sender, StorageStateChangedEventArgs e) { Program.WriteRuntimeLog( e.Unavailable ? "Text-history storage became unavailable: " + e.Error : "Text-history storage recovered.", null); try { if (invoker != null && invoker.IsHandleCreated) { invoker.BeginInvoke(new Action(UpdateTray)); } } catch (InvalidOperationException) { } } private void SetClipboardIgnoringNotification(Action setClipboard) { if (setClipboard == null) return; clipMergeDetector.Reset(); setClipboard(); clipboardNotifications.Ignore(NativeMethods.GetClipboardSequenceNumber()); } private void ResetRemoteAutoCopyBaseline() { var entry = store == null ? null : store.GetNewestRemoteEntry(CurrentDeviceName()); if (entry == null) { lastAutoCopiedRemoteEntryId = string.Empty; lastAutoCopiedRemoteEntryStamp = 0; return; } lastAutoCopiedRemoteEntryId = entry.Id ?? string.Empty; lastAutoCopiedRemoteEntryStamp = entry.CreatedUnixMs; } private void MaybeAutoCopyLatestRemoteEntry() { if (!settings.AutoCopyLatestRemoteText) return; var entry = store.GetNewestRemoteEntry(CurrentDeviceName()); if (entry == null || string.IsNullOrEmpty(entry.Text)) return; var stamp = entry.CreatedUnixMs; if (stamp < lastAutoCopiedRemoteEntryStamp) { return; } if (stamp == lastAutoCopiedRemoteEntryStamp && string.Equals(entry.Id ?? string.Empty, lastAutoCopiedRemoteEntryId, StringComparison.Ordinal)) { return; } lastAutoCopiedRemoteEntryId = entry.Id ?? string.Empty; lastAutoCopiedRemoteEntryStamp = stamp; SetClipboardIgnoringNotification(() => SetEntryClipboard(entry)); sounds.Remote(settings.SoundsEnabled); } private void MaybeCopyCommandEntry(string entryId) { if (!settings.AutoCopyLatestRemoteText) return; var entry = store.GetEntryById(entryId); if (entry == null || string.IsNullOrEmpty(entry.Text)) return; try { SetClipboardIgnoringNotification(() => SetEntryClipboard(entry)); sounds.Copy(settings.SoundsEnabled); } catch (Exception ex) { Program.WriteRuntimeLog("Could not put a command-line history addition on the clipboard.", ex); sounds.Skip(settings.SoundsEnabled); } } private static void SetEntryClipboard(ClipEntry entry) { var text = ResolvedEntryText(entry); var data = new DataObject(); data.SetText(text, TextDataFormat.UnicodeText); if (entry != null && !entry.IsTemplate && string.Equals(text, entry.Text ?? string.Empty, StringComparison.Ordinal)) { RichTextData.AddToDataObject(data, entry.RichText, entry); } Clipboard.SetDataObject(data, true); } private static string ResolvedEntryText(ClipEntry entry) { if (entry == null) return string.Empty; var text = entry.Text ?? string.Empty; return entry.IsTemplate ? TemplateResolver.Resolve(text) : text; } private void FileEventStoreChanged(object sender, EventArgs e) { if (historyForm != null && !historyForm.IsDisposed) { historyForm.BeginInvoke(new Action(() => historyForm.RefreshFileClipboardEvents())); } } private string TrayText() { var storageMessage = StorageUnavailableMessage(); if (!string.IsNullOrWhiteSpace(storageMessage)) { return ShortTrayText("Clipman: storage unavailable"); } return settings.Active ? "Clipman: on" : "Clipman: off"; } private bool IsStorageUnavailable() { return !string.IsNullOrWhiteSpace(StorageUnavailableMessage()); } private string StorageUnavailableMessage() { var parts = new List(); if (!string.IsNullOrWhiteSpace(store.LastStorageError)) { parts.Add("Text history: " + store.LastStorageError); } if (!string.IsNullOrWhiteSpace(fileEventStore.LastStorageError)) { parts.Add("File history: " + fileEventStore.LastStorageError); } return string.Join("; ", parts); } private static string ShortTrayText(string text) { if (string.IsNullOrEmpty(text)) return string.Empty; return text.Length <= 63 ? text : text.Substring(0, 63); } private void UpdateTray() { notifyIcon.Text = TrayText(); notifyIcon.Icon = BuildIcon(settings.Active); notifyIcon.ContextMenuStrip = BuildTrayMenu(); if (historyForm != null && !historyForm.IsDisposed && historyForm.IsHandleCreated) { historyForm.BeginInvoke(new Action(historyForm.RefreshSteadyStatus)); } } private string HistorySteadyStatusText() { var monitoring = settings.Active ? string.Empty : "Monitoring off. "; var fileStorageError = fileEventStore == null ? string.Empty : fileEventStore.LastStorageError; // The channel notice is good news about one entry; it must never stand in for a storage // or connectivity problem, so it is only offered where the status would say "Ready". var channelNotice = store == null ? string.Empty : store.ChannelAnnouncement(); var sync = store == null ? null : store.GetServerSyncStatus(); if (IsServerStorageEnabled()) { if (sync == null || !sync.Enabled || !sync.Configured) { return monitoring + "Server is not configured; using local cache."; } if (!string.IsNullOrWhiteSpace(sync.LastError)) { return monitoring + "Server unavailable; using local cache."; } if (sync.LastSuccessUnixMs <= 0) { return monitoring + "Connecting to Clipman Server."; } if (!string.IsNullOrWhiteSpace(fileStorageError)) { return monitoring + "File history storage unavailable."; } if (!string.IsNullOrEmpty(channelNotice)) { return monitoring + channelNotice; } return settings.Active ? "Ready. Server sync connected." : "Monitoring off. Server sync connected."; } if ((store != null && !string.IsNullOrWhiteSpace(store.LastStorageError)) || !string.IsNullOrWhiteSpace(fileStorageError)) { return monitoring + "Storage unavailable."; } if (!string.IsNullOrEmpty(channelNotice)) { return monitoring + channelNotice; } return settings.Active ? "Ready. Using local or shared-folder history." : "Monitoring off. Using local or shared-folder history."; } protected override void Dispose(bool disposing) { if (disposing) { if (closeEvent != null) closeEvent.Dispose(); if (showEvent != null) showEvent.Dispose(); if (recoverEvent != null) recoverEvent.Dispose(); if (pauseEvent != null) pauseEvent.Dispose(); if (resumeEvent != null) resumeEvent.Dispose(); if (toggleEvent != null) toggleEvent.Dispose(); if (historyChangedEvent != null) historyChangedEvent.Dispose(); if (historyAddedEvent != null) historyAddedEvent.Dispose(); if (invoker != null) invoker.Dispose(); if (sharedStateWatcher != null) sharedStateWatcher.Dispose(); if (executableWatcher != null) executableWatcher.Dispose(); if (sharedStateTimer != null) sharedStateTimer.Dispose(); if (updateCheckTimer != null) updateCheckTimer.Dispose(); if (clipboardFloodRecoveryTimer != null) clipboardFloodRecoveryTimer.Dispose(); NativeMethods.RemoveClipboardFormatListener(messageWindow.Handle); NativeMethods.UnregisterHotKey(messageWindow.Handle, ShowHotkeyId); NativeMethods.UnregisterHotKey(messageWindow.Handle, ToggleHotkeyId); NativeMethods.UnregisterHotKey(messageWindow.Handle, ToggleHotkeyAlternateId); NativeMethods.UnregisterHotKey(messageWindow.Handle, SaveCurrentClipboardHotkeyId); NativeMethods.UnregisterHotKey(messageWindow.Handle, QuickClipHotkeyId); foreach (var hotkeyId in quickCopyHotkeyEntryIds.Keys.ToList()) { NativeMethods.UnregisterHotKey(messageWindow.Handle, hotkeyId); } notifyIcon.Visible = false; notifyIcon.Dispose(); store.StorageStateChanged -= StoreStorageStateChanged; store.Dispose(); fileEventStore.Dispose(); messageWindow.Dispose(); messageWindow.DestroyHandle(); } base.Dispose(disposing); } private void WaitForClose() { try { closeEvent.WaitOne(); if (invoker != null && invoker.IsHandleCreated) { invoker.BeginInvoke(new Action(ExitThread)); } } catch { } } private void WaitForShow() { while (true) { try { showEvent.WaitOne(); showEvent.Reset(); if (invoker != null && invoker.IsHandleCreated) { invoker.BeginInvoke(new Action(ShowHistory)); } } catch { return; } } } private void WaitForState(EventWaitHandle ev, bool active) { while (true) { try { ev.WaitOne(); ev.Reset(); if (invoker != null && invoker.IsHandleCreated) { invoker.BeginInvoke(new Action(() => SetActive(active, true))); } } catch { return; } } } private void WaitForToggle() { while (true) { try { toggleEvent.WaitOne(); toggleEvent.Reset(); if (invoker != null && invoker.IsHandleCreated) { invoker.BeginInvoke(new Action(ToggleActive)); } } catch { return; } } } private void StartSharedUpdateWatchers() { var sharedExecutableSettingsDirectory = SharedExecutableSettingsDirectory(); if (!SharedUpdateStateStore.CanCoordinateExecutable(sharedExecutableSettingsDirectory, Application.ExecutablePath)) return; try { Directory.CreateDirectory(sharedExecutableSettingsDirectory); sharedStateTimer = new System.Threading.Timer(delegate { try { CheckSharedUpdateState(); } catch (Exception ex) { Program.WriteRuntimeLog("Shared executable update coordination check failed.", ex); } }, null, Timeout.Infinite, Timeout.Infinite); sharedStateWatcher = new FileSystemWatcher(sharedExecutableSettingsDirectory, "clipman-shared-state*.json") { NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.FileName | NotifyFilters.CreationTime }; sharedStateWatcher.Changed += SharedUpdateStateChanged; sharedStateWatcher.Created += SharedUpdateStateChanged; sharedStateWatcher.Renamed += SharedUpdateStateChanged; sharedStateWatcher.EnableRaisingEvents = true; executableWatcher = new FileSystemWatcher(appDirectory, "clipman.exe") { NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.FileName | NotifyFilters.CreationTime }; executableWatcher.Changed += SharedUpdateStateChanged; executableWatcher.Created += SharedUpdateStateChanged; executableWatcher.Renamed += SharedUpdateStateChanged; executableWatcher.EnableRaisingEvents = true; } catch { } } private void RestartSharedUpdateWatchers() { try { if (sharedStateWatcher != null) { sharedStateWatcher.Dispose(); sharedStateWatcher = null; } if (executableWatcher != null) { executableWatcher.Dispose(); executableWatcher = null; } if (sharedStateTimer != null) { sharedStateTimer.Dispose(); sharedStateTimer = null; } StartSharedUpdateWatchers(); ScheduleSharedUpdateCheck(5000); } catch { } } private void ReopenFileHistoryStore() { try { if (fileEventStore != null) { fileEventStore.Changed -= FileEventStoreChanged; fileEventStore.Dispose(); } } catch { } fileEventStore = new FileClipboardEventStore(settingsStore.DefaultFileHistoryDatabasePath(), CurrentDatabasePassword); fileEventStore.Changed += FileEventStoreChanged; } private void ApplyStartupRegistration(bool showErrors) { try { StartupRegistration.SetEnabled(settings.RunAtStartup); } catch (Exception ex) { if (showErrors) { MessageBox.Show("Could not update the Windows startup entry.\r\n\r\n" + ex.Message, "Clipman startup", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } } private void ScheduleUpdateChecks() { try { if (updateCheckTimer != null) { updateCheckTimer.Dispose(); updateCheckTimer = null; } var frequency = settings.UpdateCheckFrequency ?? "Never"; if (string.Equals(frequency, "Never", StringComparison.OrdinalIgnoreCase)) { return; } var period = Timeout.Infinite; if (string.Equals(frequency, "Hourly", StringComparison.OrdinalIgnoreCase)) { period = (int)TimeSpan.FromHours(1).TotalMilliseconds; } else if (string.Equals(frequency, "Daily", StringComparison.OrdinalIgnoreCase)) { period = (int)TimeSpan.FromDays(1).TotalMilliseconds; } updateCheckTimer = new System.Threading.Timer(delegate { CheckForUpdatesAutomatically(); }, null, 30000, period); } catch { } } private void CheckForUpdatesAutomatically() { try { if (invoker != null && invoker.IsHandleCreated) { invoker.BeginInvoke(new Action(() => UpdateService.CheckForUpdatesAutomatic(UpdateWindowOwner(), AppVersion(), ExitThread, settings.InstallUpdatesSilently))); } } catch { } } private void ResolveDatabasePassword() { ResolveDatabasePassword(EffectiveTextHistoryDatabasePath(), message => PasswordPromptForm.Ask( "Clipman history password", message)); } private void ResolveDatabasePassword(string activeDatabasePath, Func requestPassword) { // Server mode opens its cache, or seeds it from the configured file if absent. var historyPath = File.Exists(activeDatabasePath) ? activeDatabasePath : settings.DatabasePath; var encryptedPaths = new[] { historyPath, settingsStore.DefaultFileHistoryDatabasePath(), settingsStore.DefaultSecretsDatabasePath() }.Where(path => !string.IsNullOrWhiteSpace(path) && ClipDatabaseFile.IsEncryptedFile(path)) .Distinct(StringComparer.OrdinalIgnoreCase).ToList(); var encryptedHistoryExists = encryptedPaths.Count > 0; if (!encryptedHistoryExists && !settings.DatabaseEncryptionEnabled && string.IsNullOrWhiteSpace(settings.ProtectedDatabasePassword)) { databasePassword = string.Empty; settings.DatabaseEncryptionEnabled = false; settings.RememberDatabasePassword = false; settings.ProtectedDatabasePassword = string.Empty; return; } if (settings.RememberDatabasePassword && !string.IsNullOrWhiteSpace(settings.ProtectedDatabasePassword)) { try { var password = settingsStore.DatabasePassword(settings); if (!string.IsNullOrEmpty(password)) { databasePassword = password; settings.DatabaseEncryptionEnabled = true; return; } } catch { } } if (!string.IsNullOrEmpty(databasePassword)) { settings.DatabaseEncryptionEnabled = true; if (settings.RememberDatabasePassword && string.IsNullOrWhiteSpace(settings.ProtectedDatabasePassword)) { settings.ProtectedDatabasePassword = DatabasePasswordProtector.Protect(databasePassword); settingsStore.Save(settings); } return; } if (!encryptedHistoryExists) { settings.DatabaseEncryptionEnabled = false; settings.RememberDatabasePassword = false; settings.ProtectedDatabasePassword = string.Empty; settingsStore.Save(settings); return; } string entered; var prompt = "This machine needs the password for the encrypted Clipman history database."; while (true) { entered = requestPassword(prompt); if (string.IsNullOrEmpty(entered)) { throw new OperationCanceledException("Clipman history password was not provided."); } try { // Validate before remembering the password; never rewrite encrypted data here. foreach (var path in encryptedPaths) { ClipDatabaseFile.Load>(path, entered); } break; } catch (Exception ex) { if (!(ex is DatabasePasswordRequiredException) && !(ex is System.Security.Cryptography.CryptographicException)) throw; prompt = "Could not unlock history. Check the password and try again, or Cancel. Your data has not been changed."; } } databasePassword = entered; settings.DatabaseEncryptionEnabled = true; if (settings.RememberDatabasePassword) { settings.ProtectedDatabasePassword = DatabasePasswordProtector.Protect(entered); } else { settings.ProtectedDatabasePassword = string.Empty; } settingsStore.Save(settings); } private void ResolveDatabaseLocation() { if (IsServerStorageEnabled()) { if (settings.UseDefaultDatabasePath || string.IsNullOrWhiteSpace(settings.DatabasePath)) { settings.UseDefaultDatabasePath = true; settings.DatabasePath = settingsStore.DefaultDatabasePath(); settingsStore.Save(settings); } return; } if (settings.UseDefaultDatabasePath || string.IsNullOrWhiteSpace(settings.DatabasePath)) { settings.UseDefaultDatabasePath = true; settings.DatabasePath = settingsStore.DefaultDatabasePath(); settingsStore.Save(settings); return; } if (File.Exists(settings.DatabasePath)) { return; } var message = "Clipman cannot find the configured history database:" + Environment.NewLine + Environment.NewLine + settings.DatabasePath + Environment.NewLine + Environment.NewLine + "Choose Yes to browse for the data folder that contains the database." + Environment.NewLine + "Choose No to use the default database beside Clipman." + Environment.NewLine + "Choose Cancel to stop Clipman."; var choice = MessageBox.Show( message, "Clipman database not found", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button1); if (choice == DialogResult.Cancel) { throw new OperationCanceledException("Clipman database location was not selected."); } if (choice == DialogResult.Yes) { using (var dialog = new FolderBrowserDialog()) { dialog.Description = "Choose the Clipman data folder. Clipman will use clipman-history.clipdb inside this folder."; dialog.ShowNewFolderButton = true; var oldDir = Path.GetDirectoryName(settings.DatabasePath); if (!string.IsNullOrWhiteSpace(oldDir) && Directory.Exists(oldDir)) { dialog.SelectedPath = oldDir; } if (dialog.ShowDialog() != DialogResult.OK) { throw new OperationCanceledException("Clipman database location was not selected."); } settings.DatabasePath = Path.Combine(dialog.SelectedPath, "clipman-history.clipdb"); settings.UseDefaultDatabasePath = settingsStore.IsCurrentDefaultDatabasePath(settings.DatabasePath); settingsStore.Save(settings); return; } } settings.UseDefaultDatabasePath = true; settings.DatabasePath = settingsStore.DefaultDatabasePath(); settingsStore.Save(settings); } private string EffectiveTextHistoryDatabasePath() { return settingsStore.EffectiveTextHistoryDatabasePath(settings); } private void WaitForRecover() { while (true) { try { recoverEvent.WaitOne(); recoverEvent.Reset(); if (invoker != null && invoker.IsHandleCreated) { invoker.BeginInvoke(new Action(delegate { if (IsStorageUnavailable()) RetryStorage(); })); } } catch { return; } } } private void WaitForHistoryChanged() { while (true) { try { historyChangedEvent.WaitOne(); historyChangedEvent.Reset(); store.ReloadExternalChangeAndSync(); } catch (ObjectDisposedException) { return; } catch (Exception ex) { Program.WriteRuntimeLog("Could not reload and synchronize an external history change.", ex); } } } private void WaitForHistoryAdded() { while (true) { try { historyAddedEvent.WaitOne(); historyAddedEvent.Reset(); var entryId = InstanceStateStore.TakePendingCommandEntry((long)TimeSpan.FromMinutes(5).TotalMilliseconds); store.ReloadExternalChange(); if (!string.IsNullOrEmpty(entryId) && invoker != null && invoker.IsHandleCreated) { invoker.BeginInvoke(new Action(() => MaybeCopyCommandEntry(entryId))); } store.SyncExternalChange(); } catch (ObjectDisposedException) { return; } catch (Exception ex) { Program.WriteRuntimeLog("Could not process and synchronize a command-line history addition.", ex); } } } private string ServerCacheDatabasePath() { return settingsStore.EffectiveTextHistoryDatabasePath(new AppSettings { StorageMode = "Server" }); } private void MergeServerCacheIntoConfiguredDatabase(string serverCachePath) { if (string.IsNullOrWhiteSpace(serverCachePath) || !File.Exists(serverCachePath)) return; if (string.IsNullOrWhiteSpace(settings.DatabasePath)) return; try { var targetPath = settings.DatabasePath; if (string.Equals(Path.GetFullPath(serverCachePath), Path.GetFullPath(targetPath), StringComparison.OrdinalIgnoreCase)) return; var targetDatabase = File.Exists(targetPath) ? ClipDatabaseFile.Load(targetPath, CurrentDatabasePassword()) : new ClipDatabase(); var cacheDatabase = ClipDatabaseFile.Load(serverCachePath, CurrentDatabasePassword()); SyncConflictResolver.MergeInto(targetDatabase, cacheDatabase); var dir = Path.GetDirectoryName(targetPath); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); ClipDatabaseFile.SaveAtomic(targetPath, targetDatabase, CurrentDatabasePassword()); } catch (Exception ex) { Debug.WriteLine("Could not merge the server cache into the selected local database: " + ex.Message); } } private void SeedServerCacheFromConfiguredDatabase() { if (!IsServerStorageEnabled()) return; var cachePath = EffectiveTextHistoryDatabasePath(); if (File.Exists(cachePath)) return; if (string.IsNullOrWhiteSpace(settings.DatabasePath)) return; if (!File.Exists(settings.DatabasePath)) return; if (string.Equals(Path.GetFullPath(cachePath), Path.GetFullPath(settings.DatabasePath), StringComparison.OrdinalIgnoreCase)) return; try { var dir = Path.GetDirectoryName(cachePath); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); File.Copy(settings.DatabasePath, cachePath, false); } catch { } } private string CurrentDatabasePassword() { return databasePassword ?? string.Empty; } private string CurrentDeviceName() { var name = settings == null ? string.Empty : (settings.DeviceName ?? string.Empty).Trim(); return name.Length == 0 ? (Environment.MachineName ?? string.Empty).Trim() : name; } private string DefaultSecretsDatabasePath() { return settingsStore.DefaultSecretsDatabasePath(); } private void ReopenSecretStore() { try { secretStore = new SecretStore(DefaultSecretsDatabasePath(), CurrentDatabasePassword); if (secretsForm != null && !secretsForm.IsDisposed) { secretsForm.Close(); secretsForm = null; } } catch (Exception ex) { Debug.WriteLine("Could not reopen secrets store: " + ex); secretStore = null; } } private bool IsServerStorageEnabled() { return string.Equals(settings.StorageMode, "Server", StringComparison.OrdinalIgnoreCase); } private void ConfigureTextHistoryServerStorage() { if (store == null) return; store.ConfigureServerStorage( IsServerStorageEnabled(), settings.ServerUrl, settings.ServerToken, settings.ServerCaCertPem, settings.ServerCaHost); } private IWin32Window UpdateWindowOwner() { return historyForm != null && !historyForm.IsDisposed ? historyForm : null; } private static string AppVersion() { var version = typeof(ClipmanApplicationContext).Assembly.GetName().Version; return version == null ? "1.1.0" : version.Major + "." + version.Minor + "." + version.Build; } private string SharedExecutableSettingsDirectory() { return Path.Combine(appDirectory, "Settings"); } private void SharedUpdateStateChanged(object sender, FileSystemEventArgs e) { ScheduleSharedUpdateCheck(10000); } private void ScheduleSharedUpdateCheck(int delayMs) { try { if (sharedStateTimer != null) { sharedStateTimer.Change(delayMs, Timeout.Infinite); } } catch { } } private void CheckSharedUpdateState() { var sharedExecutableSettingsDirectory = SharedExecutableSettingsDirectory(); if (!SharedUpdateStateStore.CanCoordinateExecutable(sharedExecutableSettingsDirectory, Application.ExecutablePath)) return; SharedUpdateState closeState; if (SharedUpdateStateStore.HasActiveCloseRequest(sharedExecutableSettingsDirectory, Application.ExecutablePath, lastHandledCloseRequestId, out closeState)) { lastHandledCloseRequestId = closeState.CloseRequestId ?? string.Empty; if (invoker != null && invoker.IsHandleCreated) { invoker.BeginInvoke(new Action(StartStandDownRestartHelper)); } return; } SharedUpdateState state; string reason; if (!SharedUpdateStateStore.ShouldRestartForState(sharedExecutableSettingsDirectory, Application.ExecutablePath, out state, out reason)) { if (SharedUpdateStateStore.IsNewerStateFromAnotherMachine(state)) { if (invoker != null && invoker.IsHandleCreated) { invoker.BeginInvoke(new Action(StartStandDownRestartHelper)); } return; } if (!string.IsNullOrWhiteSpace(reason)) { ScheduleSharedUpdateCheck(15000); } else { ScheduleSharedUpdateCheck(60000); } return; } if (invoker != null && invoker.IsHandleCreated) { invoker.BeginInvoke(new Action(RestartForSharedUpdate)); } } private void RestartForSharedUpdate() { try { Process.Start(new ProcessStartInfo { FileName = Application.ExecutablePath, WorkingDirectory = appDirectory, UseShellExecute = true }); } catch { return; } ExitThread(); } private void StartStandDownRestartHelper() { var sharedExecutableSettingsDirectory = SharedExecutableSettingsDirectory(); if (!SharedUpdateStateStore.CanCoordinateExecutable(sharedExecutableSettingsDirectory, Application.ExecutablePath)) return; try { var tempRoot = Path.Combine(Path.GetTempPath(), "ClipmanRestart-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempRoot); var helperExe = Path.Combine(tempRoot, "clipman-restart-helper.exe"); File.Copy(Application.ExecutablePath, helperExe, true); Process.Start(new ProcessStartInfo { FileName = helperExe, Arguments = "--wait-restart" + " --restart-exe " + Quote(Application.ExecutablePath) + " --restart-working-dir " + Quote(appDirectory) + " --restart-state " + Quote(SharedUpdateStateStore.StatePath(sharedExecutableSettingsDirectory)) + " --restart-current-build " + BuildInfo.BuildStampUtcMs + " --restart-wait-pid " + Process.GetCurrentProcess().Id + " --restart-timeout-ms 120000", WorkingDirectory = tempRoot, UseShellExecute = false, CreateNoWindow = true }); ExitThread(); } catch { } } private static string Quote(string value) { if (value == null) return "\"\""; return "\"" + value.Replace("\"", "\\\"") + "\""; } private static Icon BuildIcon(bool active) { var bitmap = new Bitmap(16, 16); using (var g = Graphics.FromImage(bitmap)) using (var back = new SolidBrush(active ? Color.ForestGreen : Color.DarkRed)) using (var pen = new Pen(Color.White, 2)) { g.Clear(Color.Transparent); g.FillRectangle(back, 1, 1, 14, 14); g.DrawRectangle(Pens.Black, 1, 1, 14, 14); if (active) { g.DrawLine(pen, 4, 8, 7, 11); g.DrawLine(pen, 7, 11, 12, 4); } else { g.DrawLine(pen, 5, 5, 11, 11); g.DrawLine(pen, 11, 5, 5, 11); } } return Icon.FromHandle(bitmap.GetHicon()); } private sealed class MessageWindow : NativeWindow, IDisposable { private const int ClipboardSettleMilliseconds = 40; private readonly ClipmanApplicationContext app; private readonly System.Windows.Forms.Timer clipboardSettleTimer; public MessageWindow(ClipmanApplicationContext app) { this.app = app; clipboardSettleTimer = new System.Windows.Forms.Timer { Interval = ClipboardSettleMilliseconds }; clipboardSettleTimer.Tick += ClipboardSettleTimerTick; CreateHandle(new CreateParams()); } public void Dispose() { clipboardSettleTimer.Stop(); clipboardSettleTimer.Tick -= ClipboardSettleTimerTick; clipboardSettleTimer.Dispose(); } private void ClipboardSettleTimerTick(object sender, EventArgs e) { clipboardSettleTimer.Stop(); long? observedMilliseconds; var sequence = app.clipboardNotifications.TakePending(out observedMilliseconds); app.HandleClipboardUpdate(false, sequence, observedMilliseconds: observedMilliseconds); } protected override void WndProc(ref Message m) { if (m.Msg == NativeMethods.WM_HOTKEY) { app.HandleHotkey(m.WParam.ToInt32()); return; } if (m.Msg == NativeMethods.WM_CLIPBOARDUPDATE) { app.clipboardNotifications.Observe(NativeMethods.GetClipboardSequenceNumber(), ClipboardNotificationState.MessageObservedMilliseconds(NativeMethods.GetMessageTime(), Environment.TickCount, ClipboardFloodGuard.MonotonicMilliseconds())); clipboardSettleTimer.Stop(); clipboardSettleTimer.Start(); return; } base.WndProc(ref m); } } } }