1 /*
2   KeePass Password Safe - The Open-Source Password Manager
3   Copyright (C) 2003-2021 Dominik Reichl <dominik.reichl@t-online.de>
4 
5   This program is free software; you can redistribute it and/or modify
6   it under the terms of the GNU General Public License as published by
7   the Free Software Foundation; either version 2 of the License, or
8   (at your option) any later version.
9 
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14 
15   You should have received a copy of the GNU General Public License
16   along with this program; if not, write to the Free Software
17   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18 */
19 
20 using System;
21 using System.Collections.Generic;
22 using System.ComponentModel;
23 using System.Diagnostics;
24 using System.Drawing;
25 using System.Drawing.Drawing2D;
26 using System.Drawing.Imaging;
27 using System.IO;
28 using System.Text;
29 using System.Windows.Forms;
30 
31 using KeePass.App;
32 using KeePass.Native;
33 using KeePass.Resources;
34 using KeePass.UI;
35 using KeePass.Util;
36 
37 using KeePassLib;
38 using KeePassLib.Utility;
39 
40 namespace KeePass.Forms
41 {
42 	public partial class FileBrowserForm : Form
43 	{
44 		private bool m_bSaveMode = false;
45 		private string m_strTitle = PwDefs.ShortProductName;
46 		private string m_strHint = string.Empty;
47 		private string m_strContext = null;
48 
49 		private ImageList m_ilFolders = null;
50 		private List<Image> m_vFolderImages = new List<Image>();
51 		private ImageList m_ilFiles = null;
52 		private List<Image> m_vFileImages = new List<Image>();
53 
54 		private int m_nIconDim = DpiUtil.ScaleIntY(16);
55 
56 		private const string StrDummyNode = "66913D76EA3F4F2A8B1A0899B7322EC3";
57 
58 		private sealed class FbfPrivTviComparer : IComparer<TreeNode>
59 		{
Compare(TreeNode x, TreeNode y)60 			public int Compare(TreeNode x, TreeNode y)
61 			{
62 				Debug.Assert((x != null) && (y != null));
63 				return StrUtil.CompareNaturally(x.Text, y.Text);
64 			}
65 		}
66 
67 		private sealed class FbfPrivLviComparer : IComparer<ListViewItem>
68 		{
Compare(ListViewItem x, ListViewItem y)69 			public int Compare(ListViewItem x, ListViewItem y)
70 			{
71 				Debug.Assert((x != null) && (y != null));
72 				return StrUtil.CompareNaturally(x.Text, y.Text);
73 			}
74 		}
75 
76 		private string m_strSelectedFile = null;
77 		public string SelectedFile
78 		{
79 			get { return m_strSelectedFile; }
80 		}
81 
InitEx(bool bSaveMode, string strTitle, string strHint, string strContext)82 		public void InitEx(bool bSaveMode, string strTitle, string strHint,
83 			string strContext)
84 		{
85 			m_bSaveMode = bSaveMode;
86 			if(strTitle != null) m_strTitle = strTitle;
87 			if(strHint != null) m_strHint = strHint;
88 			m_strContext = strContext;
89 		}
90 
FileBrowserForm()91 		public FileBrowserForm()
92 		{
93 			InitializeComponent();
94 			GlobalWindowManager.InitializeForm(this);
95 		}
96 
OnFormLoad(object sender, EventArgs e)97 		private void OnFormLoad(object sender, EventArgs e)
98 		{
99 			GlobalWindowManager.AddWindow(this);
100 
101 			this.Icon = AppIcons.Default;
102 			this.Text = m_strTitle;
103 
104 			m_nIconDim = m_tvFolders.ItemHeight;
105 
106 			if(UIUtil.VistaStyleListsSupported)
107 			{
108 				UIUtil.SetExplorerTheme(m_tvFolders, true);
109 				UIUtil.SetExplorerTheme(m_lvFiles, true);
110 			}
111 
112 			m_btnOK.Text = (m_bSaveMode ? KPRes.SaveCmd : KPRes.OpenCmd);
113 			Debug.Assert(!m_lblHint.AutoSize); // For RTL support
114 			m_lblHint.Text = m_strHint;
115 
116 			if(UIUtil.ColorsEqual(m_lblHint.ForeColor, Color.Black))
117 				m_lblHint.ForeColor = Color.FromArgb(96, 96, 96);
118 
119 			int nWidth = m_lvFiles.ClientSize.Width - UIUtil.GetVScrollBarWidth();
120 			m_lvFiles.Columns.Add(KPRes.Name, (nWidth * 3) / 4);
121 			m_lvFiles.Columns.Add(KPRes.Size, nWidth / 4, HorizontalAlignment.Right);
122 
123 			InitialPopulateFolders();
124 
125 			string strWorkDir = Program.Config.Application.GetWorkingDirectory(m_strContext);
126 			if(string.IsNullOrEmpty(strWorkDir))
127 				strWorkDir = WinUtil.GetHomeDirectory();
128 			BrowseToFolder(strWorkDir);
129 
130 			EnableControlsEx();
131 		}
132 
OnFormClosed(object sender, FormClosedEventArgs e)133 		private void OnFormClosed(object sender, FormClosedEventArgs e)
134 		{
135 			m_tvFolders.Nodes.Clear();
136 			m_lvFiles.Items.Clear();
137 			m_tvFolders.ImageList = null;
138 			m_lvFiles.SmallImageList = null;
139 
140 			if(m_ilFolders != null) { m_ilFolders.Dispose(); m_ilFolders = null; }
141 			if(m_ilFiles != null) { m_ilFiles.Dispose(); m_ilFiles = null; }
142 
143 			foreach(Image imgFld in m_vFolderImages) imgFld.Dispose();
144 			m_vFolderImages.Clear();
145 			foreach(Image imgFile in m_vFileImages) imgFile.Dispose();
146 			m_vFileImages.Clear();
147 
148 			GlobalWindowManager.RemoveWindow(this);
149 		}
150 
EnableControlsEx()151 		private void EnableControlsEx()
152 		{
153 			m_btnOK.Enabled = (m_lvFiles.SelectedIndices.Count == 1);
154 		}
155 
InitialPopulateFolders()156 		private void InitialPopulateFolders()
157 		{
158 			List<TreeNode> l = new List<TreeNode>();
159 
160 			string str = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
161 			if(!string.IsNullOrEmpty(str))
162 			{
163 				TreeNode tn = CreateFolderNode(str, false, null);
164 				if(tn != null) l.Add(tn);
165 			}
166 
167 			str = Environment.GetEnvironmentVariable("USERPROFILE");
168 			if(!string.IsNullOrEmpty(str))
169 			{
170 				TreeNode tn = CreateFolderNode(str, false, null);
171 				if(tn != null) l.Add(tn);
172 			}
173 
174 			DriveInfo[] vDrives = DriveInfo.GetDrives();
175 			foreach(DriveInfo drv in vDrives)
176 			{
177 				try
178 				{
179 					DirectoryInfo diDrive = drv.RootDirectory;
180 					TreeNode tn = CreateFolderNode(diDrive.FullName, true, drv);
181 					if(tn != null) l.Add(tn);
182 				}
183 				catch(Exception) { Debug.Assert(false); }
184 			}
185 
186 			RebuildFolderImageList();
187 			m_tvFolders.Nodes.AddRange(l.ToArray());
188 		}
189 
GetObjectProps(string strPath, DriveInfo drvHint, out Image img, ref string strDisplayName)190 		private void GetObjectProps(string strPath, DriveInfo drvHint,
191 			out Image img, ref string strDisplayName)
192 		{
193 			GetObjectPropsUnscaled(strPath, drvHint, out img, ref strDisplayName);
194 
195 			if(img != null)
196 			{
197 				if((img.Width != m_nIconDim) || (img.Height != m_nIconDim))
198 				{
199 					Image imgScaled = GfxUtil.ScaleImage(img, m_nIconDim,
200 						m_nIconDim, ScaleTransformFlags.UIIcon);
201 					img.Dispose(); // Dispose unscaled version
202 					img = imgScaled;
203 				}
204 			}
205 		}
206 
GetObjectPropsUnscaled(string strPath, DriveInfo drvHint, out Image img, ref string strDisplayName)207 		private void GetObjectPropsUnscaled(string strPath, DriveInfo drvHint,
208 			out Image img, ref string strDisplayName)
209 		{
210 			img = null;
211 
212 			try
213 			{
214 				string strName;
215 				NativeMethods.SHGetFileInfo(strPath, m_nIconDim, m_nIconDim,
216 					out img, out strName);
217 
218 				if(!string.IsNullOrEmpty(strName) && (strName.IndexOf(
219 					Path.DirectorySeparatorChar) < 0))
220 					strDisplayName = strName;
221 
222 				if(img != null) return;
223 			}
224 			catch(Exception) { Debug.Assert(false); }
225 
226 			ImageList.ImageCollection icons = Program.MainForm.ClientIcons.Images;
227 
228 			if((strPath.Length <= 3) && (drvHint != null))
229 			{
230 				switch(drvHint.DriveType)
231 				{
232 					case DriveType.Fixed:
233 						img = new Bitmap(icons[(int)PwIcon.Drive]);
234 						break;
235 					case DriveType.CDRom:
236 						img = new Bitmap(icons[(int)PwIcon.CDRom]);
237 						break;
238 					case DriveType.Network:
239 						img = new Bitmap(icons[(int)PwIcon.NetworkServer]);
240 						break;
241 					case DriveType.Ram:
242 						img = new Bitmap(icons[(int)PwIcon.Memory]);
243 						break;
244 					case DriveType.Removable:
245 						img = new Bitmap(icons[(int)PwIcon.Disk]);
246 						break;
247 					default:
248 						img = new Bitmap(icons[(int)PwIcon.Folder]);
249 						break;
250 				}
251 
252 				return;
253 			}
254 
255 			img = UIUtil.GetFileIcon(strPath, m_nIconDim, m_nIconDim);
256 			if(img != null) return;
257 
258 			if(Directory.Exists(strPath))
259 				img = new Bitmap(icons[(int)PwIcon.Folder]);
260 			else if(File.Exists(strPath))
261 				img = new Bitmap(icons[(int)PwIcon.PaperNew]);
262 			else
263 			{
264 				Debug.Assert(false);
265 				img = new Bitmap(icons[(int)PwIcon.Star]);
266 			}
267 		}
268 
CreateFolderNode(string strDir, bool bForcePlusMinus, DriveInfo drvHint)269 		private TreeNode CreateFolderNode(string strDir, bool bForcePlusMinus,
270 			DriveInfo drvHint)
271 		{
272 			try
273 			{
274 				DirectoryInfo di = new DirectoryInfo(strDir);
275 
276 				Image img;
277 				string strText = di.Name;
278 				GetObjectProps(di.FullName, drvHint, out img, ref strText);
279 
280 				m_vFolderImages.Add(img);
281 
282 				TreeNode tn = new TreeNode(strText, m_vFolderImages.Count - 1,
283 					m_vFolderImages.Count - 1);
284 				tn.Tag = di.FullName;
285 
286 				InitNodePlusMinus(tn, di, bForcePlusMinus);
287 				return tn;
288 			}
289 			catch(Exception) { Debug.Assert(false); }
290 
291 			return null;
292 		}
293 
InitNodePlusMinus(TreeNode tn, DirectoryInfo di, bool bForce)294 		private static void InitNodePlusMinus(TreeNode tn, DirectoryInfo di,
295 			bool bForce)
296 		{
297 			bool bMark = true;
298 
299 			if(!bForce)
300 			{
301 				try
302 				{
303 					DirectoryInfo[] vDirs = di.GetDirectories();
304 					bool bFoundDir = false;
305 					foreach(DirectoryInfo diSub in vDirs)
306 					{
307 						if(!IsValidFileSystemObject(diSub)) continue;
308 
309 						bFoundDir = true;
310 						break;
311 					}
312 
313 					if(!bFoundDir) bMark = false;
314 				}
315 				catch(Exception) { bMark = false; } // Usually unauthorized
316 			}
317 
318 			if(bMark)
319 			{
320 				tn.Nodes.Add(StrDummyNode);
321 				tn.Collapse();
322 			}
323 		}
324 
RebuildFolderImageList()325 		private void RebuildFolderImageList()
326 		{
327 			ImageList imgNew = UIUtil.BuildImageListUnscaled(
328 				m_vFolderImages, m_nIconDim, m_nIconDim);
329 			m_tvFolders.ImageList = imgNew;
330 
331 			if(m_ilFolders != null) m_ilFolders.Dispose();
332 			m_ilFolders = imgNew;
333 		}
334 
BuildFilesList(DirectoryInfo di)335 		private void BuildFilesList(DirectoryInfo di)
336 		{
337 			m_lvFiles.BeginUpdate();
338 			m_lvFiles.Items.Clear();
339 
340 			DirectoryInfo[] vDirs;
341 			FileInfo[] vFiles;
342 			try
343 			{
344 				vDirs = di.GetDirectories();
345 				vFiles = di.GetFiles();
346 			}
347 			catch(Exception) { m_lvFiles.EndUpdate(); return; } // Unauthorized
348 
349 			foreach(Image imgFile in m_vFileImages) imgFile.Dispose();
350 			m_vFileImages.Clear();
351 
352 			List<ListViewItem> lDirItems = new List<ListViewItem>();
353 			List<ListViewItem> lFileItems = new List<ListViewItem>();
354 
355 			foreach(DirectoryInfo diSub in vDirs)
356 			{
357 				AddFileItem(diSub, m_vFileImages, lDirItems, -1);
358 			}
359 			foreach(FileInfo fi in vFiles)
360 			{
361 				AddFileItem(fi, m_vFileImages, lFileItems, fi.Length);
362 			}
363 
364 			m_lvFiles.SmallImageList = null;
365 			if(m_ilFiles != null) m_ilFiles.Dispose();
366 			m_ilFiles = UIUtil.BuildImageListUnscaled(m_vFileImages, m_nIconDim, m_nIconDim);
367 			m_lvFiles.SmallImageList = m_ilFiles;
368 
369 			lDirItems.Sort(new FbfPrivLviComparer());
370 			m_lvFiles.Items.AddRange(lDirItems.ToArray());
371 			lFileItems.Sort(new FbfPrivLviComparer());
372 			m_lvFiles.Items.AddRange(lFileItems.ToArray());
373 			m_lvFiles.EndUpdate();
374 
375 			EnableControlsEx();
376 		}
377 
IsValidFileSystemObject(FileSystemInfo fsi)378 		private static bool IsValidFileSystemObject(FileSystemInfo fsi)
379 		{
380 			if(fsi == null) { Debug.Assert(false); return false; }
381 
382 			string strName = fsi.Name;
383 			if(string.IsNullOrEmpty(strName) || (strName == ".") ||
384 				(strName == "..")) return false;
385 			if(strName.EndsWith(".lnk", StrUtil.CaseIgnoreCmp)) return false;
386 			if(strName.EndsWith(".url", StrUtil.CaseIgnoreCmp)) return false;
387 
388 			FileAttributes fa = fsi.Attributes;
389 			if((long)(fa & FileAttributes.ReparsePoint) != 0) return false;
390 			if(((long)(fa & FileAttributes.System) != 0) &&
391 				((long)(fa & FileAttributes.Hidden) != 0)) return false;
392 
393 			return true;
394 		}
395 
AddFileItem(FileSystemInfo fsi, List<Image> lImages, List<ListViewItem> lItems, long lFileLength)396 		private void AddFileItem(FileSystemInfo fsi, List<Image> lImages,
397 			List<ListViewItem> lItems, long lFileLength)
398 		{
399 			if(!IsValidFileSystemObject(fsi)) return;
400 
401 			Image img;
402 			string strText = fsi.Name;
403 			GetObjectProps(fsi.FullName, null, out img, ref strText);
404 
405 			lImages.Add(img);
406 
407 			ListViewItem lvi = new ListViewItem(strText, lImages.Count - 1);
408 			lvi.Tag = fsi.FullName;
409 
410 			if(lFileLength < 0) lvi.SubItems.Add(string.Empty);
411 			else lvi.SubItems.Add(StrUtil.FormatDataSizeKB((ulong)lFileLength));
412 
413 			lItems.Add(lvi);
414 		}
415 
PerformFileSelection()416 		private bool PerformFileSelection()
417 		{
418 			ListView.SelectedListViewItemCollection lvsic = m_lvFiles.SelectedItems;
419 			if((lvsic == null) || (lvsic.Count != 1)) { Debug.Assert(false); return false; }
420 
421 			string str = (lvsic[0].Tag as string);
422 			if(string.IsNullOrEmpty(str)) { Debug.Assert(false); return false; }
423 
424 			try
425 			{
426 				if(Directory.Exists(str))
427 				{
428 					TreeNode tn = m_tvFolders.SelectedNode;
429 					if(tn == null) { Debug.Assert(false); return false; }
430 
431 					if(!tn.IsExpanded) tn.Expand();
432 
433 					foreach(TreeNode tnSub in tn.Nodes)
434 					{
435 						string strSub = (tnSub.Tag as string);
436 						if(string.IsNullOrEmpty(strSub)) { Debug.Assert(false); continue; }
437 
438 						if(strSub.Equals(str, StrUtil.CaseIgnoreCmp))
439 						{
440 							m_tvFolders.SelectedNode = tnSub;
441 							tnSub.EnsureVisible();
442 							return false; // Success, but not a file selection!
443 						}
444 					}
445 
446 					Debug.Assert(false);
447 				}
448 				else if(File.Exists(str))
449 				{
450 					m_strSelectedFile = str;
451 
452 					Program.Config.Application.SetWorkingDirectory(m_strContext,
453 						UrlUtil.GetFileDirectory(str, false, true));
454 
455 					return true;
456 				}
457 				else { Debug.Assert(false); }
458 			}
459 			catch(Exception) { Debug.Assert(false); }
460 
461 			return false;
462 		}
463 
OnBtnOK(object sender, EventArgs e)464 		private void OnBtnOK(object sender, EventArgs e)
465 		{
466 			if(!PerformFileSelection()) this.DialogResult = DialogResult.None;
467 		}
468 
OnFilesItemActivate(object sender, EventArgs e)469 		private void OnFilesItemActivate(object sender, EventArgs e)
470 		{
471 			if(PerformFileSelection()) this.DialogResult = DialogResult.OK;
472 		}
473 
OnFoldersBeforeExpand(object sender, TreeViewCancelEventArgs e)474 		private void OnFoldersBeforeExpand(object sender, TreeViewCancelEventArgs e)
475 		{
476 			TreeNode tn = e.Node;
477 			if(tn == null) { Debug.Assert(false); e.Cancel = true; return; }
478 
479 			if((tn.Nodes.Count == 1) && (tn.Nodes[0].Text == StrDummyNode))
480 			{
481 				tn.Nodes.Clear();
482 				List<TreeNode> lNodes = new List<TreeNode>();
483 
484 				try
485 				{
486 					DirectoryInfo di = new DirectoryInfo(tn.Tag as string);
487 					DirectoryInfo[] vSubDirs = di.GetDirectories();
488 					foreach(DirectoryInfo diSub in vSubDirs)
489 					{
490 						if(!IsValidFileSystemObject(diSub)) continue;
491 
492 						TreeNode tnSub = CreateFolderNode(diSub.FullName, false, null);
493 						if(tnSub != null) lNodes.Add(tnSub);
494 					}
495 				}
496 				catch(Exception) { Debug.Assert(false); }
497 
498 				RebuildFolderImageList();
499 				lNodes.Sort(new FbfPrivTviComparer());
500 				tn.Nodes.AddRange(lNodes.ToArray());
501 			}
502 		}
503 
BrowseToFolder(string strPath)504 		private void BrowseToFolder(string strPath)
505 		{
506 			try
507 			{
508 				DirectoryInfo di = new DirectoryInfo(strPath);
509 				string[] vPath = di.FullName.Split(Path.DirectorySeparatorChar);
510 				if((vPath == null) || (vPath.Length == 0)) { Debug.Assert(false); return; }
511 
512 				TreeNode tn = null;
513 				string str = string.Empty;
514 				for(int i = 0; i < vPath.Length; ++i)
515 				{
516 					if(i > 0) str = UrlUtil.EnsureTerminatingSeparator(str, false);
517 					str += vPath[i];
518 					if(i == 0) str = UrlUtil.EnsureTerminatingSeparator(str, false);
519 
520 					TreeNodeCollection tnc = ((tn != null) ? tn.Nodes : m_tvFolders.Nodes);
521 					tn = null;
522 
523 					foreach(TreeNode tnSub in tnc)
524 					{
525 						string strSub = (tnSub.Tag as string);
526 						if(string.IsNullOrEmpty(strSub)) { Debug.Assert(false); continue; }
527 
528 						if(strSub.Equals(str, StrUtil.CaseIgnoreCmp))
529 						{
530 							tn = tnSub;
531 							break;
532 						}
533 					}
534 
535 					if(tn == null) { Debug.Assert(false); break; }
536 
537 					if((i != (vPath.Length - 1)) && !tn.IsExpanded) tn.Expand();
538 				}
539 
540 				if(tn != null)
541 				{
542 					m_tvFolders.SelectedNode = tn;
543 					tn.EnsureVisible();
544 				}
545 				else { Debug.Assert(false); }
546 			}
547 			catch(Exception) { Debug.Assert(false); }
548 		}
549 
OnFoldersAfterSelect(object sender, TreeViewEventArgs e)550 		private void OnFoldersAfterSelect(object sender, TreeViewEventArgs e)
551 		{
552 			TreeNode tn = e.Node;
553 			string strPath = (tn.Tag as string);
554 			if(strPath == null) { Debug.Assert(false); return; }
555 
556 			try
557 			{
558 				DirectoryInfo di = new DirectoryInfo(strPath);
559 				BuildFilesList(di);
560 			}
561 			catch(Exception) { Debug.Assert(false); }
562 		}
563 
OnFilesSelectedIndexChanged(object sender, EventArgs e)564 		private void OnFilesSelectedIndexChanged(object sender, EventArgs e)
565 		{
566 			EnableControlsEx();
567 		}
568 	}
569 }
570