aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Converters/WixConverter.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Converters/WixConverter.cs')
-rw-r--r--src/WixToolset.Converters/WixConverter.cs1098
1 files changed, 1098 insertions, 0 deletions
diff --git a/src/WixToolset.Converters/WixConverter.cs b/src/WixToolset.Converters/WixConverter.cs
new file mode 100644
index 00000000..a89d44ce
--- /dev/null
+++ b/src/WixToolset.Converters/WixConverter.cs
@@ -0,0 +1,1098 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Converters
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.IO;
9 using System.Linq;
10 using System.Text;
11 using System.Text.RegularExpressions;
12 using System.Xml;
13 using System.Xml.Linq;
14 using WixToolset.Data;
15 using WixToolset.Extensibility.Services;
16
17 /// <summary>
18 /// WiX source code converter.
19 /// </summary>
20 public class WixConverter
21 {
22 private static readonly Regex AddPrefix = new Regex(@"^[^a-zA-Z_]", RegexOptions.Compiled);
23 private static readonly Regex IllegalIdentifierCharacters = new Regex(@"[^A-Za-z0-9_\.]|\.{2,}", RegexOptions.Compiled); // non 'words' and assorted valid characters
24
25 private const char XDocumentNewLine = '\n'; // XDocument normalizes "\r\n" to just "\n".
26 private static readonly XNamespace WixNamespace = "http://wixtoolset.org/schemas/v4/wxs";
27 private static readonly XNamespace WixUtilNamespace = "http://wixtoolset.org/schemas/v4/wxs/util";
28
29 private static readonly XName AdminExecuteSequenceElementName = WixNamespace + "AdminExecuteSequence";
30 private static readonly XName AdminUISequenceSequenceElementName = WixNamespace + "AdminUISequence";
31 private static readonly XName AdvertiseExecuteSequenceElementName = WixNamespace + "AdvertiseExecuteSequence";
32 private static readonly XName InstallExecuteSequenceElementName = WixNamespace + "InstallExecuteSequence";
33 private static readonly XName InstallUISequenceSequenceElementName = WixNamespace + "InstallUISequence";
34 private static readonly XName EmbeddedChainerElementName = WixNamespace + "EmbeddedChainer";
35 private static readonly XName ColumnElementName = WixNamespace + "Column";
36 private static readonly XName ComponentElementName = WixNamespace + "Component";
37 private static readonly XName ControlElementName = WixNamespace + "Control";
38 private static readonly XName ConditionElementName = WixNamespace + "Condition";
39 private static readonly XName CreateFolderElementName = WixNamespace + "CreateFolder";
40 private static readonly XName CustomTableElementName = WixNamespace + "CustomTable";
41 private static readonly XName DirectoryElementName = WixNamespace + "Directory";
42 private static readonly XName FeatureElementName = WixNamespace + "Feature";
43 private static readonly XName FileElementName = WixNamespace + "File";
44 private static readonly XName FragmentElementName = WixNamespace + "Fragment";
45 private static readonly XName ErrorElementName = WixNamespace + "Error";
46 private static readonly XName LaunchElementName = WixNamespace + "Launch";
47 private static readonly XName LevelElementName = WixNamespace + "Level";
48 private static readonly XName ExePackageElementName = WixNamespace + "ExePackage";
49 private static readonly XName MsiPackageElementName = WixNamespace + "MsiPackage";
50 private static readonly XName MspPackageElementName = WixNamespace + "MspPackage";
51 private static readonly XName MsuPackageElementName = WixNamespace + "MsuPackage";
52 private static readonly XName PayloadElementName = WixNamespace + "Payload";
53 private static readonly XName PermissionExElementName = WixNamespace + "PermissionEx";
54 private static readonly XName ProductElementName = WixNamespace + "Product";
55 private static readonly XName ProgressTextElementName = WixNamespace + "ProgressText";
56 private static readonly XName PublishElementName = WixNamespace + "Publish";
57 private static readonly XName MultiStringValueElementName = WixNamespace + "MultiStringValue";
58 private static readonly XName RequiredPrivilegeElementName = WixNamespace + "RequiredPrivilege";
59 private static readonly XName RowElementName = WixNamespace + "Row";
60 private static readonly XName ServiceArgumentElementName = WixNamespace + "ServiceArgument";
61 private static readonly XName SetDirectoryElementName = WixNamespace + "SetDirectory";
62 private static readonly XName SetPropertyElementName = WixNamespace + "SetProperty";
63 private static readonly XName ShortcutPropertyElementName = WixNamespace + "ShortcutProperty";
64 private static readonly XName TextElementName = WixNamespace + "Text";
65 private static readonly XName UITextElementName = WixNamespace + "UIText";
66 private static readonly XName UtilPermissionExElementName = WixUtilNamespace + "PermissionEx";
67 private static readonly XName CustomActionElementName = WixNamespace + "CustomAction";
68 private static readonly XName PropertyElementName = WixNamespace + "Property";
69 private static readonly XName WixElementWithoutNamespaceName = XNamespace.None + "Wix";
70 private static readonly XName IncludeElementWithoutNamespaceName = XNamespace.None + "Include";
71
72 private static readonly Dictionary<string, XNamespace> OldToNewNamespaceMapping = new Dictionary<string, XNamespace>()
73 {
74 { "http://schemas.microsoft.com/wix/BalExtension", "http://wixtoolset.org/schemas/v4/wxs/bal" },
75 { "http://schemas.microsoft.com/wix/ComPlusExtension", "http://wixtoolset.org/schemas/v4/wxs/complus" },
76 { "http://schemas.microsoft.com/wix/DependencyExtension", "http://wixtoolset.org/schemas/v4/wxs/dependency" },
77 { "http://schemas.microsoft.com/wix/DifxAppExtension", "http://wixtoolset.org/schemas/v4/wxs/difxapp" },
78 { "http://schemas.microsoft.com/wix/FirewallExtension", "http://wixtoolset.org/schemas/v4/wxs/firewall" },
79 { "http://schemas.microsoft.com/wix/HttpExtension", "http://wixtoolset.org/schemas/v4/wxs/http" },
80 { "http://schemas.microsoft.com/wix/IIsExtension", "http://wixtoolset.org/schemas/v4/wxs/iis" },
81 { "http://schemas.microsoft.com/wix/MsmqExtension", "http://wixtoolset.org/schemas/v4/wxs/msmq" },
82 { "http://schemas.microsoft.com/wix/NetFxExtension", "http://wixtoolset.org/schemas/v4/wxs/netfx" },
83 { "http://schemas.microsoft.com/wix/PSExtension", "http://wixtoolset.org/schemas/v4/wxs/powershell" },
84 { "http://schemas.microsoft.com/wix/SqlExtension", "http://wixtoolset.org/schemas/v4/wxs/sql" },
85 { "http://schemas.microsoft.com/wix/TagExtension", "http://wixtoolset.org/schemas/v4/wxs/tag" },
86 { "http://schemas.microsoft.com/wix/UtilExtension", WixUtilNamespace },
87 { "http://schemas.microsoft.com/wix/VSExtension", "http://wixtoolset.org/schemas/v4/wxs/vs" },
88 { "http://wixtoolset.org/schemas/thmutil/2010", "http://wixtoolset.org/schemas/v4/thmutil" },
89 { "http://schemas.microsoft.com/wix/2009/Lux", "http://wixtoolset.org/schemas/v4/lux" },
90 { "http://schemas.microsoft.com/wix/2006/wi", "http://wixtoolset.org/schemas/v4/wxs" },
91 { "http://schemas.microsoft.com/wix/2006/localization", "http://wixtoolset.org/schemas/v4/wxl" },
92 { "http://schemas.microsoft.com/wix/2006/libraries", "http://wixtoolset.org/schemas/v4/wixlib" },
93 { "http://schemas.microsoft.com/wix/2006/objects", "http://wixtoolset.org/schemas/v4/wixobj" },
94 { "http://schemas.microsoft.com/wix/2006/outputs", "http://wixtoolset.org/schemas/v4/wixout" },
95 { "http://schemas.microsoft.com/wix/2007/pdbs", "http://wixtoolset.org/schemas/v4/wixpdb" },
96 { "http://schemas.microsoft.com/wix/2003/04/actions", "http://wixtoolset.org/schemas/v4/wi/actions" },
97 { "http://schemas.microsoft.com/wix/2006/tables", "http://wixtoolset.org/schemas/v4/wi/tables" },
98 { "http://schemas.microsoft.com/wix/2006/WixUnit", "http://wixtoolset.org/schemas/v4/wixunit" },
99 };
100
101 private readonly static SortedSet<string> Wix3Namespaces = new SortedSet<string>
102 {
103 "http://schemas.microsoft.com/wix/2006/wi",
104 "http://schemas.microsoft.com/wix/2006/localization",
105 };
106
107 private readonly static SortedSet<string> Wix4Namespaces = new SortedSet<string>
108 {
109 "http://wixtoolset.org/schemas/v4/wxs",
110 "http://wixtoolset.org/schemas/v4/wxl",
111 };
112
113 private readonly Dictionary<XName, Action<XElement>> ConvertElementMapping;
114
115 /// <summary>
116 /// Instantiate a new Converter class.
117 /// </summary>
118 /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param>
119 /// <param name="errorsAsWarnings">Test errors to display as warnings.</param>
120 /// <param name="ignoreErrors">Test errors to ignore.</param>
121 public WixConverter(IMessaging messaging, int indentationAmount, IEnumerable<string> errorsAsWarnings = null, IEnumerable<string> ignoreErrors = null)
122 {
123 this.ConvertElementMapping = new Dictionary<XName, Action<XElement>>
124 {
125 { WixConverter.AdminExecuteSequenceElementName, this.ConvertSequenceElement },
126 { WixConverter.AdminUISequenceSequenceElementName, this.ConvertSequenceElement },
127 { WixConverter.AdvertiseExecuteSequenceElementName, this.ConvertSequenceElement },
128 { WixConverter.InstallUISequenceSequenceElementName, this.ConvertSequenceElement },
129 { WixConverter.InstallExecuteSequenceElementName, this.ConvertSequenceElement },
130 { WixConverter.ColumnElementName, this.ConvertColumnElement },
131 { WixConverter.CustomTableElementName, this.ConvertCustomTableElement },
132 { WixConverter.ControlElementName, this.ConvertControlElement },
133 { WixConverter.ComponentElementName, this.ConvertComponentElement },
134 { WixConverter.DirectoryElementName, this.ConvertDirectoryElement },
135 { WixConverter.FeatureElementName, this.ConvertFeatureElement },
136 { WixConverter.FileElementName, this.ConvertFileElement },
137 { WixConverter.FragmentElementName, this.ConvertFragmentElement },
138 { WixConverter.EmbeddedChainerElementName, this.ConvertEmbeddedChainerElement },
139 { WixConverter.ErrorElementName, this.ConvertErrorElement },
140 { WixConverter.ExePackageElementName, this.ConvertSuppressSignatureValidation },
141 { WixConverter.MsiPackageElementName, this.ConvertSuppressSignatureValidation },
142 { WixConverter.MspPackageElementName, this.ConvertSuppressSignatureValidation },
143 { WixConverter.MsuPackageElementName, this.ConvertSuppressSignatureValidation },
144 { WixConverter.PayloadElementName, this.ConvertSuppressSignatureValidation },
145 { WixConverter.PermissionExElementName, this.ConvertPermissionExElement },
146 { WixConverter.ProductElementName, this.ConvertProductElement },
147 { WixConverter.ProgressTextElementName, this.ConvertProgressTextElement },
148 { WixConverter.PublishElementName, this.ConvertPublishElement },
149 { WixConverter.MultiStringValueElementName, this.ConvertMultiStringValueElement },
150 { WixConverter.RequiredPrivilegeElementName, this.ConvertRequiredPrivilegeElement },
151 { WixConverter.RowElementName, this.ConvertRowElement },
152 { WixConverter.CustomActionElementName, this.ConvertCustomActionElement },
153 { WixConverter.ServiceArgumentElementName, this.ConvertServiceArgumentElement },
154 { WixConverter.SetDirectoryElementName, this.ConvertSetDirectoryElement },
155 { WixConverter.SetPropertyElementName, this.ConvertSetPropertyElement },
156 { WixConverter.ShortcutPropertyElementName, this.ConvertShortcutPropertyElement },
157 { WixConverter.TextElementName, this.ConvertTextElement },
158 { WixConverter.UITextElementName, this.ConvertUITextElement },
159 { WixConverter.UtilPermissionExElementName, this.ConvertUtilPermissionExElement },
160 { WixConverter.PropertyElementName, this.ConvertPropertyElement },
161 { WixConverter.WixElementWithoutNamespaceName, this.ConvertElementWithoutNamespace },
162 { WixConverter.IncludeElementWithoutNamespaceName, this.ConvertElementWithoutNamespace },
163 };
164
165 this.Messaging = messaging;
166
167 this.IndentationAmount = indentationAmount;
168
169 this.ErrorsAsWarnings = new HashSet<ConverterTestType>(this.YieldConverterTypes(errorsAsWarnings));
170
171 this.IgnoreErrors = new HashSet<ConverterTestType>(this.YieldConverterTypes(ignoreErrors));
172 }
173
174 private int Errors { get; set; }
175
176 private HashSet<ConverterTestType> ErrorsAsWarnings { get; set; }
177
178 private HashSet<ConverterTestType> IgnoreErrors { get; set; }
179
180 private IMessaging Messaging { get; }
181
182 private int IndentationAmount { get; set; }
183
184 private string SourceFile { get; set; }
185
186 private int SourceVersion { get; set; }
187
188 /// <summary>
189 /// Convert a file.
190 /// </summary>
191 /// <param name="sourceFile">The file to convert.</param>
192 /// <param name="saveConvertedFile">Option to save the converted errors that are found.</param>
193 /// <returns>The number of errors found.</returns>
194 public int ConvertFile(string sourceFile, bool saveConvertedFile)
195 {
196 XDocument document;
197
198 // Set the instance info.
199 this.Errors = 0;
200 this.SourceFile = sourceFile;
201 this.SourceVersion = 0;
202
203 try
204 {
205 document = XDocument.Load(this.SourceFile, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
206 }
207 catch (XmlException e)
208 {
209 this.OnError(ConverterTestType.XmlException, null, "The xml is invalid. Detail: '{0}'", e.Message);
210
211 return this.Errors;
212 }
213
214 this.ConvertDocument(document);
215
216 // Fix errors if requested and necessary.
217 if (saveConvertedFile && 0 < this.Errors)
218 {
219 try
220 {
221 using (var writer = XmlWriter.Create(this.SourceFile, new XmlWriterSettings { OmitXmlDeclaration = true }))
222 {
223 document.Save(writer);
224 }
225 }
226 catch (UnauthorizedAccessException)
227 {
228 this.OnError(ConverterTestType.UnauthorizedAccessException, null, "Could not write to file.");
229 }
230 }
231
232 return this.Errors;
233 }
234
235 /// <summary>
236 /// Convert a document.
237 /// </summary>
238 /// <param name="document">The document to convert.</param>
239 /// <returns>The number of errors found.</returns>
240 public int ConvertDocument(XDocument document)
241 {
242 this.Errors = 0;
243 this.SourceVersion = 0;
244
245 var declaration = document.Declaration;
246
247 // Remove the declaration.
248 if (null != declaration)
249 {
250 if (this.OnError(ConverterTestType.DeclarationPresent, null, "This file contains an XML declaration on the first line."))
251 {
252 document.Declaration = null;
253 }
254 }
255
256 TrimLeadingText(document);
257
258 // Start converting the nodes at the top.
259 this.ConvertNodes(document.Nodes(), 0);
260
261 return this.Errors;
262 }
263
264 private void ConvertNodes(IEnumerable<XNode> nodes, int level)
265 {
266 // Note we operate on a copy of the node list since we may
267 // remove some whitespace nodes during this processing.
268 foreach (var node in nodes.ToList())
269 {
270 if (node is XText text)
271 {
272 if (!String.IsNullOrWhiteSpace(text.Value))
273 {
274 text.Value = text.Value.Trim();
275 }
276 else if (node.NextNode is XCData cdata)
277 {
278 this.EnsurePrecedingWhitespaceRemoved(text, node, ConverterTestType.WhitespacePrecedingNodeWrong);
279 }
280 else if (node.NextNode is XElement element)
281 {
282 this.EnsurePrecedingWhitespaceCorrect(text, node, level, ConverterTestType.WhitespacePrecedingNodeWrong);
283 }
284 else if (node.NextNode is null) // this is the space before the close element
285 {
286 if (node.PreviousNode is null || node.PreviousNode is XCData)
287 {
288 this.EnsurePrecedingWhitespaceRemoved(text, node.Parent, ConverterTestType.WhitespacePrecedingEndElementWrong);
289 }
290 else if (level == 0) // root element's close tag
291 {
292 this.EnsurePrecedingWhitespaceCorrect(text, node, 0, ConverterTestType.WhitespacePrecedingEndElementWrong);
293 }
294 else
295 {
296 this.EnsurePrecedingWhitespaceCorrect(text, node, level - 1, ConverterTestType.WhitespacePrecedingEndElementWrong);
297 }
298 }
299 }
300 else if (node is XElement element)
301 {
302 this.ConvertElement(element);
303
304 this.ConvertNodes(element.Nodes(), level + 1);
305 }
306 }
307 }
308
309 private void EnsurePrecedingWhitespaceCorrect(XText whitespace, XNode node, int level, ConverterTestType testType)
310 {
311 if (!WixConverter.LeadingWhitespaceValid(this.IndentationAmount, level, whitespace.Value))
312 {
313 var message = testType == ConverterTestType.WhitespacePrecedingEndElementWrong ? "The whitespace preceding this end element is incorrect." : "The whitespace preceding this node is incorrect.";
314
315 if (this.OnError(testType, node, message))
316 {
317 WixConverter.FixupWhitespace(this.IndentationAmount, level, whitespace);
318 }
319 }
320 }
321
322 private void EnsurePrecedingWhitespaceRemoved(XText whitespace, XNode node, ConverterTestType testType)
323 {
324 if (!String.IsNullOrEmpty(whitespace.Value) && whitespace.NodeType != XmlNodeType.CDATA)
325 {
326 var message = testType == ConverterTestType.WhitespacePrecedingEndElementWrong ? "The whitespace preceding this end element is incorrect." : "The whitespace preceding this node is incorrect.";
327
328 if (this.OnError(testType, node, message))
329 {
330 whitespace.Remove();
331 }
332 }
333 }
334
335 private void ConvertElement(XElement element)
336 {
337 // Gather any deprecated namespaces, then update this element tree based on those deprecations.
338 var deprecatedToUpdatedNamespaces = new Dictionary<XNamespace, XNamespace>();
339
340 foreach (var declaration in element.Attributes().Where(a => a.IsNamespaceDeclaration))
341 {
342 if (WixConverter.OldToNewNamespaceMapping.TryGetValue(declaration.Value, out var ns))
343 {
344 if (Wix3Namespaces.Contains(declaration.Value))
345 {
346 this.SourceVersion = 3;
347 }
348 else if (Wix4Namespaces.Contains(declaration.Value))
349 {
350 this.SourceVersion = 4;
351 }
352
353 if (this.OnError(ConverterTestType.XmlnsValueWrong, declaration, "The namespace '{0}' is out of date. It must be '{1}'.", declaration.Value, ns.NamespaceName))
354 {
355 deprecatedToUpdatedNamespaces.Add(declaration.Value, ns);
356 }
357 }
358 }
359
360 if (deprecatedToUpdatedNamespaces.Any())
361 {
362 WixConverter.UpdateElementsWithDeprecatedNamespaces(element.DescendantsAndSelf(), deprecatedToUpdatedNamespaces);
363 }
364
365 // Apply any specialized conversion actions.
366 if (this.ConvertElementMapping.TryGetValue(element.Name, out var convert))
367 {
368 convert(element);
369 }
370 }
371
372 private void ConvertColumnElement(XElement element)
373 {
374 var category = element.Attribute("Category");
375 if (category != null)
376 {
377 var camelCaseValue = LowercaseFirstChar(category.Value);
378 if (category.Value != camelCaseValue &&
379 this.OnError(ConverterTestType.ColumnCategoryCamelCase, element, "The CustomTable Category attribute contains an incorrectly cased '{0}' value. Lowercase the first character instead.", category.Name))
380 {
381 category.Value = camelCaseValue;
382 }
383 }
384
385 var modularization = element.Attribute("Modularize");
386 if (modularization != null)
387 {
388 var camelCaseValue = LowercaseFirstChar(modularization.Value);
389 if (category.Value != camelCaseValue &&
390 this.OnError(ConverterTestType.ColumnModularizeCamelCase, element, "The CustomTable Modularize attribute contains an incorrectly cased '{0}' value. Lowercase the first character instead.", modularization.Name))
391 {
392 modularization.Value = camelCaseValue;
393 }
394 }
395 }
396
397 private void ConvertCustomTableElement(XElement element)
398 {
399 var bootstrapperApplicationData = element.Attribute("BootstrapperApplicationData");
400 if (bootstrapperApplicationData != null
401 && this.OnError(ConverterTestType.BootstrapperApplicationDataDeprecated, element, "The CustomTable element contains deprecated '{0}' attribute. Use the 'Unreal' attribute instead.", bootstrapperApplicationData.Name))
402 {
403 element.Add(new XAttribute("Unreal", bootstrapperApplicationData.Value));
404 bootstrapperApplicationData.Remove();
405 }
406 }
407
408 private void ConvertControlElement(XElement element)
409 {
410 var xCondition = element.Element(ConditionElementName);
411 if (xCondition != null)
412 {
413 var action = UppercaseFirstChar(xCondition.Attribute("Action")?.Value);
414 if (!String.IsNullOrEmpty(action) &&
415 TryGetInnerText(xCondition, out var text) &&
416 this.OnError(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the '{1}Condition' attribute instead.", xCondition.Name.LocalName, action))
417 {
418 element.Add(new XAttribute(action + "Condition", text));
419 xCondition.Remove();
420 }
421 }
422 }
423
424 private void ConvertComponentElement(XElement element)
425 {
426 var guid = element.Attribute("Guid");
427 if (guid != null && guid.Value == "*")
428 {
429 if (this.OnError(ConverterTestType.AutoGuidUnnecessary, element, "Using '*' for the Component Guid attribute is unnecessary. Remove the attribute to remove the redundancy."))
430 {
431 guid.Remove();
432 }
433 }
434
435 var xCondition = element.Element(ConditionElementName);
436 if (xCondition != null)
437 {
438 if (TryGetInnerText(xCondition, out var text) &&
439 this.OnError(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Condition' attribute instead.", xCondition.Name.LocalName))
440 {
441 element.Add(new XAttribute("Condition", text));
442 xCondition.Remove();
443 }
444 }
445 }
446
447 private void ConvertDirectoryElement(XElement element)
448 {
449 if (null == element.Attribute("Name"))
450 {
451 var attribute = element.Attribute("ShortName");
452 if (null != attribute)
453 {
454 var shortName = attribute.Value;
455 if (this.OnError(ConverterTestType.AssignDirectoryNameFromShortName, element, "The directory ShortName attribute is being renamed to Name since Name wasn't specified for value '{0}'", shortName))
456 {
457 element.Add(new XAttribute("Name", shortName));
458 attribute.Remove();
459 }
460 }
461 }
462 }
463
464 private void ConvertFeatureElement(XElement element)
465 {
466 var xCondition = element.Element(ConditionElementName);
467 if (xCondition != null)
468 {
469 var level = xCondition.Attribute("Level")?.Value;
470 if (!String.IsNullOrEmpty(level) &&
471 TryGetInnerText(xCondition, out var text) &&
472 this.OnError(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Level' element instead.", xCondition.Name.LocalName))
473 {
474 xCondition.AddAfterSelf(new XElement(LevelElementName,
475 new XAttribute("Value", level),
476 new XAttribute("Condition", text)
477 ));
478 xCondition.Remove();
479 }
480 }
481 }
482
483 private void ConvertFileElement(XElement element)
484 {
485 if (this.SourceVersion < 4 && null == element.Attribute("Id"))
486 {
487 var attribute = element.Attribute("Name");
488
489 if (null == attribute)
490 {
491 attribute = element.Attribute("Source");
492 }
493
494 if (null != attribute)
495 {
496 var name = Path.GetFileName(attribute.Value);
497
498 if (this.OnError(ConverterTestType.AssignAnonymousFileId, element, "The file id is being updated to '{0}' to ensure it remains the same as the v3 default", name))
499 {
500 IEnumerable<XAttribute> attributes = element.Attributes().ToList();
501 element.RemoveAttributes();
502 element.Add(new XAttribute("Id", GetIdentifierFromName(name)));
503 element.Add(attributes);
504 }
505 }
506 }
507 }
508
509 private void ConvertFragmentElement(XElement element)
510 {
511 var xCondition = element.Element(ConditionElementName);
512 if (xCondition != null)
513 {
514 var message = xCondition.Attribute("Message")?.Value;
515
516 if (!String.IsNullOrEmpty(message) &&
517 TryGetInnerText(xCondition, out var text) &&
518 this.OnError(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Launch' element instead.", xCondition.Name.LocalName))
519 {
520 xCondition.AddAfterSelf(new XElement(LaunchElementName,
521 new XAttribute("Condition", text),
522 new XAttribute("Message", message)
523 ));
524 xCondition.Remove();
525 }
526 }
527 }
528
529 private void ConvertEmbeddedChainerElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Condition");
530
531 private void ConvertErrorElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Message");
532
533 private void ConvertPermissionExElement(XElement element)
534 {
535 var xCondition = element.Element(ConditionElementName);
536 if (xCondition != null)
537 {
538 if (TryGetInnerText(xCondition, out var text) &&
539 this.OnError(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Condition' attribute instead.", xCondition.Name.LocalName))
540 {
541 element.Add(new XAttribute("Condition", text));
542 xCondition.Remove();
543 }
544 }
545 }
546
547 private void ConvertProgressTextElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Message");
548
549 private void ConvertProductElement(XElement element)
550 {
551 var id = element.Attribute("Id");
552 if (id != null && id.Value == "*")
553 {
554 if (this.OnError(ConverterTestType.AutoGuidUnnecessary, element, "Using '*' for the Product Id attribute is unnecessary. Remove the attribute to remove the redundancy."))
555 {
556 id.Remove();
557 }
558 }
559
560 var xCondition = element.Element(ConditionElementName);
561 if (xCondition != null)
562 {
563 var message = element.Attribute("Message")?.Value;
564
565 if (!String.IsNullOrEmpty(message) &&
566 TryGetInnerText(xCondition, out var text) &&
567 this.OnError(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Launch' element instead.", xCondition.Name.LocalName))
568 {
569 xCondition.AddAfterSelf(new XElement(LaunchElementName,
570 new XAttribute("Condition", text),
571 new XAttribute("Message", message)
572 ));
573 xCondition.Remove();
574 }
575 }
576 }
577
578 private void ConvertPublishElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Condition");
579
580 private void ConvertMultiStringValueElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Value");
581
582 private void ConvertRequiredPrivilegeElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Name");
583
584 private void ConvertRowElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Value");
585
586 private void ConvertSequenceElement(XElement element)
587 {
588 foreach (var child in element.Elements())
589 {
590 this.ConvertInnerTextToAttribute(child, "Condition");
591 }
592 }
593
594 private void ConvertServiceArgumentElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Value");
595
596 private void ConvertSetDirectoryElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Condition");
597
598 private void ConvertSetPropertyElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Condition");
599
600 private void ConvertShortcutPropertyElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Value");
601
602 private void ConvertSuppressSignatureValidation(XElement element)
603 {
604 var suppressSignatureValidation = element.Attribute("SuppressSignatureValidation");
605
606 if (null != suppressSignatureValidation)
607 {
608 if (this.OnError(ConverterTestType.SuppressSignatureValidationDeprecated, element, "The chain package element contains deprecated '{0}' attribute. Use the 'EnableSignatureValidation' attribute instead.", suppressSignatureValidation.Name))
609 {
610 if ("no" == suppressSignatureValidation.Value)
611 {
612 element.Add(new XAttribute("EnableSignatureValidation", "yes"));
613 }
614 }
615
616 suppressSignatureValidation.Remove();
617 }
618 }
619
620 private void ConvertTextElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Value");
621
622 private void ConvertUITextElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Value");
623
624 private void ConvertCustomActionElement(XElement xCustomAction)
625 {
626 var xBinaryKey = xCustomAction.Attribute("BinaryKey");
627
628 if (xBinaryKey?.Value == "WixCA" || xBinaryKey?.Value == "UtilCA")
629 {
630 if (this.OnError(ConverterTestType.WixCABinaryIdRenamed, xCustomAction, "The WixCA custom action DLL Binary table id has been renamed. Use the id 'Wix4UtilCA_X86' instead."))
631 {
632 xBinaryKey.Value = "Wix4UtilCA_X86";
633 }
634 }
635
636 if (xBinaryKey?.Value == "WixCA_x64" || xBinaryKey?.Value == "UtilCA_x64")
637 {
638 if (this.OnError(ConverterTestType.WixCABinaryIdRenamed, xCustomAction, "The WixCA_x64 custom action DLL Binary table id has been renamed. Use the id 'Wix4UtilCA_X64' instead."))
639 {
640 xBinaryKey.Value = "Wix4UtilCA_X64";
641 }
642 }
643
644 var xDllEntry = xCustomAction.Attribute("DllEntry");
645
646 if (xDllEntry?.Value == "CAQuietExec" || xDllEntry?.Value == "CAQuietExec64")
647 {
648 if (this.OnError(ConverterTestType.QuietExecCustomActionsRenamed, xCustomAction, "The CAQuietExec and CAQuietExec64 custom action ids have been renamed. Use the ids 'WixQuietExec' and 'WixQuietExec64' instead."))
649 {
650 xDllEntry.Value = xDllEntry.Value.Replace("CAQuietExec", "WixQuietExec");
651 }
652 }
653
654 var xProperty = xCustomAction.Attribute("Property");
655
656 if (xProperty?.Value == "QtExecCmdLine" || xProperty?.Value == "QtExec64CmdLine")
657 {
658 if (this.OnError(ConverterTestType.QuietExecCustomActionsRenamed, xCustomAction, "The QtExecCmdLine and QtExec64CmdLine property ids have been renamed. Use the ids 'WixQuietExecCmdLine' and 'WixQuietExec64CmdLine' instead."))
659 {
660 xProperty.Value = xProperty.Value.Replace("QtExec", "WixQuietExec");
661 }
662 }
663
664 var xScript = xCustomAction.Attribute("Script");
665
666 if (xScript != null && TryGetInnerText(xCustomAction, out var scriptText))
667 {
668 if (this.OnError(ConverterTestType.InnerTextDeprecated, xCustomAction, "Using {0} element text is deprecated. Extract the text to a file and use the 'ScriptFile' attribute to reference it.", xCustomAction.Name.LocalName))
669 {
670 var scriptFolder = Path.GetDirectoryName(this.SourceFile) ?? String.Empty;
671 var id = xCustomAction.Attribute("Id")?.Value ?? Guid.NewGuid().ToString("N");
672 var ext = (xScript.Value == "jscript") ? ".js" : (xScript.Value == "vbscript") ? ".vbs" : ".txt";
673
674 var scriptFile = Path.Combine(scriptFolder, id + ext);
675 File.WriteAllText(scriptFile, scriptText);
676
677 RemoveChildren(xCustomAction);
678 xCustomAction.Add(new XAttribute("ScriptFile", scriptFile));
679 }
680 }
681 }
682
683 private void ConvertPropertyElement(XElement xProperty)
684 {
685 var xId = xProperty.Attribute("Id");
686
687 if (xId.Value == "QtExecCmdTimeout")
688 {
689 this.OnError(ConverterTestType.QtExecCmdTimeoutAmbiguous, xProperty, "QtExecCmdTimeout was previously used for both CAQuietExec and CAQuietExec64. For WixQuietExec, use WixQuietExecCmdTimeout. For WixQuietExec64, use WixQuietExec64CmdTimeout.");
690 }
691
692 this.ConvertInnerTextToAttribute(xProperty, "Value");
693 }
694
695 private void ConvertUtilPermissionExElement(XElement element)
696 {
697 if (this.SourceVersion < 4 && null == element.Attribute("Inheritable"))
698 {
699 var inheritable = element.Parent.Name == CreateFolderElementName;
700 if (!inheritable)
701 {
702 if (this.OnError(ConverterTestType.AssignPermissionExInheritable, element, "The PermissionEx Inheritable attribute is being set to 'no' to ensure it remains the same as the v3 default"))
703 {
704 element.Add(new XAttribute("Inheritable", "no"));
705 }
706 }
707 }
708 }
709
710 /// <summary>
711 /// Converts a Wix element.
712 /// </summary>
713 /// <param name="element">The Wix element to convert.</param>
714 /// <returns>The converted element.</returns>
715 private void ConvertElementWithoutNamespace(XElement element)
716 {
717 if (this.OnError(ConverterTestType.XmlnsMissing, element, "The xmlns attribute is missing. It must be present with a value of '{0}'.", WixNamespace.NamespaceName))
718 {
719 element.Name = WixNamespace.GetName(element.Name.LocalName);
720
721 element.Add(new XAttribute("xmlns", WixNamespace.NamespaceName)); // set the default namespace.
722
723 foreach (var elementWithoutNamespace in element.DescendantsAndSelf().Where(e => XNamespace.None == e.Name.Namespace))
724 {
725 elementWithoutNamespace.Name = WixNamespace.GetName(elementWithoutNamespace.Name.LocalName);
726 }
727 }
728 }
729
730 private void ConvertInnerTextToAttribute(XElement element, string attributeName)
731 {
732 if (TryGetInnerText(element, out var text) &&
733 this.OnError(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the '{1}' attribute instead.", element.Name.LocalName, attributeName))
734 {
735 element.Add(new XAttribute(attributeName, text));
736 RemoveChildren(element);
737 }
738 }
739
740 private IEnumerable<ConverterTestType> YieldConverterTypes(IEnumerable<string> types)
741 {
742 if (null != types)
743 {
744 foreach (var type in types)
745 {
746 if (Enum.TryParse<ConverterTestType>(type, true, out var itt))
747 {
748 yield return itt;
749 }
750 else // not a known ConverterTestType
751 {
752 this.OnError(ConverterTestType.ConverterTestTypeUnknown, null, "Unknown error type: '{0}'.", type);
753 }
754 }
755 }
756 }
757
758 private static void UpdateElementsWithDeprecatedNamespaces(IEnumerable<XElement> elements, Dictionary<XNamespace, XNamespace> deprecatedToUpdatedNamespaces)
759 {
760 foreach (var element in elements)
761 {
762
763 if (deprecatedToUpdatedNamespaces.TryGetValue(element.Name.Namespace, out var ns))
764 {
765 element.Name = ns.GetName(element.Name.LocalName);
766 }
767
768 // Remove all the attributes and add them back to with their namespace updated (as necessary).
769 IEnumerable<XAttribute> attributes = element.Attributes().ToList();
770 element.RemoveAttributes();
771
772 foreach (var attribute in attributes)
773 {
774 var convertedAttribute = attribute;
775
776 if (attribute.IsNamespaceDeclaration)
777 {
778 if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Value, out ns))
779 {
780 convertedAttribute = ("xmlns" == attribute.Name.LocalName) ? new XAttribute(attribute.Name.LocalName, ns.NamespaceName) : new XAttribute(XNamespace.Xmlns + attribute.Name.LocalName, ns.NamespaceName);
781 }
782 }
783 else if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Name.Namespace, out ns))
784 {
785 convertedAttribute = new XAttribute(ns.GetName(attribute.Name.LocalName), attribute.Value);
786 }
787
788 element.Add(convertedAttribute);
789 }
790 }
791 }
792
793 /// <summary>
794 /// Determine if the whitespace preceding a node is appropriate for its depth level.
795 /// </summary>
796 /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param>
797 /// <param name="level">The depth level that should match this whitespace.</param>
798 /// <param name="whitespace">The whitespace to validate.</param>
799 /// <returns>true if the whitespace is legal; false otherwise.</returns>
800 private static bool LeadingWhitespaceValid(int indentationAmount, int level, string whitespace)
801 {
802 // Strip off leading newlines; there can be an arbitrary number of these.
803 whitespace = whitespace.TrimStart(XDocumentNewLine);
804
805 var indentation = new string(' ', level * indentationAmount);
806
807 return whitespace == indentation;
808 }
809
810 /// <summary>
811 /// Fix the whitespace in a whitespace node.
812 /// </summary>
813 /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param>
814 /// <param name="level">The depth level of the desired whitespace.</param>
815 /// <param name="whitespace">The whitespace node to fix.</param>
816 private static void FixupWhitespace(int indentationAmount, int level, XText whitespace)
817 {
818 var value = new StringBuilder(whitespace.Value.Length);
819
820 // Keep any previous preceeding new lines.
821 var newlines = whitespace.Value.TakeWhile(c => c == XDocumentNewLine).Count();
822
823 // Ensure there is always at least one new line before the indentation.
824 value.Append(XDocumentNewLine, newlines == 0 ? 1 : newlines);
825
826 whitespace.Value = value.Append(' ', level * indentationAmount).ToString();
827 }
828
829 /// <summary>
830 /// Output an error message to the console.
831 /// </summary>
832 /// <param name="converterTestType">The type of converter test.</param>
833 /// <param name="node">The node that caused the error.</param>
834 /// <param name="message">Detailed error message.</param>
835 /// <param name="args">Additional formatted string arguments.</param>
836 /// <returns>Returns true indicating that action should be taken on this error, and false if it should be ignored.</returns>
837 private bool OnError(ConverterTestType converterTestType, XObject node, string message, params object[] args)
838 {
839 if (this.IgnoreErrors.Contains(converterTestType)) // ignore the error
840 {
841 return false;
842 }
843
844 // Increase the error count.
845 this.Errors++;
846
847 var sourceLine = (null == node) ? new SourceLineNumber(this.SourceFile ?? "wixcop.exe") : new SourceLineNumber(this.SourceFile, ((IXmlLineInfo)node).LineNumber);
848 var warning = this.ErrorsAsWarnings.Contains(converterTestType);
849 var display = String.Format(CultureInfo.CurrentCulture, message, args);
850
851 var msg = new Message(sourceLine, warning ? MessageLevel.Warning : MessageLevel.Error, (int)converterTestType, "{0} ({1})", display, converterTestType.ToString());
852
853 this.Messaging.Write(msg);
854
855 return true;
856 }
857
858 /// <summary>
859 /// Return an identifier based on passed file/directory name
860 /// </summary>
861 /// <param name="name">File/directory name to generate identifer from</param>
862 /// <returns>A version of the name that is a legal identifier.</returns>
863 /// <remarks>This is duplicated from WiX's Common class.</remarks>
864 private static string GetIdentifierFromName(string name)
865 {
866 var result = IllegalIdentifierCharacters.Replace(name, "_"); // replace illegal characters with "_".
867
868 // MSI identifiers must begin with an alphabetic character or an
869 // underscore. Prefix all other values with an underscore.
870 if (AddPrefix.IsMatch(name))
871 {
872 result = String.Concat("_", result);
873 }
874
875 return result;
876 }
877
878 private static string LowercaseFirstChar(string value)
879 {
880 if (!String.IsNullOrEmpty(value))
881 {
882 var c = Char.ToLowerInvariant(value[0]);
883 if (c != value[0])
884 {
885 var remainder = value.Length > 1 ? value.Substring(1) : String.Empty;
886 return c + remainder;
887 }
888 }
889
890 return value;
891 }
892
893 private static string UppercaseFirstChar(string value)
894 {
895 if (!String.IsNullOrEmpty(value))
896 {
897 var c = Char.ToUpperInvariant(value[0]);
898 if (c != value[0])
899 {
900 var remainder = value.Length > 1 ? value.Substring(1) : String.Empty;
901 return c + remainder;
902 }
903 }
904
905 return value;
906 }
907
908 private static bool TryGetInnerText(XElement element, out string value)
909 {
910 value = null;
911
912 var nodes = element.Nodes();
913
914 if (nodes.All(e => e.NodeType == XmlNodeType.Text || e.NodeType == XmlNodeType.CDATA))
915 {
916 value = String.Join(String.Empty, nodes.Cast<XText>().Select(TrimTextValue));
917 }
918
919 return !String.IsNullOrEmpty(value);
920 }
921
922 private static bool IsTextNode(XNode node, out XText text)
923 {
924 text = null;
925
926 if (node.NodeType == XmlNodeType.Text || node.NodeType == XmlNodeType.CDATA)
927 {
928 text = (XText)node;
929 }
930
931 return text != null;
932 }
933
934 private static void TrimLeadingText(XDocument document)
935 {
936 while (IsTextNode(document.Nodes().FirstOrDefault(), out var text))
937 {
938 text.Remove();
939 }
940 }
941
942 private static string TrimTextValue(XText text)
943 {
944 var value = text.Value;
945
946 if (String.IsNullOrEmpty(value))
947 {
948 return String.Empty;
949 }
950 else if (text.NodeType == XmlNodeType.CDATA && String.IsNullOrWhiteSpace(value))
951 {
952 return " ";
953 }
954
955 return value.Trim();
956 }
957
958 private static void RemoveChildren(XElement element)
959 {
960 var nodes = element.Nodes().ToList();
961 foreach (var node in nodes)
962 {
963 node.Remove();
964 }
965 }
966
967 /// <summary>
968 /// Converter test types. These are used to condition error messages down to warnings.
969 /// </summary>
970 private enum ConverterTestType
971 {
972 /// <summary>
973 /// Internal-only: displayed when a string cannot be converted to an ConverterTestType.
974 /// </summary>
975 ConverterTestTypeUnknown,
976
977 /// <summary>
978 /// Displayed when an XML loading exception has occurred.
979 /// </summary>
980 XmlException,
981
982 /// <summary>
983 /// Displayed when a file cannot be accessed; typically when trying to save back a fixed file.
984 /// </summary>
985 UnauthorizedAccessException,
986
987 /// <summary>
988 /// Displayed when the encoding attribute in the XML declaration is not 'UTF-8'.
989 /// </summary>
990 DeclarationEncodingWrong,
991
992 /// <summary>
993 /// Displayed when the XML declaration is missing from the source file.
994 /// </summary>
995 DeclarationMissing,
996
997 /// <summary>
998 /// Displayed when the whitespace preceding a CDATA node is wrong.
999 /// </summary>
1000 WhitespacePrecedingCDATAWrong,
1001
1002 /// <summary>
1003 /// Displayed when the whitespace preceding a node is wrong.
1004 /// </summary>
1005 WhitespacePrecedingNodeWrong,
1006
1007 /// <summary>
1008 /// Displayed when an element is not empty as it should be.
1009 /// </summary>
1010 NotEmptyElement,
1011
1012 /// <summary>
1013 /// Displayed when the whitespace following a CDATA node is wrong.
1014 /// </summary>
1015 WhitespaceFollowingCDATAWrong,
1016
1017 /// <summary>
1018 /// Displayed when the whitespace preceding an end element is wrong.
1019 /// </summary>
1020 WhitespacePrecedingEndElementWrong,
1021
1022 /// <summary>
1023 /// Displayed when the xmlns attribute is missing from the document element.
1024 /// </summary>
1025 XmlnsMissing,
1026
1027 /// <summary>
1028 /// Displayed when the xmlns attribute on the document element is wrong.
1029 /// </summary>
1030 XmlnsValueWrong,
1031
1032 /// <summary>
1033 /// Assign an identifier to a File element when on Id attribute is specified.
1034 /// </summary>
1035 AssignAnonymousFileId,
1036
1037 /// <summary>
1038 /// SuppressSignatureValidation attribute is deprecated and replaced with EnableSignatureValidation.
1039 /// </summary>
1040 SuppressSignatureValidationDeprecated,
1041
1042 /// <summary>
1043 /// WixCA Binary/@Id has been renamed to UtilCA.
1044 /// </summary>
1045 WixCABinaryIdRenamed,
1046
1047 /// <summary>
1048 /// QtExec custom actions have been renamed.
1049 /// </summary>
1050 QuietExecCustomActionsRenamed,
1051
1052 /// <summary>
1053 /// QtExecCmdTimeout was previously used for both CAQuietExec and CAQuietExec64. For WixQuietExec, use WixQuietExecCmdTimeout. For WixQuietExec64, use WixQuietExec64CmdTimeout.
1054 /// </summary>
1055 QtExecCmdTimeoutAmbiguous,
1056
1057 /// <summary>
1058 /// Directory/@ShortName may only be specified with Directory/@Name.
1059 /// </summary>
1060 AssignDirectoryNameFromShortName,
1061
1062 /// <summary>
1063 /// BootstrapperApplicationData attribute is deprecated and replaced with Unreal.
1064 /// </summary>
1065 BootstrapperApplicationDataDeprecated,
1066
1067 /// <summary>
1068 /// Inheritable is new and is now defaulted to 'yes' which is a change in behavior for all but children of CreateFolder.
1069 /// </summary>
1070 AssignPermissionExInheritable,
1071
1072 /// <summary>
1073 /// Column element's Category attribute is camel-case.
1074 /// </summary>
1075 ColumnCategoryCamelCase,
1076
1077 /// <summary>
1078 /// Column element's Modularize attribute is camel-case.
1079 /// </summary>
1080 ColumnModularizeCamelCase,
1081
1082 /// <summary>
1083 /// Inner text value should move to an attribute.
1084 /// </summary>
1085 InnerTextDeprecated,
1086
1087 /// <summary>
1088 /// Explicit auto-GUID unnecessary.
1089 /// </summary>
1090 AutoGuidUnnecessary,
1091
1092 /// <summary>
1093 /// Displayed when the XML declaration is present in the source file.
1094 /// </summary>
1095 DeclarationPresent,
1096 }
1097 }
1098}