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.Diagnostics;
23 using System.IO;
24 using System.Text;
25 using System.Text.RegularExpressions;
26 using System.Windows.Forms;
27 using System.Xml;
28 using System.Xml.XPath;
29 
30 using KeePass.Forms;
31 using KeePass.Resources;
32 using KeePass.UI;
33 
34 using KeePassLib;
35 using KeePassLib.Collections;
36 using KeePassLib.Delegates;
37 using KeePassLib.Interfaces;
38 using KeePassLib.Security;
39 using KeePassLib.Serialization;
40 using KeePassLib.Utility;
41 
42 namespace KeePass.Util
43 {
44 	[Flags]
45 	public enum XmlReplaceFlags
46 	{
47 		None = 0x0,
48 		StatusUI = 0x1,
49 
50 		CaseSensitive = 0x10,
51 		Regex = 0x20
52 	}
53 
54 	public enum XmlReplaceOp
55 	{
56 		None = 0,
57 		RemoveNodes,
58 		ReplaceData
59 	}
60 
61 	public enum XmlReplaceData
62 	{
63 		None = 0,
64 		InnerText,
65 		InnerXml,
66 		OuterXml
67 	}
68 
69 	public sealed class XmlReplaceOptions
70 	{
71 		private XmlReplaceFlags m_f = XmlReplaceFlags.None;
72 		public XmlReplaceFlags Flags
73 		{
74 			get { return m_f; }
75 			set { m_f = value; }
76 		}
77 
78 		private string m_strSelXPath = string.Empty;
79 		public string SelectNodesXPath
80 		{
81 			get { return m_strSelXPath; }
82 			set
83 			{
84 				if(value == null) { Debug.Assert(false); m_strSelXPath = string.Empty; }
85 				else m_strSelXPath = value;
86 			}
87 		}
88 
89 		private XmlReplaceOp m_op = XmlReplaceOp.None;
90 		public XmlReplaceOp Operation
91 		{
92 			get { return m_op; }
93 			set { m_op = value; }
94 		}
95 
96 		private XmlReplaceData m_d = XmlReplaceData.None;
97 		public XmlReplaceData Data
98 		{
99 			get { return m_d; }
100 			set { m_d = value; }
101 		}
102 
103 		private string m_strFind = string.Empty;
104 		public string FindText
105 		{
106 			get { return m_strFind; }
107 			set
108 			{
109 				if(value == null) { Debug.Assert(false); m_strFind = string.Empty; }
110 				else m_strFind = value;
111 			}
112 		}
113 
114 		private string m_strReplace = string.Empty;
115 		public string ReplaceText
116 		{
117 			get { return m_strReplace; }
118 			set
119 			{
120 				if(value == null) { Debug.Assert(false); m_strReplace = string.Empty; }
121 				else m_strReplace = value;
122 			}
123 		}
124 
125 		private Form m_fParent = null;
126 		public Form ParentForm
127 		{
128 			get { return m_fParent; }
129 			set { m_fParent = value; }
130 		}
131 
XmlReplaceOptions()132 		public XmlReplaceOptions() { }
133 	}
134 
135 	public static partial class XmlUtil
136 	{
Replace(PwDatabase pd, XmlReplaceOptions opt)137 		public static void Replace(PwDatabase pd, XmlReplaceOptions opt)
138 		{
139 			if(pd == null) { Debug.Assert(false); return; }
140 			if(opt == null) { Debug.Assert(false); return; }
141 
142 			StatusProgressForm dlg = null;
143 			try
144 			{
145 				if((opt.Flags & XmlReplaceFlags.StatusUI) != XmlReplaceFlags.None)
146 					dlg = StatusProgressForm.ConstructEx(KPRes.XmlReplace,
147 						true, false, opt.ParentForm, KPRes.XmlReplace + "...");
148 
149 				PerformXmlReplace(pd, opt, dlg);
150 			}
151 			finally
152 			{
153 				if(dlg != null) StatusProgressForm.DestroyEx(dlg);
154 			}
155 		}
156 
PerformXmlReplace(PwDatabase pd, XmlReplaceOptions opt, IStatusLogger sl)157 		private static void PerformXmlReplace(PwDatabase pd, XmlReplaceOptions opt,
158 			IStatusLogger sl)
159 		{
160 			if(opt.SelectNodesXPath.Length == 0) return;
161 			if(opt.Operation == XmlReplaceOp.None) return;
162 
163 			bool bRemove = (opt.Operation == XmlReplaceOp.RemoveNodes);
164 			bool bReplace = (opt.Operation == XmlReplaceOp.ReplaceData);
165 			bool bMatchCase = ((opt.Flags & XmlReplaceFlags.CaseSensitive) != XmlReplaceFlags.None);
166 			bool bRegex = ((opt.Flags & XmlReplaceFlags.Regex) != XmlReplaceFlags.None);
167 
168 			Regex rxFind = null;
169 			if(bReplace && bRegex)
170 				rxFind = new Regex(opt.FindText, (bMatchCase ? RegexOptions.None :
171 					RegexOptions.IgnoreCase));
172 
173 			EnsureStandardFieldsExist(pd);
174 
175 			XmlDocument xd;
176 			XPathNodeIterator xpIt = XmlUtilEx.FindNodes(pd, opt.SelectNodesXPath,
177 				sl, out xd);
178 
179 			// XPathNavigators must be cloned to make them independent
180 			List<XPathNavigator> lNodes = new List<XPathNavigator>();
181 			while(xpIt.MoveNext()) lNodes.Add(xpIt.Current.Clone());
182 
183 			if(lNodes.Count == 0) return;
184 
185 			for(int i = lNodes.Count - 1; i >= 0; --i)
186 			{
187 				if((sl != null) && !sl.ContinueWork()) return;
188 
189 				XPathNavigator xpNav = lNodes[i];
190 
191 				if(bRemove) xpNav.DeleteSelf();
192 				else if(bReplace) ApplyReplace(xpNav, opt, rxFind);
193 				else { Debug.Assert(false); } // Unknown action
194 			}
195 
196 			MemoryStream msMod = new MemoryStream();
197 			using(XmlWriter xw = XmlUtilEx.CreateXmlWriter(msMod))
198 			{
199 				xd.Save(xw);
200 			}
201 			byte[] pbMod = msMod.ToArray();
202 			msMod.Close();
203 
204 			PwDatabase pdMod = new PwDatabase();
205 			msMod = new MemoryStream(pbMod, false);
206 			try
207 			{
208 				KdbxFile kdbxMod = new KdbxFile(pdMod);
209 				kdbxMod.Load(msMod, KdbxFormat.PlainXml, sl);
210 			}
211 			catch(Exception)
212 			{
213 				throw new Exception(KPRes.XmlModInvalid + MessageService.NewParagraph +
214 					KPRes.OpAborted + MessageService.NewParagraph +
215 					KPRes.DbNoModBy.Replace(@"{PARAM}", @"'" + KPRes.XmlReplace + @"'"));
216 			}
217 			finally { msMod.Close(); }
218 
219 			PrepareModDbForMerge(pdMod, pd);
220 
221 			pd.Modified = true;
222 			pd.UINeedsIconUpdate = true;
223 			pd.MergeIn(pdMod, PwMergeMethod.Synchronize, sl);
224 		}
225 
ApplyReplace(XPathNavigator xpNav, XmlReplaceOptions opt, Regex rxFind)226 		private static void ApplyReplace(XPathNavigator xpNav, XmlReplaceOptions opt,
227 			Regex rxFind)
228 		{
229 			string strData;
230 			if(opt.Data == XmlReplaceData.InnerText) strData = xpNav.Value;
231 			else if(opt.Data == XmlReplaceData.InnerXml) strData = xpNav.InnerXml;
232 			else if(opt.Data == XmlReplaceData.OuterXml) strData = xpNav.OuterXml;
233 			else return;
234 			if(strData == null) { Debug.Assert(false); strData = string.Empty; }
235 
236 			string str = null;
237 			if(rxFind != null) str = rxFind.Replace(strData, opt.ReplaceText);
238 			else
239 			{
240 				if((opt.Flags & XmlReplaceFlags.CaseSensitive) != XmlReplaceFlags.None)
241 					str = strData.Replace(opt.FindText, opt.ReplaceText);
242 				else
243 					str = StrUtil.ReplaceCaseInsensitive(strData, opt.FindText,
244 						opt.ReplaceText);
245 			}
246 
247 			if((str != null) && (str != strData))
248 			{
249 				if(opt.Data == XmlReplaceData.InnerText)
250 					xpNav.SetValue(str);
251 				else if(opt.Data == XmlReplaceData.InnerXml)
252 					xpNav.InnerXml = str;
253 				else if(opt.Data == XmlReplaceData.OuterXml)
254 					xpNav.OuterXml = str;
255 				else { Debug.Assert(false); }
256 			}
257 		}
258 
PrepareModDbForMerge(PwDatabase pd, PwDatabase pdOrg)259 		private static void PrepareModDbForMerge(PwDatabase pd, PwDatabase pdOrg)
260 		{
261 			PwGroup pgRootOrg = pdOrg.RootGroup;
262 			PwGroup pgRootNew = pd.RootGroup;
263 			if(pgRootNew == null) { Debug.Assert(false); return; }
264 
265 			PwCompareOptions pwCmp = (PwCompareOptions.IgnoreParentGroup |
266 				PwCompareOptions.NullEmptyEquivStd);
267 			DateTime dtNow = DateTime.UtcNow;
268 
269 			GroupHandler ghOrg = delegate(PwGroup pg)
270 			{
271 				PwGroup pgNew = pgRootNew.FindGroup(pg.Uuid, true);
272 				if(pgNew == null)
273 				{
274 					AddDeletedObject(pd, pg.Uuid);
275 					return true;
276 				}
277 
278 				if(!pgNew.EqualsGroup(pg, (pwCmp | PwCompareOptions.PropertiesOnly),
279 					MemProtCmpMode.Full))
280 					pgNew.Touch(true, false);
281 
282 				PwGroup pgParentA = pg.ParentGroup;
283 				PwGroup pgParentB = pgNew.ParentGroup;
284 				if((pgParentA != null) && (pgParentB != null))
285 				{
286 					if(!pgParentA.Uuid.Equals(pgParentB.Uuid))
287 						pgNew.LocationChanged = dtNow;
288 				}
289 				else if((pgParentA == null) && (pgParentB == null)) { }
290 				else pgNew.LocationChanged = dtNow;
291 
292 				return true;
293 			};
294 
295 			EntryHandler ehOrg = delegate(PwEntry pe)
296 			{
297 				PwEntry peNew = pgRootNew.FindEntry(pe.Uuid, true);
298 				if(peNew == null)
299 				{
300 					AddDeletedObject(pd, pe.Uuid);
301 					return true;
302 				}
303 
304 				if(!peNew.EqualsEntry(pe, pwCmp, MemProtCmpMode.Full))
305 				{
306 					peNew.Touch(true, false);
307 
308 					bool bRestoreHistory = false;
309 					if(peNew.History.UCount != pe.History.UCount)
310 						bRestoreHistory = true;
311 					else
312 					{
313 						for(uint u = 0; u < pe.History.UCount; ++u)
314 						{
315 							if(!peNew.History.GetAt(u).EqualsEntry(
316 								pe.History.GetAt(u), pwCmp, MemProtCmpMode.CustomOnly))
317 							{
318 								bRestoreHistory = true;
319 								break;
320 							}
321 						}
322 					}
323 
324 					if(bRestoreHistory)
325 					{
326 						peNew.History = pe.History.CloneDeep();
327 						foreach(PwEntry peHistNew in peNew.History)
328 							peHistNew.ParentGroup = peNew.ParentGroup;
329 					}
330 				}
331 
332 				PwGroup pgParentA = pe.ParentGroup;
333 				PwGroup pgParentB = peNew.ParentGroup;
334 				if((pgParentA != null) && (pgParentB != null))
335 				{
336 					if(!pgParentA.Uuid.Equals(pgParentB.Uuid))
337 						peNew.LocationChanged = dtNow;
338 				}
339 				else if((pgParentA == null) && (pgParentB == null)) { }
340 				else peNew.LocationChanged = dtNow;
341 
342 				return true;
343 			};
344 
345 			pgRootOrg.TraverseTree(TraversalMethod.PreOrder, ghOrg, ehOrg);
346 		}
347 
AddDeletedObject(PwDatabase pd, PwUuid pu)348 		private static void AddDeletedObject(PwDatabase pd, PwUuid pu)
349 		{
350 			foreach(PwDeletedObject pdo in pd.DeletedObjects)
351 			{
352 				if(pdo.Uuid.Equals(pu)) { Debug.Assert(false); return; }
353 			}
354 
355 			PwDeletedObject pdoNew = new PwDeletedObject(pu, DateTime.UtcNow);
356 			pd.DeletedObjects.Add(pdoNew);
357 		}
358 
EnsureStandardFieldsExist(PwDatabase pd)359 		private static void EnsureStandardFieldsExist(PwDatabase pd)
360 		{
361 			List<string> l = PwDefs.GetStandardFields();
362 
363 			EntryHandler eh = delegate(PwEntry pe)
364 			{
365 				foreach(string strName in l)
366 				{
367 					ProtectedString ps = pe.Strings.Get(strName);
368 					if(ps == null)
369 						pe.Strings.Set(strName, new ProtectedString(
370 							pd.MemoryProtection.GetProtection(strName), string.Empty));
371 				}
372 
373 				return true;
374 			};
375 
376 			pd.RootGroup.TraverseTree(TraversalMethod.PreOrder, null, eh);
377 		}
378 	}
379 }
380