MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Collections.Generic;5using System.Collections.ObjectModel;6using System.ComponentModel;7using System.Diagnostics;8using System.Globalization;9using System.IO;10using System.IO.Compression;11using System.Management.Automation.Configuration;12using System.Management.Automation.Internal;13using System.Net;14using System.Net.Http;15using System.Runtime.Serialization;16using System.Security;17using System.Text;18using System.Threading;19using System.Threading.Tasks;20using System.Xml;21using System.Xml.Schema;22 23using Microsoft.PowerShell.Commands;24using Microsoft.Win32;25 26namespace System.Management.Automation.Help27{28 /// <summary>29 /// Updatable help system exception.30 /// </summary>31 internal class UpdatableHelpSystemException : Exception32 {33 /// <summary>34 /// Class constructor.35 /// </summary>36 /// <param name="errorId">FullyQualifiedErrorId.</param>37 /// <param name="message">Exception message.</param>38 /// <param name="cat">Category.</param>39 /// <param name="targetObject">Target object.</param>40 /// <param name="innerException">Inner exception.</param>41 internal UpdatableHelpSystemException(string errorId, string message, ErrorCategory cat, object targetObject, Exception innerException)42 : base(message, innerException)43 {44 FullyQualifiedErrorId = errorId;45 ErrorCategory = cat;46 TargetObject = targetObject;47 }48 49#if !CORECLR50 /// <summary>51 /// Class constructor.52 /// </summary>53 /// <param name="serializationInfo">Serialization info.</param>54 /// <param name="streamingContext">Streaming context.</param>55 protected UpdatableHelpSystemException(SerializationInfo serializationInfo, StreamingContext streamingContext)56 : base(serializationInfo, streamingContext)57 {58 }59#endif60 61 /// <summary>62 /// Fully qualified error id.63 /// </summary>64 internal string FullyQualifiedErrorId { get; }65 66 /// <summary>67 /// Error category.68 /// </summary>69 internal ErrorCategory ErrorCategory { get; }70 71 /// <summary>72 /// Target object.73 /// </summary>74 internal object TargetObject { get; }75 }76 77 /// <summary>78 /// Exception context.79 /// </summary>80 internal class UpdatableHelpExceptionContext81 {82 /// <summary>83 /// Class constructor.84 /// </summary>85 /// <param name="exception">Exception to wrap.</param>86 internal UpdatableHelpExceptionContext(UpdatableHelpSystemException exception)87 {88 Exception = exception;89 Modules = new HashSet<string>(StringComparer.OrdinalIgnoreCase);90 Cultures = new HashSet<string>(StringComparer.OrdinalIgnoreCase);91 }92 93 /// <summary>94 /// A list of modules.95 /// </summary>96 internal HashSet<string> Modules { get; set; }97 98 /// <summary>99 /// A list of UI cultures.100 /// </summary>101 internal HashSet<string> Cultures { get; set; }102 103 /// <summary>104 /// Gets the help system exception.105 /// </summary>106 internal UpdatableHelpSystemException Exception { get; }107 108 /// <summary>109 /// Creates an error record from this context.110 /// </summary>111 /// <param name="commandType">Command type.</param>112 /// <returns>Error record.</returns>113 internal ErrorRecord CreateErrorRecord(UpdatableHelpCommandType commandType)114 {115 Debug.Assert(Modules.Count != 0);116 117 return new ErrorRecord(new Exception(GetExceptionMessage(commandType)), Exception.FullyQualifiedErrorId, Exception.ErrorCategory,118 Exception.TargetObject);119 }120 121 /// <summary>122 /// Gets the exception message.123 /// </summary>124 /// <param name="commandType"></param>125 /// <returns></returns>126 internal string GetExceptionMessage(UpdatableHelpCommandType commandType)127 {128 string message = string.Empty;129 SortedSet<string> sortedModules = new SortedSet<string>(Modules, StringComparer.CurrentCultureIgnoreCase);130 SortedSet<string> sortedCultures = new SortedSet<string>(Cultures, StringComparer.CurrentCultureIgnoreCase);131 string modules = string.Join(", ", sortedModules);132 string cultures = string.Join(", ", sortedCultures);133 134 if (commandType == UpdatableHelpCommandType.UpdateHelpCommand)135 {136 if (Cultures.Count == 0)137 {138 message = StringUtil.Format(HelpDisplayStrings.FailedToUpdateHelpForModule, modules, Exception.Message);139 }140 else141 {142 message = StringUtil.Format(HelpDisplayStrings.FailedToUpdateHelpForModuleWithCulture, modules, cultures, Exception.Message);143 }144 }145 else146 {147 if (Cultures.Count == 0)148 {149 message = StringUtil.Format(HelpDisplayStrings.FailedToSaveHelpForModule, modules, Exception.Message);150 }151 else152 {153 message = StringUtil.Format(HelpDisplayStrings.FailedToSaveHelpForModuleWithCulture, modules, cultures, Exception.Message);154 }155 }156 157 return message;158 }159 }160 161 /// <summary>162 /// Enumeration showing Update or Save help.163 /// </summary>164 internal enum UpdatableHelpCommandType165 {166 UnknownCommand = 0,167 UpdateHelpCommand = 1,168 SaveHelpCommand = 2169 }170 171 /// <summary>172 /// Progress event arguments.173 /// </summary>174 internal class UpdatableHelpProgressEventArgs : EventArgs175 {176 /// <summary>177 /// Class constructor.178 /// </summary>179 /// <param name="moduleName">Module name.</param>180 /// <param name="status">Progress status.</param>181 /// <param name="percent">Progress percentage.</param>182 internal UpdatableHelpProgressEventArgs(string moduleName, string status, int percent)183 {184 Debug.Assert(!string.IsNullOrEmpty(status));185 186 CommandType = UpdatableHelpCommandType.UnknownCommand;187 ProgressStatus = status;188 ProgressPercent = percent;189 ModuleName = moduleName;190 }191 192 /// <summary>193 /// Class constructor.194 /// </summary>195 /// <param name="moduleName">Module name.</param>196 /// <param name="type">Command type.</param>197 /// <param name="status">Progress status.</param>198 /// <param name="percent">Progress percentage.</param>199 internal UpdatableHelpProgressEventArgs(string moduleName, UpdatableHelpCommandType type, string status, int percent)200 {201 Debug.Assert(!string.IsNullOrEmpty(status));202 203 CommandType = type;204 ProgressStatus = status;205 ProgressPercent = percent;206 ModuleName = moduleName;207 }208 209 /// <summary>210 /// Progress status.211 /// </summary>212 internal string ProgressStatus { get; }213 214 /// <summary>215 /// Progress percentage.216 /// </summary>217 internal int ProgressPercent { get; }218 219 /// <summary>220 /// Module name.221 /// </summary>222 internal string ModuleName { get; }223 224 /// <summary>225 /// Command type.226 /// </summary>227 internal UpdatableHelpCommandType CommandType { get; set; }228 }229 230 /// <summary>231 /// This class implements the Updatable Help System common operations.232 /// </summary>233 internal class UpdatableHelpSystem : IDisposable234 {235 private readonly TimeSpan _defaultTimeout;236 private readonly Collection<UpdatableHelpProgressEventArgs> _progressEvents;237 private bool _stopping;238 private readonly object _syncObject;239 private readonly UpdatableHelpCommandBase _cmdlet;240 private readonly CancellationTokenSource _cancelTokenSource;241 242 internal HttpClient HttpClient { get; }243 244 internal bool UseDefaultCredentials;245 246 internal string CurrentModule { get; set; }247 248 /// <summary>249 /// Class constructor.250 /// </summary>251 internal UpdatableHelpSystem(UpdatableHelpCommandBase cmdlet, bool useDefaultCredentials)252 {253 HttpClient = new HttpClient();254 _defaultTimeout = new TimeSpan(0, 0, 30);255 _progressEvents = new Collection<UpdatableHelpProgressEventArgs>();256 Errors = new Collection<Exception>();257 _stopping = false;258 _syncObject = new object();259 _cmdlet = cmdlet;260 _cancelTokenSource = new CancellationTokenSource();261 262 UseDefaultCredentials = useDefaultCredentials;263 264#if !CORECLR265 WebClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(HandleDownloadProgressChanged);266 WebClient.DownloadFileCompleted += new AsyncCompletedEventHandler(HandleDownloadFileCompleted);267#endif268 }269 270 /// <summary>271 /// Disposes the help system.272 /// </summary>273 public void Dispose()274 {275#if !CORECLR276 _completionEvent.Dispose();277#endif278 _cancelTokenSource.Dispose();279 HttpClient.Dispose();280 GC.SuppressFinalize(this);281 }282 283 /// <summary>284 /// Help system errors.285 /// </summary>286 internal Collection<Exception> Errors { get; }287 288 /// <summary>289 /// Gets the current UIculture (includes the fallback chain)290 /// </summary>291 /// <returns>A list of cultures.</returns>292 internal IEnumerable<string> GetCurrentUICulture()293 {294 CultureInfo culture = CultureInfo.CurrentUICulture;295 296 // Allow tests to override system culture297 if (InternalTestHooks.CurrentUICulture != null)298 {299 culture = InternalTestHooks.CurrentUICulture;300 }301 302 return CultureSpecificUpdatableHelp.GetCultureFallbackChain(culture);303 }304 305 #region Help Metadata Retrieval306 307 /// <summary>308 /// Gets an internal help URI.309 /// </summary>310 /// <param name="module">Internal module information.</param>311 /// <param name="culture">Help content culture.</param>312 /// <returns>Internal help uri representation.</returns>313 internal UpdatableHelpUri GetHelpInfoUri(UpdatableHelpModuleInfo module, CultureInfo culture)314 {315 return new UpdatableHelpUri(module.ModuleName, module.ModuleGuid, culture, ResolveUri(module.HelpInfoUri, false));316 }317 318 /// <summary>319 /// Gets the HelpInfo xml from the given URI.320 /// </summary>321 /// <param name="commandType">Command type.</param>322 /// <param name="uri">HelpInfo URI.</param>323 /// <param name="moduleName">Module name.</param>324 /// <param name="moduleGuid">Module GUID.</param>325 /// <param name="culture">Current UI culture.</param>326 /// <returns>HelpInfo object.</returns>327 internal UpdatableHelpInfo GetHelpInfo(UpdatableHelpCommandType commandType, string uri, string moduleName, Guid moduleGuid, string culture)328 {329 try330 {331 OnProgressChanged(this, new UpdatableHelpProgressEventArgs(CurrentModule, commandType, StringUtil.Format(332 HelpDisplayStrings.UpdateProgressLocating), 0));333 334 string xml;335 using (HttpClientHandler handler = new HttpClientHandler())336 {337 handler.UseDefaultCredentials = UseDefaultCredentials;338 using (HttpClient client = new HttpClient(handler))339 {340 client.Timeout = _defaultTimeout;341 Task<string> responseBody = client.GetStringAsync(uri);342 xml = responseBody.Result;343 if (responseBody.Exception != null)344 {345 return null;346 }347 }348 }349 350 UpdatableHelpInfo helpInfo = CreateHelpInfo(xml, moduleName, moduleGuid,351 currentCulture: culture, pathOverride: null, verbose: true,352 shouldResolveUri: true, ignoreValidationException: false);353 354 return helpInfo;355 }356#if !CORECLR357 catch (WebException)358 {359 return null;360 }361#endif362 finally363 {364 OnProgressChanged(this, new UpdatableHelpProgressEventArgs(CurrentModule, commandType, StringUtil.Format(365 HelpDisplayStrings.UpdateProgressLocating), 100));366 }367 }368 369 /// <summary>370 /// Sends a standard HTTP request to get the resolved URI (potential FwLinks)371 /// </summary>372 /// <param name="baseUri">Base URI.</param>373 /// <param name="verbose"></param>374 /// <returns>Resolved URI.</returns>375 private string ResolveUri(string baseUri, bool verbose)376 {377 Debug.Assert(!string.IsNullOrEmpty(baseUri));378 379 // Directory.Exists checks if baseUri is a network drive or380 // a local directory. If baseUri is local, we don't need to resolve it.381 //382 // The / check works because all of our fwlinks must resolve383 // to a remote virtual directory. I think HTTP always appends /384 // in reference to a directory.385 // Like if you send a request to www.technet.com/powershell you will get386 // a 301/203 response with the response URI set to www.technet.com/powershell/387 //388 if (Directory.Exists(baseUri) || baseUri.EndsWith('/'))389 {390 if (verbose)391 {392 _cmdlet.WriteVerbose(StringUtil.Format(RemotingErrorIdStrings.URIRedirectWarningToHost, baseUri));393 }394 395 return baseUri;396 }397 398 if (verbose)399 {400 _cmdlet.WriteVerbose(StringUtil.Format(HelpDisplayStrings.UpdateHelpResolveUriVerbose, baseUri));401 }402 403 string uri = baseUri;404 405 try406 {407 // We only allow 10 redirections408 for (int i = 0; i < 10; i++)409 {410 if (_stopping)411 {412 return uri;413 }414 415 using (HttpClientHandler handler = new HttpClientHandler())416 {417 handler.AllowAutoRedirect = false;418 handler.UseDefaultCredentials = UseDefaultCredentials;419 using (HttpClient client = new HttpClient(handler))420 {421 client.Timeout = new TimeSpan(0, 0, 30); // Set 30 second timeout422 // codeql[cs/ssrf] - This is expected Poweshell behavior and the user assumes trust for the module they download and any URIs it references. The URIs are also not executables or scripts that would be invoked by this method.423 Task<HttpResponseMessage> responseMessage = client.GetAsync(uri);424 using (HttpResponseMessage response = responseMessage.Result)425 {426 if (response.StatusCode == HttpStatusCode.Found ||427 response.StatusCode == HttpStatusCode.Redirect ||428 response.StatusCode == HttpStatusCode.Moved ||429 response.StatusCode == HttpStatusCode.MovedPermanently)430 {431 Uri responseUri = response.Headers.Location;432 433 if (responseUri.IsAbsoluteUri)434 {435 uri = responseUri.ToString();436 }437 else438 {439 Uri originalAbs = new Uri(uri);440 uri = uri.Replace(originalAbs.AbsolutePath, responseUri.ToString());441 }442 443 uri = uri.Trim();444 445 if (verbose)446 {447 _cmdlet.WriteVerbose(StringUtil.Format(RemotingErrorIdStrings.URIRedirectWarningToHost, uri));448 }449 450 if (uri.EndsWith('/'))451 {452 return uri;453 }454 }455 else if (response.StatusCode == HttpStatusCode.OK)456 {457 if (uri.EndsWith('/'))458 {459 return uri;460 }461 else462 {463 throw new UpdatableHelpSystemException("InvalidHelpInfoUri", StringUtil.Format(HelpDisplayStrings.InvalidHelpInfoUri, uri),464 ErrorCategory.InvalidOperation, null, null);465 }466 }467 }468 }469 }470 }471 }472 catch (UriFormatException e)473 {474 throw new UpdatableHelpSystemException("InvalidUriFormat", e.Message, ErrorCategory.InvalidData, null, e);475 }476 477 throw new UpdatableHelpSystemException("TooManyRedirections", StringUtil.Format(HelpDisplayStrings.TooManyRedirections),478 ErrorCategory.InvalidOperation, null, null);479 }480 481 /// <summary>482 /// HelpInfo.xml schema.483 /// </summary>484 private const string HelpInfoXmlSchema = @"<?xml version=""1.0"" encoding=""utf-8""?>485 <xs:schema attributeFormDefault=""unqualified"" elementFormDefault=""qualified""486 targetNamespace=""http://schemas.microsoft.com/powershell/help/2010/05"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">487 <xs:element name=""HelpInfo"">488 <xs:complexType>489 <xs:sequence>490 <xs:element name=""HelpContentURI"" type=""xs:anyURI"" minOccurs=""1"" maxOccurs=""1"" />491 <xs:element name=""SupportedUICultures"" minOccurs=""1"" maxOccurs=""1"">492 <xs:complexType>493 <xs:sequence>494 <xs:element name=""UICulture"" minOccurs=""1"" maxOccurs=""unbounded"">495 <xs:complexType>496 <xs:sequence>497 <xs:element name=""UICultureName"" type=""xs:language"" minOccurs=""1"" maxOccurs=""1"" />498 <xs:element name=""UICultureVersion"" type=""xs:string"" minOccurs=""1"" maxOccurs=""1"" />499 </xs:sequence>500 </xs:complexType>501 </xs:element>502 </xs:sequence>503 </xs:complexType>504 </xs:element>505 </xs:sequence>506 </xs:complexType>507 </xs:element>508 </xs:schema>";509 510 private const string HelpInfoXmlNamespace = "http://schemas.microsoft.com/powershell/help/2010/05";511 private const string HelpInfoXmlValidationFailure = "HelpInfoXmlValidationFailure";512 private const string MamlXmlNamespace = "http://schemas.microsoft.com/maml/2004/10";513 private const string CommandXmlNamespace = "http://schemas.microsoft.com/maml/dev/command/2004/10";514 private const string DscResourceXmlNamespace = "http://schemas.microsoft.com/maml/dev/dscResource/2004/10";515 516 /// <summary>517 /// Creates a HelpInfo object.518 /// </summary>519 /// <param name="xml">XML text.</param>520 /// <param name="moduleName">Module name.</param>521 /// <param name="moduleGuid">Module GUID.</param>522 /// <param name="currentCulture">Current UI cultures.</param>523 /// <param name="pathOverride">Overrides the path contained within HelpInfo.xml.</param>524 /// <param name="verbose"></param>525 /// <param name="shouldResolveUri">526 /// Resolve the uri retrieved from the <paramref name="xml"/> content. The uri is resolved527 /// to handle redirections if any.528 /// </param>529 /// <param name="ignoreValidationException">Ignore the xsd validation exception and return null in such case.</param>530 /// <returns>HelpInfo object.</returns>531 internal UpdatableHelpInfo CreateHelpInfo(string xml, string moduleName, Guid moduleGuid,532 string currentCulture, string pathOverride, bool verbose, bool shouldResolveUri, bool ignoreValidationException)533 {534 XmlDocument document = null;535 try536 {537 document = CreateValidXmlDocument(xml, HelpInfoXmlNamespace, HelpInfoXmlSchema,538 new ValidationEventHandler(HelpInfoValidationHandler),539 true);540 }541 catch (UpdatableHelpSystemException e)542 {543 if (ignoreValidationException && HelpInfoXmlValidationFailure.Equals(e.FullyQualifiedErrorId, StringComparison.Ordinal))544 {545 return null;546 }547 548 throw;549 }550 catch (XmlException e)551 {552 if (ignoreValidationException)553 {554 return null;555 }556 557 throw new UpdatableHelpSystemException(HelpInfoXmlValidationFailure,558 e.Message, ErrorCategory.InvalidData, null, e);559 }560 561 string uri = pathOverride;562 string unresolvedUri = document["HelpInfo"]["HelpContentURI"].InnerText;563 564 if (string.IsNullOrEmpty(pathOverride))565 {566 if (shouldResolveUri)567 {568 uri = ResolveUri(unresolvedUri, verbose);569 }570 else571 {572 uri = unresolvedUri;573 }574 }575 576 XmlNodeList cultures = document["HelpInfo"]["SupportedUICultures"].ChildNodes;577 578 CultureSpecificUpdatableHelp[] updatableHelpItem = new CultureSpecificUpdatableHelp[cultures.Count];579 580 for (int i = 0; i < cultures.Count; i++)581 {582 updatableHelpItem[i] = new CultureSpecificUpdatableHelp(583 new CultureInfo(cultures[i]["UICultureName"].InnerText),584 new Version(cultures[i]["UICultureVersion"].InnerText));585 }586 587 UpdatableHelpInfo helpInfo = new UpdatableHelpInfo(unresolvedUri, updatableHelpItem);588 589 if (!string.IsNullOrEmpty(currentCulture))590 {591 for (int i = 0; i < updatableHelpItem.Length; i++)592 {593 if (updatableHelpItem[i].IsCultureSupported(currentCulture))594 {595 helpInfo.HelpContentUriCollection.Add(new UpdatableHelpUri(moduleName, moduleGuid, updatableHelpItem[i].Culture, uri));596 }597 }598 }599 600 if (!string.IsNullOrEmpty(currentCulture) && helpInfo.HelpContentUriCollection.Count == 0)601 {602 // throw exception603 throw new UpdatableHelpSystemException("HelpCultureNotSupported",604 StringUtil.Format(HelpDisplayStrings.HelpCultureNotSupported,605 currentCulture, helpInfo.GetSupportedCultures()), ErrorCategory.InvalidOperation, null, null);606 }607 608 return helpInfo;609 }610 611 /// <summary>612 /// Creates a valid xml document.613 /// </summary>614 /// <param name="xml">Input xml.</param>615 /// <param name="ns">Schema namespace.</param>616 /// <param name="schema">Xml schema.</param>617 /// <param name="handler">Validation event handler.</param>618 /// <param name="helpInfo">HelpInfo or HelpContent?</param>619 private static XmlDocument CreateValidXmlDocument(string xml, string ns, string schema, ValidationEventHandler handler,620 bool helpInfo)621 {622 XmlReaderSettings settings = new XmlReaderSettings();623 624 settings.Schemas.Add(ns, new XmlTextReader(new StringReader(schema)));625 settings.ValidationType = ValidationType.Schema;626 627 XmlReader reader = XmlReader.Create(new StringReader(xml), settings);628 XmlDocument document = new XmlDocument();629 630 try631 {632 document.Load(reader);633 document.Validate(handler);634 }635 catch (XmlSchemaValidationException e)636 {637 if (helpInfo)638 {639 throw new UpdatableHelpSystemException(HelpInfoXmlValidationFailure,640 StringUtil.Format(HelpDisplayStrings.HelpInfoXmlValidationFailure, e.Message),641 ErrorCategory.InvalidData, null, e);642 }643 else644 {645 throw new UpdatableHelpSystemException("HelpContentXmlValidationFailure",646 StringUtil.Format(HelpDisplayStrings.HelpContentXmlValidationFailure, e.Message),647 ErrorCategory.InvalidData, null, e);648 }649 }650 651 return document;652 }653 654 /// <summary>655 /// Handles HelpInfo XML validation events.656 /// </summary>657 /// <param name="sender">Event sender.</param>658 /// <param name="arg">Event arguments.</param>659 private void HelpInfoValidationHandler(object sender, ValidationEventArgs arg)660 {661 switch (arg.Severity)662 {663 case XmlSeverityType.Error:664 {665 throw new UpdatableHelpSystemException(HelpInfoXmlValidationFailure,666 StringUtil.Format(HelpDisplayStrings.HelpInfoXmlValidationFailure),667 ErrorCategory.InvalidData, null, arg.Exception);668 }669 case XmlSeverityType.Warning:670 break;671 }672 }673 674 /// <summary>675 /// Handles Help content MAML validation events.676 /// </summary>677 /// <param name="sender">Event sender.</param>678 /// <param name="arg">Event arguments.</param>679 private void HelpContentValidationHandler(object sender, ValidationEventArgs arg)680 {681 switch (arg.Severity)682 {683 case XmlSeverityType.Error:684 {685 throw new UpdatableHelpSystemException("HelpContentXmlValidationFailure",686 StringUtil.Format(HelpDisplayStrings.HelpContentXmlValidationFailure),687 ErrorCategory.InvalidData, null, arg.Exception);688 }689 case XmlSeverityType.Warning:690 break;691 }692 }693 694 #endregion695 696 #region Help Content Retrieval697 698 /// <summary>699 /// Cancels all asynchronous download operations.700 /// </summary>701 internal void CancelDownload()702 {703 _cancelTokenSource.Cancel();704 _stopping = true;705 }706 707 /// <summary>708 /// Downloads and installs help content.709 /// </summary>710 /// <param name="commandType">Command type.</param>711 /// <param name="context">Execution context.</param>712 /// <param name="destPaths">Destination paths.</param>713 /// <param name="fileName">File names.</param>714 /// <param name="culture">Culture to update.</param>715 /// <param name="helpContentUri">Help content uri.</param>716 /// <param name="xsdPath">Path of the maml XSDs.</param>717 /// <param name="installed">Files installed.</param>718 /// <returns>True if the operation succeeded, false if not.</returns>719 internal bool DownloadAndInstallHelpContent(UpdatableHelpCommandType commandType, ExecutionContext context, Collection<string> destPaths,720 string fileName, CultureInfo culture, string helpContentUri, string xsdPath, out Collection<string> installed)721 {722 if (_stopping)723 {724 installed = new Collection<string>();725 return false;726 }727 728 string cache = Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));729 730 if (!DownloadHelpContent(commandType, cache, helpContentUri, fileName, culture.Name))731 {732 installed = new Collection<string>();733 return false;734 }735 736 InstallHelpContent(commandType, context, cache, destPaths, fileName, cache, culture, xsdPath, out installed);737 738 return true;739 }740 741 /// <summary>742 /// Downloads the help content.743 /// </summary>744 /// <param name="commandType">Command type.</param>745 /// <param name="path">Destination path.</param>746 /// <param name="helpContentUri">Help content uri.</param>747 /// <param name="fileName">Combined file name.</param>748 /// <param name="culture">Culture name.</param>749 /// <returns>True if the operation succeeded, false if not.</returns>750 internal bool DownloadHelpContent(UpdatableHelpCommandType commandType, string path, string helpContentUri, string fileName, string culture)751 {752 if (_stopping)753 {754 return false;755 }756 757 if (!Directory.Exists(path))758 {759 Directory.CreateDirectory(path);760 }761 762 OnProgressChanged(this, new UpdatableHelpProgressEventArgs(CurrentModule, commandType, StringUtil.Format(763 HelpDisplayStrings.UpdateProgressConnecting), 0));764 765 string uri = helpContentUri + fileName;766 767 return DownloadHelpContentHttpClient(uri, Path.Combine(path, fileName), commandType);768 }769 770 /// <summary>771 /// Downloads the help content and saves it to a directory.772 /// </summary>773 /// <param name="uri"></param>774 /// <param name="fileName"></param>775 /// <param name="commandType"></param>776 /// <returns></returns>777 private bool DownloadHelpContentHttpClient(string uri, string fileName, UpdatableHelpCommandType commandType)778 {779 // TODO: Was it intentional for them to remove IDisposable from Task?780 using (HttpClientHandler handler = new HttpClientHandler())781 {782 handler.AllowAutoRedirect = false;783 handler.UseDefaultCredentials = UseDefaultCredentials;784 using (HttpClient client = new HttpClient(handler))785 {786 client.Timeout = _defaultTimeout;787 // codeql[cs/ssrf] - This is expected Poweshell behavior and the user assumes trust for the module they download and any URIs it references. The URIs are also not executables or scripts that would be invoked by this method.788 Task<HttpResponseMessage> responseMsg = client.GetAsync(new Uri(uri), _cancelTokenSource.Token);789 790 // TODO: Should I use a continuation to write the stream to a file?791 responseMsg.Wait();792 793 if (_stopping)794 {795 return true;796 }797 798 if (!responseMsg.IsCanceled)799 {800 if (responseMsg.Exception != null)801 {802 Errors.Add(new UpdatableHelpSystemException("HelpContentNotFound",803 StringUtil.Format(HelpDisplayStrings.HelpContentNotFound),804 ErrorCategory.ResourceUnavailable, null, responseMsg.Exception));805 }806 else807 {808 lock (_syncObject)809 {810 _progressEvents.Add(new UpdatableHelpProgressEventArgs(CurrentModule, StringUtil.Format(811 HelpDisplayStrings.UpdateProgressDownloading), 100));812 }813 814 // Write the stream to the specified file to achieve functional parity with WebClient.DownloadFileAsync().815 HttpResponseMessage response = responseMsg.Result;816 if (response.IsSuccessStatusCode)817 {818 WriteResponseToFile(response, fileName);819 }820 else821 {822 Errors.Add(new UpdatableHelpSystemException("HelpContentNotFound",823 StringUtil.Format(HelpDisplayStrings.HelpContentNotFound),824 ErrorCategory.ResourceUnavailable, null, responseMsg.Exception));825 }826 }827 }828 829 SendProgressEvents(commandType);830 }831 }832 833 return (Errors.Count == 0);834 }835 836 /// <summary>837 /// Writes the content of an HTTP response to the specified file.838 /// </summary>839 /// <param name="response"></param>840 /// <param name="fileName"></param>841 private void WriteResponseToFile(HttpResponseMessage response, string fileName)842 {843 // TODO: Settings to use? FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite844 using (FileStream downloadedFileStream = new FileStream(fileName, FileMode.Create, FileAccess.Write))845 {846 Task copyStreamOp = response.Content.CopyToAsync(downloadedFileStream);847 copyStreamOp.Wait();848 if (copyStreamOp.Exception != null)849 {850 Errors.Add(copyStreamOp.Exception);851 }852 }853 }854 855 private void SendProgressEvents(UpdatableHelpCommandType commandType)856 {857 // Send progress events858 lock (_syncObject)859 {860 if (_progressEvents.Count > 0)861 {862 foreach (UpdatableHelpProgressEventArgs evt in _progressEvents)863 {864 evt.CommandType = commandType;865 866 OnProgressChanged(this, evt);867 }868 869 _progressEvents.Clear();870 }871 }872 }873 874 /// <summary>875 /// Installs HelpInfo.xml.876 /// </summary>877 /// <param name="moduleName"></param>878 /// <param name="moduleGuid"></param>879 /// <param name="culture">Culture updated.</param>880 /// <param name="version">Version updated.</param>881 /// <param name="contentUri">Help content uri.</param>882 /// <param name="destPath">Destination name.</param>883 /// <param name="fileName">Combined file name.</param>884 /// <param name="force">Forces the file to copy.</param>885 internal void GenerateHelpInfo(string moduleName, Guid moduleGuid, string contentUri, string culture, Version version, string destPath, string fileName, bool force)886 {887 Debug.Assert(Directory.Exists(destPath));888 889 if (_stopping)890 {891 return;892 }893 894 string destHelpInfo = Path.Combine(destPath, fileName);895 896 if (force)897 {898 RemoveReadOnly(destHelpInfo);899 }900 901 UpdatableHelpInfo oldHelpInfo = null;902 string xml = UpdatableHelpSystem.LoadStringFromPath(_cmdlet, destHelpInfo, null);903 904 if (xml != null)905 {906 // constructing the helpinfo object from previous update help log xml..907 // no need to resolve the uri's in this case.908 oldHelpInfo = CreateHelpInfo(xml, moduleName, moduleGuid, currentCulture: null, pathOverride: null,909 verbose: false, shouldResolveUri: false, ignoreValidationException: force);910 }911 912 using (FileStream file = new FileStream(destHelpInfo, FileMode.Create, FileAccess.Write))913 {914 XmlWriterSettings settings = new XmlWriterSettings();915 settings.Encoding = Encoding.UTF8;916 settings.Indent = true; // Default indentation is two spaces917 using (XmlWriter writer = XmlWriter.Create(file, settings))918 {919 writer.WriteStartDocument();920 writer.WriteStartElement("HelpInfo", "http://schemas.microsoft.com/powershell/help/2010/05");921 922 writer.WriteStartElement("HelpContentURI");923 writer.WriteValue(contentUri);924 writer.WriteEndElement();925 926 writer.WriteStartElement("SupportedUICultures");927 928 bool found = false;929 930 if (oldHelpInfo != null)931 {932 foreach (CultureSpecificUpdatableHelp oldInfo in oldHelpInfo.UpdatableHelpItems)933 {934 if (oldInfo.Culture.Name.Equals(culture, StringComparison.OrdinalIgnoreCase))935 {936 if (oldInfo.Version.Equals(version))937 {938 writer.WriteStartElement("UICulture");939 writer.WriteStartElement("UICultureName");940 writer.WriteValue(oldInfo.Culture.Name);941 writer.WriteEndElement();942 writer.WriteStartElement("UICultureVersion");943 writer.WriteValue(oldInfo.Version.ToString());944 writer.WriteEndElement();945 writer.WriteEndElement();946 }947 else948 {949 writer.WriteStartElement("UICulture");950 writer.WriteStartElement("UICultureName");951 writer.WriteValue(culture);952 writer.WriteEndElement();953 writer.WriteStartElement("UICultureVersion");954 writer.WriteValue(version.ToString());955 writer.WriteEndElement();956 writer.WriteEndElement();957 }958 959 found = true;960 }961 else962 {963 writer.WriteStartElement("UICulture");964 writer.WriteStartElement("UICultureName");965 writer.WriteValue(oldInfo.Culture.Name);966 writer.WriteEndElement();967 writer.WriteStartElement("UICultureVersion");968 writer.WriteValue(oldInfo.Version.ToString());969 writer.WriteEndElement();970 writer.WriteEndElement();971 }972 }973 }974 975 if (!found)976 {977 writer.WriteStartElement("UICulture");978 writer.WriteStartElement("UICultureName");979 writer.WriteValue(culture);980 writer.WriteEndElement();981 writer.WriteStartElement("UICultureVersion");982 writer.WriteValue(version.ToString());983 writer.WriteEndElement();984 writer.WriteEndElement();985 }986 987 writer.WriteEndElement();988 writer.WriteEndDocument();989 }990 }991 }992 993 /// <summary>994 /// Removes the read only attribute.995 /// </summary>996 /// <param name="path"></param>997 private static void RemoveReadOnly(string path)998 {999 if (File.Exists(path))1000 {1001 FileAttributes attributes = File.GetAttributes(path);1002 1003 if ((attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)1004 {1005 attributes &= ~FileAttributes.ReadOnly;1006 File.SetAttributes(path, attributes);1007 }1008 }1009 }1010 1011 /// <summary>1012 /// Installs (unzips) the help content.1013 /// </summary>1014 /// <param name="commandType">Command type.</param>1015 /// <param name="context">Execution context.</param>1016 /// <param name="sourcePath">Source directory.</param>1017 /// <param name="destPaths">Destination paths.</param>1018 /// <param name="fileName">Help content file name.</param>1019 /// <param name="tempPath">Temporary path.</param>1020 /// <param name="culture">Current culture.</param>1021 /// <param name="xsdPath">Path of the maml XSDs.</param>1022 /// <param name="installed">Files installed.</param>1023 /// <remarks>1024 /// Directory pointed by <paramref name="tempPath"/> (if any) will be deleted.1025 /// </remarks>1026 internal void InstallHelpContent(UpdatableHelpCommandType commandType, ExecutionContext context, string sourcePath,1027 Collection<string> destPaths, string fileName, string tempPath, CultureInfo culture, string xsdPath,1028 out Collection<string> installed)1029 {1030 installed = new Collection<string>();1031 1032 if (_stopping)1033 {1034 installed = new Collection<string>();1035 return;1036 }1037 1038 // These paths must exist1039 if (!Directory.Exists(tempPath))1040 {1041 Directory.CreateDirectory(tempPath);1042 }1043 1044 Debug.Assert(destPaths.Count > 0);1045 1046 try1047 {1048 OnProgressChanged(this, new UpdatableHelpProgressEventArgs(CurrentModule, commandType, StringUtil.Format(1049 HelpDisplayStrings.UpdateProgressInstalling), 0));1050 1051 string combinedSourcePath = GetFilePath(Path.Combine(sourcePath, fileName));1052 1053 if (string.IsNullOrEmpty(combinedSourcePath))1054 {1055 throw new UpdatableHelpSystemException("HelpContentNotFound", StringUtil.Format(HelpDisplayStrings.HelpContentNotFound),1056 ErrorCategory.ResourceUnavailable, null, null);1057 }1058 1059 string combinedTempPath = Path.Combine(tempPath, Path.GetFileNameWithoutExtension(fileName));1060 1061 if (Directory.Exists(combinedTempPath))1062 {1063 Directory.Delete(combinedTempPath, true);1064 }1065 1066 bool needToCopy = true;1067 UnzipHelpContent(context, combinedSourcePath, combinedTempPath, out needToCopy);1068 if (needToCopy)1069 {1070 ValidateAndCopyHelpContent(combinedTempPath, destPaths, culture.Name, xsdPath, out installed);1071 }1072 }1073 finally1074 {1075 OnProgressChanged(this, new UpdatableHelpProgressEventArgs(CurrentModule, commandType, StringUtil.Format(1076 HelpDisplayStrings.UpdateProgressInstalling), 100));1077 1078 try1079 {1080 if (Directory.Exists(tempPath))1081 {1082 Directory.Delete(tempPath);1083 }1084 }1085 catch (IOException) { }1086 catch (UnauthorizedAccessException) { }1087 catch (ArgumentException) { }1088 }1089 }1090 1091#if UNIX1092 private static bool ExpandArchive(string source, string destination)1093 {1094 bool successfulDecompression = false;1095 1096 try1097 {1098 using (ZipArchive zipArchive = ZipFile.Open(source, ZipArchiveMode.Read))1099 {1100 zipArchive.ExtractToDirectory(destination);1101 successfulDecompression = true;1102 }1103 }1104 catch (ArgumentException) { }1105 catch (InvalidDataException) { }1106 catch (NotSupportedException) { }1107 catch (FileNotFoundException) { }1108 catch (IOException) { }1109 catch (SecurityException) { }1110 catch (UnauthorizedAccessException) { }1111 catch (ObjectDisposedException) { }1112 1113 return successfulDecompression;1114 }1115#endif1116 1117 /// <summary>1118 /// Unzips to help content to a given location.1119 /// </summary>1120 /// <param name="context">Execution context.</param>1121 /// <param name="srcPath">Source path.</param>1122 /// <param name="destPath">Destination path.</param>1123 /// <param name="needToCopy">Is set to false if we find a single file placeholder.txt in cab. This means we no longer need to install help files.</param>1124 private static void UnzipHelpContent(ExecutionContext context, string srcPath, string destPath, out bool needToCopy)1125 {1126 needToCopy = true;1127 1128 if (!Directory.Exists(destPath))1129 {1130 Directory.CreateDirectory(destPath);1131 }1132 1133 string sourceDirectory = Path.GetDirectoryName(srcPath);1134 bool successfulDecompression = false;1135#if UNIX1136 successfulDecompression = ExpandArchive(Path.Combine(sourceDirectory, Path.GetFileName(srcPath)), destPath);1137#else1138 // Cabinet API doesn't handle the trailing back slash1139 if (!sourceDirectory.EndsWith('\\'))1140 {1141 sourceDirectory += "\\";1142 }1143 1144 if (!destPath.EndsWith('\\'))1145 {1146 destPath += "\\";1147 }1148 1149 successfulDecompression = CabinetExtractorFactory.GetCabinetExtractor().Extract(Path.GetFileName(srcPath), sourceDirectory, destPath);1150#endif1151 if (!successfulDecompression)1152 {1153 throw new UpdatableHelpSystemException("UnableToExtract", StringUtil.Format(HelpDisplayStrings.UnzipFailure),1154 ErrorCategory.InvalidOperation, null, null);1155 }1156 1157 string[] files = Directory.GetFiles(destPath);1158 if (files.Length == 1)1159 {1160 // If there is a single file1161 string file = Path.GetFileName(files[0]);1162 if (!string.IsNullOrEmpty(file) && file.Equals("placeholder.txt", StringComparison.OrdinalIgnoreCase))1163 {1164 // And that single file is named "placeholder.txt"1165 var fileInfo = new FileInfo(files[0]);1166 if (fileInfo.Length == 0)1167 {1168 // And if that single file has length 0, then we delete that file and no longer install help1169 needToCopy = false;1170 try1171 {1172 File.Delete(files[0]);1173 string directory = Path.GetDirectoryName(files[0]);1174 if (!string.IsNullOrEmpty(directory))1175 {1176 Directory.Delete(directory);1177 }1178 }1179 catch (FileNotFoundException)1180 { }1181 catch (DirectoryNotFoundException)1182 { }1183 catch (UnauthorizedAccessException)1184 { }1185 catch (System.Security.SecurityException)1186 { }1187 catch (ArgumentNullException)1188 { }1189 catch (ArgumentException)1190 { }1191 catch (PathTooLongException)1192 { }1193 catch (NotSupportedException)1194 { }1195 catch (IOException)1196 { }1197 }1198 }1199 }1200 else