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;
22 using System.Collections.Generic;
23 using System.Diagnostics;
24 using System.Globalization;
25 using System.IO;
26 using System.Text;
27 using System.Text.RegularExpressions;
28 
29 using KeePass.App.Configuration;
30 using KeePass.Forms;
31 
32 using KeePassLib;
33 using KeePassLib.Collections;
34 using KeePassLib.Native;
35 using KeePassLib.Security;
36 using KeePassLib.Utility;
37 
38 namespace KeePass.Util.Spr
39 {
40 	/// <summary>
41 	/// String placeholders and field reference replacement engine.
42 	/// </summary>
43 	public static partial class SprEngine
44 	{
45 		private const uint MaxRecursionDepth = 12;
46 		private const StringComparison ScMethod = StringComparison.OrdinalIgnoreCase;
47 
48 		// private static readonly char[] m_vPlhEscapes = new char[] { '{', '}', '%' };
49 
50 		// Important notes for plugin developers subscribing to the following events:
51 		// * If possible, prefer subscribing to FilterCompile instead of
52 		//   FilterCompilePre.
53 		// * If your plugin provides an active transformation (e.g. replacing a
54 		//   placeholder that changes some state or requires UI interaction), you
55 		//   must only perform the transformation if the ExtActive bit is set in
56 		//   args.Context.Flags of the event arguments object args provided to the
57 		//   event handler.
58 		// * Non-active transformations should only be performed if the ExtNonActive
59 		//   bit is set in args.Context.Flags.
60 		// * If your plugin provides a placeholder (like e.g. {EXAMPLE}), you
61 		//   should add this placeholder to the FilterPlaceholderHints list
62 		//   (e.g. add the string "{EXAMPLE}"). Please remove your strings from
63 		//   the list when your plugin is terminated.
64 		public static event EventHandler<SprEventArgs> FilterCompilePre;
65 		public static event EventHandler<SprEventArgs> FilterCompile;
66 
67 		private static List<string> m_lFilterPlh = new List<string>();
68 		// See the events above
69 		public static List<string> FilterPlaceholderHints
70 		{
71 			get { return m_lFilterPlh; }
72 		}
73 
74 		[Obsolete]
Compile(string strText, bool bIsAutoTypeSequence, PwEntry pwEntry, PwDatabase pwDatabase, bool bEscapeForAutoType, bool bEscapeQuotesForCommandLine)75 		public static string Compile(string strText, bool bIsAutoTypeSequence,
76 			PwEntry pwEntry, PwDatabase pwDatabase, bool bEscapeForAutoType,
77 			bool bEscapeQuotesForCommandLine)
78 		{
79 			SprContext ctx = new SprContext(pwEntry, pwDatabase, SprCompileFlags.All,
80 				bEscapeForAutoType, bEscapeQuotesForCommandLine);
81 			return Compile(strText, ctx);
82 		}
83 
Compile(string strText, SprContext ctx)84 		public static string Compile(string strText, SprContext ctx)
85 		{
86 			if(strText == null) { Debug.Assert(false); return string.Empty; }
87 			if(strText.Length == 0) return string.Empty;
88 
89 			if(ctx == null) ctx = new SprContext();
90 			ctx.RefCache.Clear();
91 
92 			string str = SprEngine.CompileInternal(strText, ctx, 0);
93 
94 			// if(bEscapeForAutoType && !bIsAutoTypeSequence)
95 			//	str = SprEncoding.MakeAutoTypeSequence(str);
96 
97 			return str;
98 		}
99 
CompileInternal(string strText, SprContext ctx, uint uRecursionLevel)100 		private static string CompileInternal(string strText, SprContext ctx,
101 			uint uRecursionLevel)
102 		{
103 			if(strText == null) { Debug.Assert(false); return string.Empty; }
104 			if(ctx == null) { Debug.Assert(false); ctx = new SprContext(); }
105 
106 			if(uRecursionLevel >= SprEngine.MaxRecursionDepth)
107 			{
108 				Debug.Assert(false); // Most likely a recursive reference
109 				return string.Empty; // Do not return strText (endless loop)
110 			}
111 
112 			string str = strText;
113 			MainForm mf = Program.MainForm;
114 
115 			bool bExt = ((ctx.Flags & (SprCompileFlags.ExtActive |
116 				SprCompileFlags.ExtNonActive)) != SprCompileFlags.None);
117 			if(bExt && (SprEngine.FilterCompilePre != null))
118 			{
119 				SprEventArgs args = new SprEventArgs(str, ctx.Clone());
120 				SprEngine.FilterCompilePre(null, args);
121 				str = args.Text;
122 			}
123 
124 			if((ctx.Flags & SprCompileFlags.Comments) != SprCompileFlags.None)
125 				str = RemoveComments(str);
126 
127 			// The following realizes {T-CONV:/Text/Raw/}, which should be
128 			// one of the first transformations (except comments)
129 			if((ctx.Flags & SprCompileFlags.TextTransforms) != SprCompileFlags.None)
130 				str = PerformTextTransforms(str, ctx, uRecursionLevel);
131 
132 			if((ctx.Flags & SprCompileFlags.Run) != SprCompileFlags.None)
133 				str = RunCommands(str, ctx, uRecursionLevel);
134 
135 			if((ctx.Flags & SprCompileFlags.DataActive) != SprCompileFlags.None)
136 				str = PerformClipboardCopy(str, ctx, uRecursionLevel);
137 
138 			if(((ctx.Flags & SprCompileFlags.DataNonActive) != SprCompileFlags.None) &&
139 				(str.IndexOf(@"{CLIPBOARD}", SprEngine.ScMethod) >= 0))
140 			{
141 				string strCb = null;
142 				try { strCb = ClipboardUtil.GetText(); }
143 				catch(Exception) { Debug.Assert(false); }
144 				str = Fill(str, @"{CLIPBOARD}", strCb ?? string.Empty, ctx, null);
145 			}
146 
147 			if((ctx.Flags & SprCompileFlags.AppPaths) != SprCompileFlags.None)
148 				str = AppLocator.FillPlaceholders(str, ctx);
149 
150 			if(ctx.Entry != null)
151 			{
152 				if((ctx.Flags & SprCompileFlags.PickChars) != SprCompileFlags.None)
153 					str = ReplacePickPw(str, ctx, uRecursionLevel);
154 
155 				if((ctx.Flags & SprCompileFlags.EntryStrings) != SprCompileFlags.None)
156 					str = FillEntryStrings(str, ctx, uRecursionLevel);
157 
158 				if((ctx.Flags & SprCompileFlags.EntryStringsSpecial) != SprCompileFlags.None)
159 					str = FillEntryStringsSpecial(str, ctx, uRecursionLevel);
160 
161 				if(((ctx.Flags & SprCompileFlags.EntryProperties) != SprCompileFlags.None) &&
162 					(str.IndexOf(@"{UUID}", SprEngine.ScMethod) >= 0))
163 					str = Fill(str, @"{UUID}", ctx.Entry.Uuid.ToHexString(), ctx, null);
164 
165 				if(((ctx.Flags & SprCompileFlags.PasswordEnc) != SprCompileFlags.None) &&
166 					(str.IndexOf(@"{PASSWORD_ENC}", SprEngine.ScMethod) >= 0))
167 				{
168 					string strPwCmp = SprEngine.CompileInternal(@"{PASSWORD}",
169 						ctx.WithoutContentTransformations(), uRecursionLevel + 1);
170 					str = Fill(str, @"{PASSWORD_ENC}", StrUtil.EncryptString(
171 						strPwCmp), ctx, null);
172 				}
173 
174 				PwGroup pg = ctx.Entry.ParentGroup;
175 				if(((ctx.Flags & SprCompileFlags.Group) != SprCompileFlags.None) &&
176 					(pg != null))
177 					str = FillGroupPlh(str, @"{GROUP", pg, ctx, uRecursionLevel);
178 			}
179 
180 			if((ctx.Flags & SprCompileFlags.Paths) != SprCompileFlags.None)
181 			{
182 				if(mf != null)
183 				{
184 					PwGroup pgSel = mf.GetSelectedGroup();
185 					if(pgSel != null)
186 						str = FillGroupPlh(str, @"{GROUP_SEL", pgSel, ctx, uRecursionLevel);
187 				}
188 
189 				str = Fill(str, @"{APPDIR}", UrlUtil.GetFileDirectory(
190 					WinUtil.GetExecutable(), false, false), ctx, uRecursionLevel);
191 
192 				str = Fill(str, @"{ENV_DIRSEP}", Path.DirectorySeparatorChar.ToString(),
193 					ctx, null);
194 
195 				string strPF86 = Environment.GetEnvironmentVariable("ProgramFiles(x86)");
196 				if(string.IsNullOrEmpty(strPF86))
197 					strPF86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
198 				if(strPF86 != null)
199 					str = Fill(str, @"{ENV_PROGRAMFILES_X86}", strPF86, ctx, uRecursionLevel);
200 				else { Debug.Assert(false); }
201 
202 				if(ctx.Database != null)
203 				{
204 					string strPath = ctx.Database.IOConnectionInfo.Path;
205 					string strDir = UrlUtil.GetFileDirectory(strPath, false, false);
206 					string strName = UrlUtil.GetFileName(strPath);
207 
208 					// For backward compatibility only
209 					str = Fill(str, @"{DOCDIR}", strDir, ctx, uRecursionLevel);
210 
211 					str = Fill(str, @"{DB_PATH}", strPath, ctx, uRecursionLevel);
212 					str = Fill(str, @"{DB_DIR}", strDir, ctx, uRecursionLevel);
213 					str = Fill(str, @"{DB_NAME}", strName, ctx, uRecursionLevel);
214 					str = Fill(str, @"{DB_BASENAME}", UrlUtil.StripExtension(
215 						strName), ctx, uRecursionLevel);
216 					str = Fill(str, @"{DB_EXT}", UrlUtil.GetExtension(
217 						strPath), ctx, uRecursionLevel);
218 				}
219 			}
220 
221 			if((ctx.Flags & SprCompileFlags.AutoType) != SprCompileFlags.None)
222 			{
223 				// Use Bksp instead of Del (in order to avoid Ctrl+Alt+Del);
224 				// https://sourceforge.net/p/keepass/discussion/329220/thread/4f1aa6b8/
225 				str = StrUtil.ReplaceCaseInsensitive(str, @"{CLEARFIELD}",
226 					@"{HOME}+({END}){BKSP}{DELAY 50}");
227 			}
228 
229 			if(((ctx.Flags & SprCompileFlags.DateTime) != SprCompileFlags.None) &&
230 				(str.IndexOf(@"{DT_", SprEngine.ScMethod) >= 0))
231 			{
232 				DateTime dtNow = DateTime.UtcNow;
233 				str = Fill(str, @"{DT_UTC_YEAR}", dtNow.Year.ToString("D4"),
234 					ctx, null);
235 				str = Fill(str, @"{DT_UTC_MONTH}", dtNow.Month.ToString("D2"),
236 					ctx, null);
237 				str = Fill(str, @"{DT_UTC_DAY}", dtNow.Day.ToString("D2"),
238 					ctx, null);
239 				str = Fill(str, @"{DT_UTC_HOUR}", dtNow.Hour.ToString("D2"),
240 					ctx, null);
241 				str = Fill(str, @"{DT_UTC_MINUTE}", dtNow.Minute.ToString("D2"),
242 					ctx, null);
243 				str = Fill(str, @"{DT_UTC_SECOND}", dtNow.Second.ToString("D2"),
244 					ctx, null);
245 				str = Fill(str, @"{DT_UTC_SIMPLE}", dtNow.ToString("yyyyMMddHHmmss"),
246 					ctx, null);
247 
248 				dtNow = dtNow.ToLocalTime();
249 				str = Fill(str, @"{DT_YEAR}", dtNow.Year.ToString("D4"),
250 					ctx, null);
251 				str = Fill(str, @"{DT_MONTH}", dtNow.Month.ToString("D2"),
252 					ctx, null);
253 				str = Fill(str, @"{DT_DAY}", dtNow.Day.ToString("D2"),
254 					ctx, null);
255 				str = Fill(str, @"{DT_HOUR}", dtNow.Hour.ToString("D2"),
256 					ctx, null);
257 				str = Fill(str, @"{DT_MINUTE}", dtNow.Minute.ToString("D2"),
258 					ctx, null);
259 				str = Fill(str, @"{DT_SECOND}", dtNow.Second.ToString("D2"),
260 					ctx, null);
261 				str = Fill(str, @"{DT_SIMPLE}", dtNow.ToString("yyyyMMddHHmmss"),
262 					ctx, null);
263 			}
264 
265 			if((ctx.Flags & SprCompileFlags.References) != SprCompileFlags.None)
266 				str = SprEngine.FillRefPlaceholders(str, ctx, uRecursionLevel);
267 
268 			if(((ctx.Flags & SprCompileFlags.EnvVars) != SprCompileFlags.None) &&
269 				(str.IndexOf('%') >= 0))
270 			{
271 				foreach(DictionaryEntry de in Environment.GetEnvironmentVariables())
272 				{
273 					string strKey = (de.Key as string);
274 					if(string.IsNullOrEmpty(strKey)) { Debug.Assert(false); continue; }
275 
276 					string strValue = (de.Value as string);
277 					if(strValue == null) { Debug.Assert(false); strValue = string.Empty; }
278 
279 					str = Fill(str, @"%" + strKey + @"%", strValue, ctx, uRecursionLevel);
280 				}
281 			}
282 
283 			if((ctx.Flags & SprCompileFlags.Env) != SprCompileFlags.None)
284 				str = FillUriSpecial(str, ctx, @"{BASE", (ctx.Base ?? string.Empty),
285 					ctx.BaseIsEncoded, uRecursionLevel);
286 
287 			str = EntryUtil.FillPlaceholders(str, ctx, uRecursionLevel);
288 
289 			if((ctx.Flags & SprCompileFlags.PickChars) != SprCompileFlags.None)
290 				str = ReplacePickChars(str, ctx, uRecursionLevel);
291 
292 			if(bExt && (SprEngine.FilterCompile != null))
293 			{
294 				SprEventArgs args = new SprEventArgs(str, ctx.Clone());
295 				SprEngine.FilterCompile(null, args);
296 				str = args.Text;
297 			}
298 
299 			if(ctx.EncodeAsAutoTypeSequence)
300 			{
301 				str = StrUtil.NormalizeNewLines(str, false);
302 				str = str.Replace("\n", @"{ENTER}");
303 			}
304 
305 			return str;
306 		}
307 
Fill(string strData, string strPlaceholder, string strReplacement, SprContext ctx, uint? ouRecursionLevel)308 		private static string Fill(string strData, string strPlaceholder,
309 			string strReplacement, SprContext ctx, uint? ouRecursionLevel)
310 		{
311 			if(strData == null) { Debug.Assert(false); return string.Empty; }
312 			if(string.IsNullOrEmpty(strPlaceholder)) { Debug.Assert(false); return strData; }
313 			if(strReplacement == null) { Debug.Assert(false); strReplacement = string.Empty; }
314 
315 			if(strData.IndexOf(strPlaceholder, SprEngine.ScMethod) < 0) return strData;
316 
317 			string strValue = strReplacement;
318 			if(ouRecursionLevel.HasValue)
319 				strValue = SprEngine.CompileInternal(strValue, ((ctx != null) ?
320 					ctx.WithoutContentTransformations() : null),
321 					ouRecursionLevel.Value + 1);
322 
323 			return StrUtil.ReplaceCaseInsensitive(strData, strPlaceholder,
324 				SprEngine.TransformContent(strValue, ctx));
325 		}
326 
Fill(string strData, string strPlaceholder, ProtectedString psReplacement, SprContext ctx, uint? ouRecursionLevel)327 		private static string Fill(string strData, string strPlaceholder,
328 			ProtectedString psReplacement, SprContext ctx, uint? ouRecursionLevel)
329 		{
330 			if(strData == null) { Debug.Assert(false); return string.Empty; }
331 			if(string.IsNullOrEmpty(strPlaceholder)) { Debug.Assert(false); return strData; }
332 			if(psReplacement == null) { Debug.Assert(false); psReplacement = ProtectedString.Empty; }
333 
334 			if(strData.IndexOf(strPlaceholder, SprEngine.ScMethod) < 0) return strData;
335 
336 			return Fill(strData, strPlaceholder, psReplacement.ReadString(),
337 				ctx, ouRecursionLevel);
338 		}
339 
TransformContent(string strContent, SprContext ctx)340 		public static string TransformContent(string strContent, SprContext ctx)
341 		{
342 			if(strContent == null) { Debug.Assert(false); return string.Empty; }
343 
344 			string str = strContent;
345 
346 			if(ctx != null)
347 			{
348 				if(ctx.EncodeForCommandLine)
349 					str = SprEncoding.EncodeForCommandLine(str);
350 
351 				if(ctx.EncodeAsAutoTypeSequence)
352 					str = SprEncoding.EncodeAsAutoTypeSequence(str);
353 			}
354 
355 			return str;
356 		}
357 
UntransformContent(string strContent, SprContext ctx)358 		private static string UntransformContent(string strContent, SprContext ctx)
359 		{
360 			if(strContent == null) { Debug.Assert(false); return string.Empty; }
361 
362 			string str = strContent;
363 
364 			if(ctx != null)
365 			{
366 				if(ctx.EncodeAsAutoTypeSequence) { Debug.Assert(false); }
367 
368 				if(ctx.EncodeForCommandLine)
369 					str = SprEncoding.DecodeCommandLine(str);
370 			}
371 
372 			return str;
373 		}
374 
FillEntryStrings(string str, SprContext ctx, uint uRecursionLevel)375 		private static string FillEntryStrings(string str, SprContext ctx,
376 			uint uRecursionLevel)
377 		{
378 			List<string> vKeys = ctx.Entry.Strings.GetKeys();
379 
380 			// Ensure that all standard field names are in the list
381 			// (this is required in order to replace the standard placeholders
382 			// even if the corresponding standard field isn't present in
383 			// the entry)
384 			List<string> vStdNames = PwDefs.GetStandardFields();
385 			foreach(string strStdField in vStdNames)
386 			{
387 				if(!vKeys.Contains(strStdField)) vKeys.Add(strStdField);
388 			}
389 
390 			// Do not directly enumerate the strings in ctx.Entry.Strings,
391 			// because strings might change during the Spr compilation
392 			foreach(string strField in vKeys)
393 			{
394 				string strKey = (PwDefs.IsStandardField(strField) ?
395 					(@"{" + strField + @"}") :
396 					(@"{" + PwDefs.AutoTypeStringPrefix + strField + @"}"));
397 
398 				if(!ctx.ForcePlainTextPasswords && strKey.Equals(@"{" +
399 					PwDefs.PasswordField + @"}", StrUtil.CaseIgnoreCmp) &&
400 					Program.Config.MainWindow.IsColumnHidden(AceColumnType.Password))
401 				{
402 					str = Fill(str, strKey, PwDefs.HiddenPassword, ctx, null);
403 					continue;
404 				}
405 
406 				// Use GetSafe because the field doesn't necessarily exist
407 				// (might be a standard field that has been added above)
408 				str = Fill(str, strKey, ctx.Entry.Strings.GetSafe(strField),
409 					ctx, uRecursionLevel);
410 			}
411 
412 			return str;
413 		}
414 
FillEntryStringsSpecial(string str, SprContext ctx, uint uRecursionLevel)415 		private static string FillEntryStringsSpecial(string str, SprContext ctx,
416 			uint uRecursionLevel)
417 		{
418 			return FillUriSpecial(str, ctx, @"{URL", ctx.Entry.Strings.ReadSafe(
419 				PwDefs.UrlField), false, uRecursionLevel);
420 		}
421 
FillUriSpecial(string strText, SprContext ctx, string strPlhInit, string strData, bool bDataIsEncoded, uint uRecursionLevel)422 		private static string FillUriSpecial(string strText, SprContext ctx,
423 			string strPlhInit, string strData, bool bDataIsEncoded,
424 			uint uRecursionLevel)
425 		{
426 			Debug.Assert(strPlhInit.StartsWith(@"{") && !strPlhInit.EndsWith(@"}"));
427 			Debug.Assert(strData != null);
428 
429 			string[] vPlhs = new string[] {
430 				strPlhInit + @"}",
431 				strPlhInit + @":RMVSCM}",
432 				strPlhInit + @":SCM}",
433 				strPlhInit + @":HOST}",
434 				strPlhInit + @":PORT}",
435 				strPlhInit + @":PATH}",
436 				strPlhInit + @":QUERY}",
437 				strPlhInit + @":USERINFO}",
438 				strPlhInit + @":USERNAME}",
439 				strPlhInit + @":PASSWORD}"
440 			};
441 
442 			string str = strText;
443 			string strDataCmp = null;
444 			Uri uri = null;
445 			for(int i = 0; i < vPlhs.Length; ++i)
446 			{
447 				string strPlh = vPlhs[i];
448 				if(str.IndexOf(strPlh, SprEngine.ScMethod) < 0) continue;
449 
450 				if(strDataCmp == null)
451 				{
452 					SprContext ctxData = (bDataIsEncoded ?
453 						ctx.WithoutContentTransformations() : ctx);
454 					strDataCmp = SprEngine.CompileInternal(strData, ctxData,
455 						uRecursionLevel + 1);
456 				}
457 
458 				string strRep = null;
459 				if(i == 0) strRep = strDataCmp;
460 				// UrlUtil supports prefixes like cmd://
461 				else if(i == 1) strRep = UrlUtil.RemoveScheme(strDataCmp);
462 				else if(i == 2) strRep = UrlUtil.GetScheme(strDataCmp);
463 				else
464 				{
465 					try
466 					{
467 						if(uri == null) uri = new Uri(strDataCmp);
468 
469 						int t;
470 						switch(i)
471 						{
472 							// case 2: strRep = uri.Scheme; break; // No cmd:// support
473 							case 3: strRep = uri.Host; break;
474 							case 4:
475 								strRep = uri.Port.ToString(
476 									NumberFormatInfo.InvariantInfo);
477 								break;
478 							case 5: strRep = uri.AbsolutePath; break;
479 							case 6: strRep = uri.Query; break;
480 							case 7: strRep = uri.UserInfo; break;
481 							case 8:
482 								strRep = uri.UserInfo;
483 								t = strRep.IndexOf(':');
484 								if(t >= 0) strRep = strRep.Substring(0, t);
485 								break;
486 							case 9:
487 								strRep = uri.UserInfo;
488 								t = strRep.IndexOf(':');
489 								if(t < 0) strRep = string.Empty;
490 								else strRep = strRep.Substring(t + 1);
491 								break;
492 							default: Debug.Assert(false); break;
493 						}
494 					}
495 					catch(Exception) { } // Invalid URI
496 				}
497 				if(strRep == null) strRep = string.Empty; // No assert
498 
499 				str = StrUtil.ReplaceCaseInsensitive(str, strPlh, strRep);
500 			}
501 
502 			return str;
503 		}
504 
505 		private const string StrRemStart = @"{C:";
506 		private const string StrRemEnd = @"}";
RemoveComments(string strSeq)507 		private static string RemoveComments(string strSeq)
508 		{
509 			string str = strSeq;
510 
511 			while(true)
512 			{
513 				int iStart = str.IndexOf(StrRemStart, SprEngine.ScMethod);
514 				if(iStart < 0) break;
515 				int iEnd = str.IndexOf(StrRemEnd, iStart + 1, SprEngine.ScMethod);
516 				if(iEnd <= iStart) break;
517 
518 				str = (str.Substring(0, iStart) + str.Substring(iEnd + StrRemEnd.Length));
519 			}
520 
521 			return str;
522 		}
523 
524 		internal const string StrRefStart = @"{REF:";
525 		internal const string StrRefEnd = @"}";
FillRefPlaceholders(string strSeq, SprContext ctx, uint uRecursionLevel)526 		private static string FillRefPlaceholders(string strSeq, SprContext ctx,
527 			uint uRecursionLevel)
528 		{
529 			if(ctx.Database == null) return strSeq;
530 
531 			string str = strSeq;
532 
533 			int nOffset = 0;
534 			for(int iLoop = 0; iLoop < 20; ++iLoop)
535 			{
536 				str = ctx.RefCache.Fill(str, ctx);
537 
538 				int nStart = str.IndexOf(StrRefStart, nOffset, SprEngine.ScMethod);
539 				if(nStart < 0) break;
540 				int nEnd = str.IndexOf(StrRefEnd, nStart + 1, SprEngine.ScMethod);
541 				if(nEnd <= nStart) break;
542 
543 				string strFullRef = str.Substring(nStart, nEnd - nStart + 1);
544 				char chScan, chWanted;
545 				PwEntry peFound = FindRefTarget(strFullRef, ctx, out chScan, out chWanted);
546 
547 				if(peFound != null)
548 				{
549 					string strInsData;
550 					if(chWanted == 'T')
551 						strInsData = peFound.Strings.ReadSafe(PwDefs.TitleField);
552 					else if(chWanted == 'U')
553 						strInsData = peFound.Strings.ReadSafe(PwDefs.UserNameField);
554 					else if(chWanted == 'A')
555 						strInsData = peFound.Strings.ReadSafe(PwDefs.UrlField);
556 					else if(chWanted == 'P')
557 						strInsData = peFound.Strings.ReadSafe(PwDefs.PasswordField);
558 					else if(chWanted == 'N')
559 						strInsData = peFound.Strings.ReadSafe(PwDefs.NotesField);
560 					else if(chWanted == 'I')
561 						strInsData = peFound.Uuid.ToHexString();
562 					else { nOffset = nStart + 1; continue; }
563 
564 					if((chWanted == 'P') && !ctx.ForcePlainTextPasswords &&
565 						Program.Config.MainWindow.IsColumnHidden(AceColumnType.Password))
566 						strInsData = PwDefs.HiddenPassword;
567 
568 					SprContext sprSub = ctx.WithoutContentTransformations();
569 					sprSub.Entry = peFound;
570 
571 					string strInnerContent = SprEngine.CompileInternal(strInsData,
572 						sprSub, uRecursionLevel + 1);
573 					strInnerContent = SprEngine.TransformContent(strInnerContent, ctx);
574 
575 					// str = str.Substring(0, nStart) + strInnerContent + str.Substring(nEnd + 1);
576 					ctx.RefCache.Add(strFullRef, strInnerContent, ctx);
577 					str = ctx.RefCache.Fill(str, ctx);
578 				}
579 				else { nOffset = nStart + 1; continue; }
580 			}
581 
582 			return str;
583 		}
584 
FindRefTarget(string strFullRef, SprContext ctx, out char chScan, out char chWanted)585 		public static PwEntry FindRefTarget(string strFullRef, SprContext ctx,
586 			out char chScan, out char chWanted)
587 		{
588 			chScan = char.MinValue;
589 			chWanted = char.MinValue;
590 
591 			if(strFullRef == null) { Debug.Assert(false); return null; }
592 			if(!strFullRef.StartsWith(StrRefStart, SprEngine.ScMethod) ||
593 				!strFullRef.EndsWith(StrRefEnd, SprEngine.ScMethod))
594 				return null;
595 			if((ctx == null) || (ctx.Database == null)) { Debug.Assert(false); return null; }
596 
597 			string strRef = strFullRef.Substring(StrRefStart.Length,
598 				strFullRef.Length - StrRefStart.Length - StrRefEnd.Length);
599 			if(strRef.Length <= 4) return null;
600 			if(strRef[1] != '@') return null;
601 			if(strRef[3] != ':') return null;
602 
603 			chScan = char.ToUpper(strRef[2]);
604 			chWanted = char.ToUpper(strRef[0]);
605 
606 			SearchParameters sp = SearchParameters.None;
607 			sp.SearchString = strRef.Substring(4);
608 			sp.RespectEntrySearchingDisabled = false;
609 
610 			if(chScan == 'T') sp.SearchInTitles = true;
611 			else if(chScan == 'U') sp.SearchInUserNames = true;
612 			else if(chScan == 'A') sp.SearchInUrls = true;
613 			else if(chScan == 'P') sp.SearchInPasswords = true;
614 			else if(chScan == 'N') sp.SearchInNotes = true;
615 			else if(chScan == 'I') sp.SearchInUuids = true;
616 			else if(chScan == 'O') sp.SearchInOther = true;
617 			else return null;
618 
619 			PwObjectList<PwEntry> lFound = new PwObjectList<PwEntry>();
620 			ctx.Database.RootGroup.SearchEntries(sp, lFound);
621 
622 			return ((lFound.UCount > 0) ? lFound.GetAt(0) : null);
623 		}
624 
625 		// internal static bool MightChange(string strText)
626 		// {
627 		//	if(string.IsNullOrEmpty(strText)) return false;
628 		//	return (strText.IndexOfAny(m_vPlhEscapes) >= 0);
629 		// }
630 
631 		/* internal static bool MightChange(string str)
632 		{
633 			if(str == null) { Debug.Assert(false); return false; }
634 
635 			int iBStart = str.IndexOf('{');
636 			if(iBStart >= 0)
637 			{
638 				int iBEnd = str.LastIndexOf('}');
639 				if(iBStart < iBEnd) return true;
640 			}
641 
642 			int iPFirst = str.IndexOf('%');
643 			if(iPFirst >= 0)
644 			{
645 				int iPLast = str.LastIndexOf('%');
646 				if(iPFirst < iPLast) return true;
647 			}
648 
649 			return false;
650 		} */
651 
MightChange(char[] v)652 		internal static bool MightChange(char[] v)
653 		{
654 			if(v == null) { Debug.Assert(false); return false; }
655 
656 			int iBStart = Array.IndexOf<char>(v, '{');
657 			if(iBStart >= 0)
658 			{
659 				int iBEnd = Array.LastIndexOf<char>(v, '}');
660 				if(iBStart < iBEnd) return true;
661 			}
662 
663 			int iPFirst = Array.IndexOf<char>(v, '%');
664 			if(iPFirst >= 0)
665 			{
666 				int iPLast = Array.LastIndexOf<char>(v, '%');
667 				if(iPFirst < iPLast) return true;
668 			}
669 
670 			return false;
671 		}
672 
673 		/// <summary>
674 		/// Fast probabilistic test whether a string might be
675 		/// changed when compiling with <c>SprCompileFlags.Deref</c>.
676 		/// </summary>
MightDeref(string strText)677 		internal static bool MightDeref(string strText)
678 		{
679 			if(strText == null) return false;
680 			return (strText.IndexOf('{') >= 0);
681 		}
682 
DerefFn(string str, PwEntry pe)683 		internal static string DerefFn(string str, PwEntry pe)
684 		{
685 			if(!MightDeref(str)) return str;
686 
687 			SprContext ctx = new SprContext(pe,
688 				Program.MainForm.DocumentManager.SafeFindContainerOf(pe),
689 				SprCompileFlags.Deref);
690 			// ctx.ForcePlainTextPasswords = false;
691 
692 			return Compile(str, ctx);
693 		}
694 
695 		/// <summary>
696 		/// Parse and remove a placeholder of the form
697 		/// <c>{PLH:/Param1/Param2/.../}</c>.
698 		/// </summary>
ParseAndRemovePlhWithParams(ref string str, SprContext ctx, uint uRecursionLevel, string strPlhStart, out int iStart, out List<string> lParams, bool bSprCmpParams)699 		internal static bool ParseAndRemovePlhWithParams(ref string str,
700 			SprContext ctx, uint uRecursionLevel, string strPlhStart,
701 			out int iStart, out List<string> lParams, bool bSprCmpParams)
702 		{
703 			Debug.Assert(strPlhStart.StartsWith(@"{") && !strPlhStart.EndsWith(@"}"));
704 
705 			iStart = str.IndexOf(strPlhStart, SprEngine.ScMethod);
706 			if(iStart < 0) { lParams = null; return false; }
707 
708 			lParams = new List<string>();
709 
710 			try
711 			{
712 				int p = iStart + strPlhStart.Length;
713 				if(p >= str.Length) throw new FormatException();
714 
715 				char chSep = str[p];
716 
717 				while(true)
718 				{
719 					if((p + 1) >= str.Length) throw new FormatException();
720 
721 					if(str[p + 1] == '}') break;
722 
723 					int q = str.IndexOf(chSep, p + 1);
724 					if(q < 0) throw new FormatException();
725 
726 					lParams.Add(str.Substring(p + 1, q - p - 1));
727 					p = q;
728 				}
729 
730 				Debug.Assert(str[p + 1] == '}');
731 				str = str.Remove(iStart, (p + 1) - iStart + 1);
732 			}
733 			catch(Exception)
734 			{
735 				str = str.Substring(0, iStart);
736 			}
737 
738 			if(bSprCmpParams && (ctx != null))
739 			{
740 				SprContext ctxSub = ctx.WithoutContentTransformations();
741 				for(int i = 0; i < lParams.Count; ++i)
742 					lParams[i] = CompileInternal(lParams[i], ctxSub, uRecursionLevel);
743 			}
744 
745 			return true;
746 		}
747 
PerformTextTransforms(string strText, SprContext ctx, uint uRecursionLevel)748 		private static string PerformTextTransforms(string strText, SprContext ctx,
749 			uint uRecursionLevel)
750 		{
751 			string str = strText;
752 			int iStart;
753 			List<string> lParams;
754 
755 			// {T-CONV:/Text/Raw/} should be the first transformation
756 
757 			while(ParseAndRemovePlhWithParams(ref str, ctx, uRecursionLevel,
758 				@"{T-CONV:", out iStart, out lParams, true))
759 			{
760 				if(lParams.Count < 2) continue;
761 
762 				try
763 				{
764 					string strNew = lParams[0];
765 					string strCmd = lParams[1].ToLower();
766 
767 					if((strCmd == "u") || (strCmd == "upper"))
768 						strNew = strNew.ToUpper();
769 					else if((strCmd == "l") || (strCmd == "lower"))
770 						strNew = strNew.ToLower();
771 					else if(strCmd == "base64")
772 					{
773 						byte[] pbUtf8 = StrUtil.Utf8.GetBytes(strNew);
774 						strNew = Convert.ToBase64String(pbUtf8);
775 					}
776 					else if(strCmd == "hex")
777 					{
778 						byte[] pbUtf8 = StrUtil.Utf8.GetBytes(strNew);
779 						strNew = MemUtil.ByteArrayToHexString(pbUtf8);
780 					}
781 					else if(strCmd == "uri")
782 						strNew = Uri.EscapeDataString(strNew);
783 					else if(strCmd == "uri-dec")
784 						strNew = Uri.UnescapeDataString(strNew);
785 					// "raw": no modification
786 
787 					if(strCmd != "raw")
788 						strNew = TransformContent(strNew, ctx);
789 
790 					str = str.Insert(iStart, strNew);
791 				}
792 				catch(Exception) { Debug.Assert(false); }
793 			}
794 
795 			while(ParseAndRemovePlhWithParams(ref str, ctx, uRecursionLevel,
796 				@"{T-REPLACE-RX:", out iStart, out lParams, true))
797 			{
798 				if(lParams.Count < 2) continue;
799 				if(lParams.Count == 2) lParams.Add(string.Empty);
800 
801 				try
802 				{
803 					string strNew = Regex.Replace(lParams[0], lParams[1], lParams[2]);
804 					strNew = TransformContent(strNew, ctx);
805 					str = str.Insert(iStart, strNew);
806 				}
807 				catch(Exception) { }
808 			}
809 
810 			return str;
811 		}
812 
PerformClipboardCopy(string strText, SprContext ctx, uint uRecursionLevel)813 		private static string PerformClipboardCopy(string strText, SprContext ctx,
814 			uint uRecursionLevel)
815 		{
816 			string str = strText;
817 			int iStart;
818 			List<string> lParams;
819 			SprContext ctxData = ((ctx != null) ? ctx.WithoutContentTransformations() : null);
820 
821 			while(ParseAndRemovePlhWithParams(ref str, ctxData, uRecursionLevel,
822 				@"{CLIPBOARD-SET:", out iStart, out lParams, true))
823 			{
824 				if(lParams.Count < 1) continue;
825 
826 				try
827 				{
828 					ClipboardUtil.Copy(lParams[0] ?? string.Empty, false,
829 						true, null, null, IntPtr.Zero);
830 				}
831 				catch(Exception) { Debug.Assert(false); }
832 			}
833 
834 			return str;
835 		}
836 
FillGroupPlh(string strData, string strPlhPrefix, PwGroup pg, SprContext ctx, uint uRecursionLevel)837 		private static string FillGroupPlh(string strData, string strPlhPrefix,
838 			PwGroup pg, SprContext ctx, uint uRecursionLevel)
839 		{
840 			Debug.Assert(strPlhPrefix.StartsWith("{"));
841 			Debug.Assert(!strPlhPrefix.EndsWith("_"));
842 			Debug.Assert(!strPlhPrefix.EndsWith("}"));
843 
844 			string str = strData;
845 
846 			str = Fill(str, strPlhPrefix + @"}", pg.Name, ctx, uRecursionLevel);
847 
848 			string strGroupPath = pg.GetFullPath();
849 			str = Fill(str, strPlhPrefix + @"_PATH}", strGroupPath,
850 				ctx, uRecursionLevel);
851 			str = Fill(str, strPlhPrefix + @"PATH}", strGroupPath,
852 				ctx, uRecursionLevel); // Obsolete; for backward compatibility
853 
854 			str = Fill(str, strPlhPrefix + @"_NOTES}", pg.Notes, ctx, uRecursionLevel);
855 
856 			return str;
857 		}
858 
RunCommands(string strText, SprContext ctx, uint uRecursionLevel)859 		private static string RunCommands(string strText, SprContext ctx,
860 			uint uRecursionLevel)
861 		{
862 			string str = strText;
863 			int iStart;
864 			List<string> lParams;
865 
866 			while(ParseAndRemovePlhWithParams(ref str, ctx, uRecursionLevel,
867 				@"{CMD:", out iStart, out lParams, false))
868 			{
869 				if(lParams.Count == 0) continue;
870 
871 				string strBaseRaw = null;
872 				if((ctx != null) && (ctx.Base != null))
873 				{
874 					if(ctx.BaseIsEncoded)
875 						strBaseRaw = UntransformContent(ctx.Base, ctx);
876 					else strBaseRaw = ctx.Base;
877 				}
878 
879 				string strCmd = WinUtil.CompileUrl((lParams[0] ?? string.Empty),
880 					((ctx != null) ? ctx.Entry : null), true, strBaseRaw, true);
881 				if(WinUtil.IsCommandLineUrl(strCmd))
882 					strCmd = WinUtil.GetCommandLineFromUrl(strCmd);
883 				if(string.IsNullOrEmpty(strCmd)) continue;
884 
885 				Process p = null;
886 				try
887 				{
888 					StringComparison sc = StrUtil.CaseIgnoreCmp;
889 
890 					string strOpt = ((lParams.Count >= 2) ? lParams[1] :
891 						string.Empty);
892 					Dictionary<string, string> d = SplitParams(strOpt);
893 
894 					ProcessStartInfo psi = new ProcessStartInfo();
895 
896 					string strApp, strArgs;
897 					StrUtil.SplitCommandLine(strCmd, out strApp, out strArgs);
898 					if(string.IsNullOrEmpty(strApp)) continue;
899 					psi.FileName = strApp;
900 					if(!string.IsNullOrEmpty(strArgs)) psi.Arguments = strArgs;
901 
902 					string strMethod = GetParam(d, "m", "s");
903 					bool bShellExec = !strMethod.Equals("c", sc);
904 					psi.UseShellExecute = bShellExec;
905 
906 					string strO = GetParam(d, "o", (bShellExec ? "0" : "1"));
907 					bool bStdOut = strO.Equals("1", sc);
908 					if(bStdOut) psi.RedirectStandardOutput = true;
909 
910 					string strWS = GetParam(d, "ws", "n");
911 					if(strWS.Equals("h", sc))
912 					{
913 						psi.CreateNoWindow = true;
914 						psi.WindowStyle = ProcessWindowStyle.Hidden;
915 					}
916 					else if(strWS.Equals("min", sc))
917 						psi.WindowStyle = ProcessWindowStyle.Minimized;
918 					else if(strWS.Equals("max", sc))
919 						psi.WindowStyle = ProcessWindowStyle.Maximized;
920 					else { Debug.Assert(psi.WindowStyle == ProcessWindowStyle.Normal); }
921 
922 					string strVerb = GetParam(d, "v", null);
923 					if(!string.IsNullOrEmpty(strVerb))
924 						psi.Verb = strVerb;
925 
926 					bool bWait = GetParam(d, "w", "1").Equals("1", sc);
927 
928 					p = NativeLib.StartProcessEx(psi);
929 					if(p == null) { Debug.Assert(false); continue; }
930 
931 					if(bStdOut)
932 					{
933 						string strOut = (p.StandardOutput.ReadToEnd() ?? string.Empty);
934 
935 						// Remove trailing new-line characters, like $(...);
936 						// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
937 						// https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html#Command-Substitution
938 						strOut = strOut.TrimEnd('\r', '\n');
939 
940 						strOut = TransformContent(strOut, ctx);
941 						str = str.Insert(iStart, strOut);
942 					}
943 
944 					if(bWait) p.WaitForExit();
945 				}
946 				catch(Exception ex)
947 				{
948 					string strMsg = strCmd + MessageService.NewParagraph + ex.Message;
949 					MessageService.ShowWarning(strMsg);
950 				}
951 				finally
952 				{
953 					try { if(p != null) p.Dispose(); }
954 					catch(Exception) { Debug.Assert(false); }
955 				}
956 			}
957 
958 			return str;
959 		}
960 
SplitParams(string str)961 		private static Dictionary<string, string> SplitParams(string str)
962 		{
963 			Dictionary<string, string> d = new Dictionary<string, string>();
964 			if(string.IsNullOrEmpty(str)) return d;
965 
966 			char[] vSplitPrm = new char[] { ',' };
967 			char[] vSplitKvp = new char[] { '=' };
968 
969 			string[] v = str.Split(vSplitPrm);
970 			foreach(string strOption in v)
971 			{
972 				if(string.IsNullOrEmpty(strOption)) continue;
973 
974 				string[] vKvp = strOption.Split(vSplitKvp);
975 				if(vKvp.Length != 2) continue;
976 
977 				string strKey = (vKvp[0] ?? string.Empty).Trim().ToLower();
978 				string strValue = (vKvp[1] ?? string.Empty).Trim();
979 
980 				d[strKey] = strValue;
981 			}
982 
983 			return d;
984 		}
985 
GetParam(Dictionary<string, string> d, string strName, string strDefaultValue)986 		private static string GetParam(Dictionary<string, string> d,
987 			string strName, string strDefaultValue)
988 		{
989 			if(d == null) { Debug.Assert(false); return strDefaultValue; }
990 			if(strName == null) { Debug.Assert(false); return strDefaultValue; }
991 
992 			Debug.Assert(strName == strName.ToLower());
993 
994 			string strValue;
995 			if(d.TryGetValue(strName, out strValue)) return strValue;
996 
997 			return strDefaultValue;
998 		}
999 	}
1000 }
1001